Skip to content

Commit

Permalink
Adds macOS support, updates example project
Browse files Browse the repository at this point in the history
Also fixes a thread safety issue
  • Loading branch information
dimitris-c committed May 17, 2024
1 parent 47d3e5b commit 5ab306e
Show file tree
Hide file tree
Showing 20 changed files with 402 additions and 155 deletions.
8 changes: 8 additions & 0 deletions AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
9816A8BB2BC87BC200AD1299 /* AudioPlayerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */; };
984DE9552BDAE59C004B427A /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984DE9542BDAE59C004B427A /* Notifier.swift */; };
984DE9572BDAFC7E004B427A /* AudioPlayerControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984DE9562BDAFC7E004B427A /* AudioPlayerControlsView.swift */; };
989E08E72BF7A4E300599F17 /* PrefersTabNavigationEnvironmentKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989E08E62BF7A4E300599F17 /* PrefersTabNavigationEnvironmentKey.swift */; };
98BFB41A2BC97AF800E812C0 /* DisplayLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB4192BC97AF800E812C0 /* DisplayLink.swift */; };
98BFB41D2BCD7BB800E812C0 /* EqualizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */; };
98BFB41F2BCD814000E812C0 /* EqualizerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB41E2BCD814000E812C0 /* EqualizerService.swift */; };
Expand Down Expand Up @@ -63,6 +64,7 @@
9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerService.swift; sourceTree = "<group>"; };
984DE9542BDAE59C004B427A /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
984DE9562BDAFC7E004B427A /* AudioPlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerControlsView.swift; sourceTree = "<group>"; };
989E08E62BF7A4E300599F17 /* PrefersTabNavigationEnvironmentKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefersTabNavigationEnvironmentKey.swift; sourceTree = "<group>"; };
98BFB4192BC97AF800E812C0 /* DisplayLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayLink.swift; sourceTree = "<group>"; };
98BFB41B2BCAAD8A00E812C0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -176,6 +178,7 @@
children = (
98BFB4192BC97AF800E812C0 /* DisplayLink.swift */,
984DE9542BDAE59C004B427A /* Notifier.swift */,
989E08E62BF7A4E300599F17 /* PrefersTabNavigationEnvironmentKey.swift */,
);
path = Helpers;
sourceTree = "<group>";
Expand Down Expand Up @@ -289,6 +292,7 @@
9816A8BB2BC87BC200AD1299 /* AudioPlayerService.swift in Sources */,
984DE9572BDAFC7E004B427A /* AudioPlayerControlsView.swift in Sources */,
9806E8182BC5D12500757370 /* App.swift in Sources */,
989E08E72BF7A4E300599F17 /* PrefersTabNavigationEnvironmentKey.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -439,6 +443,8 @@
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioPlayer;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
Expand Down Expand Up @@ -470,6 +476,8 @@
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioPlayer;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableThreadSanitizer = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
Expand Down
2 changes: 1 addition & 1 deletion AudioPlayer/AudioPlayer/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func provideEqualizerService(playerService: AudioPlayerService) -> EqualizerServ

func provideAudioPlayerService() -> AudioPlayerService {
AudioPlayerService(
audioPlayer: provideDefaultAudioPlayer()
audioPlayerProvider: provideDefaultAudioPlayer
)
}

Expand Down
12 changes: 12 additions & 0 deletions AudioPlayer/AudioPlayer/Common/AddNewAudioURLView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ struct AddNewAudioURLView: View {
TextField(value: $audioUrl, format: urlStyle, prompt: nil, label: {
Text("Insert URL")
})
#if os(iOS)
.keyboardType(.URL)
#endif
.autocorrectionDisabled()
.textFieldStyle(RoundedBorderTextFieldStyle())
.onSubmit {
Expand All @@ -48,6 +50,7 @@ struct AddNewAudioURLView: View {
}
.foregroundStyle(Color.white)
}
.buttonStyle(.plain)
.disabled(audioUrl == nil)
.opacity(audioUrl == nil ? 0.5 : 1.0)
.padding(.horizontal, 16)
Expand All @@ -56,16 +59,25 @@ struct AddNewAudioURLView: View {
.clipShape(RoundedRectangle(cornerRadius: 16))
}
.navigationTitle("Add Audio URL")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
#if os(iOS)
ToolbarItem(placement: .topBarTrailing) {
Button {
dismiss()
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(Color.gray)
}
.buttonStyle(.plain)
}
#else
ToolbarItem(placement: .confirmationAction) {
Button("Done", action: dismiss.callAsFunction)
}
#endif
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions AudioPlayer/AudioPlayer/Common/AudioTrack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ struct AudioTrackView: View {
} else if track.status == .buffering {
ProgressView()
.progressViewStyle(.circular)
.frame(alignment: .center)
.scaleEffect(0.7)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Created by Dimitris Chatzieleftheriou on 26/04/2024.
//

import AVFoundation
import SwiftUI

struct AudioPlayerControls: View {
Expand All @@ -17,28 +18,41 @@ struct AudioPlayerControls: View {
VStack(alignment: .leading) {
HStack {
Button(action: { model.playPause() }) {
Image(systemName: model.isPlaying ? "pause.fill" : "play.fill")
Image(systemName: model.isPlaying ? "pause" : "play")
.symbolVariant(.fill)
.font(.title)
.imageScale(.small)
}
.buttonStyle(.plain)
.contentTransition(.symbolEffect(.replace))
Button(action: {
model.stop()
currentTrack = nil
}) {
Image(systemName: "stop.fill")
Image(systemName: "stop")
.symbolVariant(.fill)
.font(.title)
.imageScale(.small)
}
.buttonStyle(.plain)
.padding(.leading, 8)
Spacer()
Button(action: { model.mute() }) {
Image(systemName: model.isMuted ? "speaker.slash.fill" : "speaker.fill")
.font(.title)
.imageScale(.small)
HStack {
Slider(value: $model.volume)
.frame(width: 80)
.onChange(of: model.volume) { _, newValue in
model.update(volume: newValue)
}
Button(action: { model.mute() }) {
Image(systemName: model.iconForVolume)
.symbolVariant(model.isMuted || model.volume == 0 ? .slash.fill : .fill)
.foregroundStyle(.teal, .gray)
.font(.title.monospaced())
.imageScale(.small)
}
.buttonStyle(.plain)
.frame(width: 20, height: 20)
}
.frame(width: 20, height: 20)
.contentTransition(.symbolEffect(.replace))
}
.tint(.mint)
.padding(16)
Expand Down Expand Up @@ -118,6 +132,8 @@ extension AudioPlayerControls {
var isPlaying: Bool = false
var isMuted: Bool = false

var volume: Float = 0.5

var playbackRate: Double = 0.0

var currentTime: Double = 0
Expand All @@ -130,6 +146,19 @@ extension AudioPlayerControls {

var currentTrack: AudioTrack?

var iconForVolume: String {
if isMuted || volume == 0 {
return "speaker"
}
if volume < 0.4 {
return "speaker.wave.1"
} else if volume < 0.8 {
return "speaker.wave.2"
} else {
return "speaker.wave.3"
}
}

init(audioPlayerService: AudioPlayerService) {
self.audioPlayerService = audioPlayerService

Expand Down Expand Up @@ -204,6 +233,10 @@ extension AudioPlayerControls {
audioPlayerService.update(rate: rate)
}

func update(volume: Float) {
audioPlayerService.update(volume: volume)
}

func stop() {
isPlaying = false
audioPlayerService.stop()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
// Copyright © 2024 Decimal. All rights reserved.
//

#if os(iOS)
import UIKit
#else
import AppKit
#endif

import Foundation
import AudioStreaming

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,32 +51,49 @@ struct AudioPlayerView: View {
}
.ignoresSafeArea(.keyboard, edges: .bottom)
.navigationTitle("Audio Player")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
ToolbarItemGroup(placement: .topBarTrailing) {
#if os(iOS)
let placement: ToolbarItemPlacement = .topBarTrailing
#else
let placement: ToolbarItemPlacement = .automatic
#endif
ToolbarItemGroup(placement: placement) {
Button {
eqSheetIsShown.toggle()
} label: {
Image(systemName: "slider.horizontal.3")
}
.buttonStyle(.plain)
Button {
addNewAudioIsShown.toggle()
} label: {
Image(systemName: "plus")
}
.buttonStyle(.plain)
}
}
.sheet(isPresented: $eqSheetIsShown) {
EqualizerView(appModel: appModel)
#if os(iOS)
.presentationDetents([.medium])
#elseif os(macOS)
.frame(minWidth: 520, maxWidth: .infinity, minHeight: 400, maxHeight: .infinity)
#endif
}
.sheet(isPresented: $addNewAudioIsShown) {
AddNewAudioURLView(
onAddNewUrl: { url in
model.addNewAudioTrack(url: url)
}
)
#if os(iOS)
.presentationDetents([.height(150)])
#elseif os(macOS)
.frame(minWidth: 320, maxWidth: .infinity, minHeight: 140, maxHeight: .infinity)
#endif
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,11 @@ struct EqualizerView: View {
}
}
.navigationTitle("Equalizer")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
#if os(iOS)
ToolbarItem(placement: .topBarTrailing) {
Button {
dismiss()
Expand All @@ -95,6 +98,11 @@ struct EqualizerView: View {
.foregroundStyle(Color.gray)
}
}
#else
ToolbarItem(placement: .confirmationAction) {
Button("Done", action: dismiss.callAsFunction)
}
#endif
}
}
}
Expand Down
22 changes: 18 additions & 4 deletions AudioPlayer/AudioPlayer/Dependencies/AudioPlayerService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ final class AudioPlayerService {
var metadataReceivedNotifier = Notifier<[String: String]>()
var playingStartedStopped = Notifier<(started: Bool, AudioEntryId, AudioPlayerStopReason?)>()

init(audioPlayer: AudioPlayer) {
player = audioPlayer
private let audioPlayerProvider: () -> AudioPlayer

init(audioPlayerProvider: @escaping () -> AudioPlayer) {
self.audioPlayerProvider = audioPlayerProvider
player = audioPlayerProvider()
player.delegate = self

configureAudioSession()
Expand Down Expand Up @@ -83,6 +86,10 @@ final class AudioPlayerService {
player.rate = rate
}

func update(volume: Float) {
player.volume = volume
}

func add(_ node: AVAudioNode) {
player.attach(node: node)
}
Expand All @@ -104,12 +111,13 @@ final class AudioPlayerService {
}

private func recreatePlayer() {
player = AudioPlayer(configuration: .init(enableLogs: true))
player = audioPlayerProvider()
player.delegate = self
}

private func registerSessionEvents() {
// Note that a real app might need to observer other AVAudioSession notifications as well
#if os(iOS)
audioSystemResetObserver = NotificationCenter.default.addObserver(
forName: AVAudioSession.mediaServicesWereResetNotification,
object: nil,
Expand All @@ -118,35 +126,42 @@ final class AudioPlayerService {
self.configureAudioSession()
self.recreatePlayer()
}
#endif
}

private func configureAudioSession() {
#if os(iOS)
do {
print("AudioSession category is AVAudioSessionCategoryPlayback")
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, policy: .longFormAudio, options: [])
try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(0.1)
} catch let error as NSError {
print("Couldn't setup audio session category to Playback \(error.localizedDescription)")
}
#endif
}

private func activateAudioSession() {
#if os(iOS)
do {
print("AudioSession is active")
try AVAudioSession.sharedInstance().setActive(true, options: [])

} catch let error as NSError {
print("Couldn't set audio session to active: \(error.localizedDescription)")
}
#endif
}

private func deactivateAudioSession() {
#if os(iOS)
do {
print("AudioSession is deactivated")
try AVAudioSession.sharedInstance().setActive(false)
} catch let error as NSError {
print("Couldn't deactivate audio session: \(error.localizedDescription)")
}
#endif
}
}

Expand Down Expand Up @@ -187,4 +202,3 @@ extension AudioPlayerService: AudioPlayerDelegate {
delegate?.metadataReceived(metadata: metadata)
}
}

Loading

0 comments on commit 5ab306e

Please sign in to comment.