diff --git a/LeCountdown.xcodeproj/project.pbxproj b/LeCountdown.xcodeproj/project.pbxproj index 9bad2f6..88e34e5 100644 --- a/LeCountdown.xcodeproj/project.pbxproj +++ b/LeCountdown.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ C415D3F729C378D10037B215 /* QP01 0037 Tropical forest morning.wav in Resources */ = {isa = PBXBuildFile; fileRef = C415D3F629C378D10037B215 /* QP01 0037 Tropical forest morning.wav */; }; C415D3FB29C37A460037B215 /* QP01 0096 Wetland lake early morning.wav in Resources */ = {isa = PBXBuildFile; fileRef = C415D3FA29C37A460037B215 /* QP01 0096 Wetland lake early morning.wav */; }; C415D3FD29C37AA40037B215 /* QP01 0118 Riparian Zone thrush.wav in Resources */ = {isa = PBXBuildFile; fileRef = C415D3FC29C37AA40037B215 /* QP01 0118 Riparian Zone thrush.wav */; }; + C4286E962A14EC4E0070D075 /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4286E952A14EC4E0070D075 /* AppError.swift */; }; C42E96FB29E59E72005B1B8C /* BackgroundBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42E96FA29E59E72005B1B8C /* BackgroundBlurView.swift */; }; C42E96FD29E5B06D005B1B8C /* ActivityCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42E96FC29E5B06D005B1B8C /* ActivityCalendarView.swift */; }; C42E96FE29E5B5CD005B1B8C /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B6429A3C37D00CB4FBA /* Filter.swift */; }; @@ -370,6 +371,7 @@ C415D3FA29C37A460037B215 /* QP01 0096 Wetland lake early morning.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = "QP01 0096 Wetland lake early morning.wav"; sourceTree = ""; }; C415D3FC29C37AA40037B215 /* QP01 0118 Riparian Zone thrush.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = "QP01 0118 Riparian Zone thrush.wav"; sourceTree = ""; }; C418A14F298428CB00C22230 /* LeCountdown.0.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.1.xcdatamodel; sourceTree = ""; }; + C4286E952A14EC4E0070D075 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; C42E96FA29E59E72005B1B8C /* BackgroundBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundBlurView.swift; sourceTree = ""; }; C42E96FC29E5B06D005B1B8C /* ActivityCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityCalendarView.swift; sourceTree = ""; }; C42E970129E6B32B005B1B8C /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = ""; }; @@ -759,6 +761,7 @@ C4060DF4297AE9A7003FAB80 /* TimeInterval+Extensions.swift */, C473C33B29ACEC4F0056B38A /* Tip.swift */, C4BA2B7829A65C1400CB4FBA /* UIDevice+Extensions.swift */, + C4286E952A14EC4E0070D075 /* AppError.swift */, ); path = Utils; sourceTree = ""; @@ -1235,6 +1238,7 @@ C4F8B1BF298ACA0B005C86A5 /* StopwatchDialView.swift in Sources */, C438C807298195E600BF3EF9 /* Model+Extensions.swift in Sources */, C4BA2B6829A3C4AC00CB4FBA /* Context+Calculations.swift in Sources */, + C4286E962A14EC4E0070D075 /* AppError.swift in Sources */, C4E5D67A29B8C5A1008E7465 /* VolumeView.swift in Sources */, C4BA2B04299A42EF00CB4FBA /* NewDataView.swift in Sources */, C4BA2B4C299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */, diff --git a/LeCountdown/Conductor.swift b/LeCountdown/Conductor.swift index c628335..46f339f 100644 --- a/LeCountdown/Conductor.swift +++ b/LeCountdown/Conductor.swift @@ -26,7 +26,7 @@ class Conductor: ObservableObject { static let maestro: Conductor = Conductor() - @Published var soundPlayer: SoundPlayer? = nil + @ObservedObject var soundPlayer: SoundPlayer = SoundPlayer() fileprivate var _delayedSoundPlayers: [TimerID : DelaySoundPlayer] = [:] @@ -335,16 +335,8 @@ class Conductor: ObservableObject { } fileprivate func _playSound(_ filename: String, duration: TimeInterval? = nil) { - self.soundPlayer?.stop() do { - let soundFile = try SoundFile(fullName: filename) - let soundPlayer = SoundPlayer() - self.soundPlayer = soundPlayer - if let duration { - try soundPlayer.play(soundFile: soundFile, for: duration) - } else { - try soundPlayer.playSound(soundFile: soundFile) - } + try self.soundPlayer.playOrPauseSound(filename, duration: duration) } catch { Logger.error(error) // TODO: manage error @@ -371,9 +363,12 @@ class Conductor: ObservableObject { } } +// func isSoundPlaying(_ sound: Sound) -> Bool { +// return self.soundPlayer.isSoundPlaying(sound) +// } + func stopMainPlayersIfPossible() { - self.soundPlayer?.stop() - self.soundPlayer = nil + self.soundPlayer.stop() } // MARK: - Intent diff --git a/LeCountdown/Sound/SoundPlayer.swift b/LeCountdown/Sound/SoundPlayer.swift index ed289c7..7fe5d4f 100644 --- a/LeCountdown/Sound/SoundPlayer.swift +++ b/LeCountdown/Sound/SoundPlayer.swift @@ -34,12 +34,49 @@ enum SoundPlayerError : Error { case playReturnedFalse } -@objc class SoundPlayer: NSObject, AVAudioPlayerDelegate { +@objc class SoundPlayer: NSObject, AVAudioPlayerDelegate, ObservableObject { fileprivate var _player: AVAudioPlayer? fileprivate var _timer: Timer? = nil + @Published var currentFileName: String? = nil + + func playSound(_ sound: Sound) throws { + try self._playSound(sound) + } + + func playSound(_ sound: Sound, duration: TimeInterval) throws { + try self._playSound(sound, duration: duration) + } + + fileprivate func _playSound(_ sound: Sound, duration: TimeInterval? = nil) throws { + try self.playOrPauseSound(sound.fileName, duration: duration) + } + + func playOrPauseSound(_ file: String, duration: TimeInterval? = nil) throws { + + if file == self.currentFileName { + if self._player?.isPlaying ?? false { + self._player?.stop() + self.currentFileName = nil + } else { + self._player?.play() + self.currentFileName = file + } + return + } + + self.currentFileName = file + self._player?.stop() + let soundFile = try SoundFile(fullName: file) + if let duration { + try self.play(soundFile: soundFile, for: duration) + } else { + try self.playSound(soundFile: soundFile) + } + } + func play(soundFile: SoundFile, for duration: TimeInterval) throws { try self.playSound(soundFile: soundFile) self._timer = Timer(timeInterval: duration, repeats: false, block: { _ in @@ -51,10 +88,6 @@ enum SoundPlayerError : Error { guard let url = soundFile.url else { throw SoundPlayerError.missingResourceError(file: soundFile) } - -// let audioSession: AVAudioSession = AVAudioSession.sharedInstance() -// try audioSession.setCategory(.playback) -// try audioSession.setActive(true) let player = try AVAudioPlayer(contentsOf: url) player.prepareToPlay() @@ -63,19 +96,23 @@ enum SoundPlayerError : Error { self._player = player -// Logger.log("Plays \(url) on player: \(String(describing: self._player))") -// Logger.log("SoundPlayer > .deviceCurrentTime = \(player.deviceCurrentTime)") player.play() } func stop() { self._player?.stop() + self.currentFileName = nil } +// func isSoundPlaying(_ sound: Sound) -> Bool { +// return sound.fileName == self.currentFileName && (self._player?.isPlaying ?? false) +// } + // MARK: - Delegate func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + self.currentFileName = nil Conductor.maestro.deactivateAudioSessionIfPossible() self.stop() } diff --git a/LeCountdown/Utils/AppError.swift b/LeCountdown/Utils/AppError.swift new file mode 100644 index 0000000..708a4a8 --- /dev/null +++ b/LeCountdown/Utils/AppError.swift @@ -0,0 +1,28 @@ +// +// MyError.swift +// LeCountdown +// +// Created by Laurent Morvillier on 17/05/2023. +// + +import Foundation + +enum AppError: LocalizedError { + + case defaultError(error: Error) + + var errorDescription: String? { + switch self { + case .defaultError(let error): + return error.localizedDescription + } + } + + var errorMessage: String? { + switch self { + case .defaultError(let error): + return error.localizedDescription + } + } + +} diff --git a/LeCountdown/Views/Reusable/SoundSelectionView.swift b/LeCountdown/Views/Reusable/SoundSelectionView.swift index 2ae7949..7c2281f 100644 --- a/LeCountdown/Views/Reusable/SoundSelectionView.swift +++ b/LeCountdown/Views/Reusable/SoundSelectionView.swift @@ -28,18 +28,28 @@ struct PlaylistsView: View { struct PlaylistSectionView: View { @StateObject var model: SoundModel - + + @ObservedObject fileprivate var _player = SoundPlayer() + var playlist: Playlist - + + @State private var error: AppError? + @State private var isShowingError: Bool = false + var body: some View { Section { let sounds = SoundCatalog.main.sounds(for: self.playlist) ForEach(sounds) { sound in + + let isPlaying = sound.fileName == self._player.currentFileName + HStack { HStack { - Image(systemName: "play.circle") + let image = isPlaying ? "stop.circle" : "play.circle" + + Image(systemName: image) .foregroundColor(Color.accentColor) Text(sound.localizedString) }.onTapGesture { @@ -63,14 +73,36 @@ struct PlaylistSectionView: View { self.model.selectPlaylist(self.playlist, selected: selected) } } + .alert(isPresented: $isShowingError, error: error) { _ in + // buttons + } message: { error in + if let message = error.errorMessage { + Text(message) + } + } } +// fileprivate func _imageForSound(_ sound: Sound) -> String { +// if Conductor.maestro.isSoundPlaying(sound) { +// return "pause.circle" +// } else { +// return "play.circle" +// } +// } + fileprivate func _playSound(_ sound: Sound) { - if AppGuard.main.isSubscriber { - Conductor.maestro.playSound(sound) - } else { - Conductor.maestro.playSound(sound, duration: 30.0) + do { + if AppGuard.main.isSubscriber { + try self._player.playSound(sound) + // Conductor.maestro.playSound(sound) + } else { + try self._player.playSound(sound, duration: 30.0) + // Conductor.maestro.playSound(sound, duration: 30.0) + } + } catch { + self.error = AppError.defaultError(error: error) + Logger.error(error) } }