diff --git a/LoopFollow/Controllers/AlarmSound.swift b/LoopFollow/Controllers/AlarmSound.swift index fafd2bf6f..65817b209 100644 --- a/LoopFollow/Controllers/AlarmSound.swift +++ b/LoopFollow/Controllers/AlarmSound.swift @@ -100,6 +100,27 @@ class AlarmSound { audioPlayer = nil restoreSystemOutputVolume() + releaseAudioSession() + } + + /// Hands the audio session back once LoopFollow has nothing left to play, so + /// audio from other apps that was ducked or interrupted returns to full volume. + /// While Silent Tune's loop holds the session, it stays active with the + /// mixable category that loop uses. + static func releaseAudioSession() { + guard !isPlaying else { return } + + let session = AVAudioSession.sharedInstance() + do { + if MainViewController.shared?.backgroundTask.isPlaying == true { + try session.setCategory(.playback, mode: .default, options: .mixWithOthers) + } else { + try session.setActive(false, options: .notifyOthersOnDeactivation) + } + } catch { + let nsError = error as NSError + LogManager.shared.log(category: .alarm, message: "AlarmSound - audio session release failed [code \(nsError.code)]: \(error.localizedDescription)") + } } static func playTest() { @@ -152,11 +173,6 @@ class AlarmSound { // When repeatDelay > 0, we play once and then use the delegate to schedule the next play with delay audioPlayer!.numberOfLoops = (repeating && repeatDelay == 0) ? -1 : 0 - // Store existing volume - if systemOutputVolumeBeforeOverride == nil { - systemOutputVolumeBeforeOverride = AVAudioSession.sharedInstance().outputVolume - } - if !audioPlayer!.prepareToPlay() { LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play") } @@ -175,9 +191,7 @@ class AlarmSound { LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play") } - if Storage.shared.alarmConfiguration.value.overrideSystemOutputVolume { - MPVolumeView.setVolume(Storage.shared.alarmConfiguration.value.forcedOutputVolume) - } + applySystemOutputVolumeOverride() } catch { LogManager.shared.log(category: .alarm, message: "AlarmSound - unable to play sound; error: \(error)") } @@ -212,6 +226,7 @@ class AlarmSound { if audioPlayer!.play() { Observable.shared.alarmSoundPlaying.value = true + applySystemOutputVolumeOverride() } else { LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play (delayed repeat)") } @@ -260,6 +275,19 @@ class AlarmSound { } } + /// Raises the output volume of the current route to the configured alarm volume, + /// remembering the volume it had so it can be put back between repeats and on stop. + fileprivate static func applySystemOutputVolumeOverride() { + guard Storage.shared.alarmConfiguration.value.overrideSystemOutputVolume else { + return + } + + if systemOutputVolumeBeforeOverride == nil { + systemOutputVolumeBeforeOverride = AVAudioSession.sharedInstance().outputVolume + } + MPVolumeView.setVolume(Storage.shared.alarmConfiguration.value.forcedOutputVolume) + } + fileprivate static func restoreSystemOutputVolume() { guard Storage.shared.alarmConfiguration.value.overrideSystemOutputVolume else { return @@ -315,9 +343,12 @@ class AudioPlayerDelegate: NSObject, AVAudioPlayerDelegate { // If we're repeating with delay, schedule the next play if AlarmSound.repeatDelay > 0 { + AlarmSound.restoreSystemOutputVolume() AlarmSound.playNextWithDelay() } else { Observable.shared.alarmSoundPlaying.value = false + AlarmSound.restoreSystemOutputVolume() + AlarmSound.releaseAudioSession() } } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index dce0b6611..025ecb6f3 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -1266,18 +1266,6 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { extension MainViewController: AVSpeechSynthesizerDelegate { func speechSynthesizer(_: AVSpeechSynthesizer, didFinish _: AVSpeechUtterance) { - let appState = UIApplication.shared.applicationState - let isSilentTuneMode = Storage.shared.backgroundRefreshType.value == .silentTune - - if isSilentTuneMode, appState == .background { - LogManager.shared.log(category: .general, message: "Silent tune active in background; not deactivating session.", isDebug: true) - } else { - do { - try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) - LogManager.shared.log(category: .general, message: "Audio session deactivated after speech.", isDebug: true) - } catch { - LogManager.shared.log(category: .alarm, message: "Failed to deactivate audio session: \(error)") - } - } + AlarmSound.releaseAudioSession() } }