1

Edit: From what I found out, starting the AVAudioEngine back up is causing the hang for 3 seconds after I enter back into my app and press the play button.

I am building a music player app with AVAudioEngine. When I press the play button in my app, it takes about 800ms to 3s of hang for audio to resume playing from my app (Main thread is blocked???). Is there a correct way to handle taking over the audio session of other apps? I couldn't find any topics here on stack over flow regarding this or even in apple docs.

Most of the interruption related docs are related to Siri or alarm and not related to music playing from other apps.

Here is part of my AudioManager class.

My app is a music player that plays songs through both the speaker and bluetooth headphones like AirPods.

class AudioPlaybackManager {

    init() {
        audioEngine.attach(playerNode)
        audioEngine.attach(eqNode)
        audioEngine.attach(pitchNode)
        audioEngine.attach(reverbNode)
      
        audioEngine.connect(playerNode, to: eqNode, format:  audioFile?.processingFormat)
        audioEngine.connect(eqNode, to: pitchNode, format: audioFile?.processingFormat) 
        audioEngine.connect(pitchNode, to: reverbNode, format: audioFile?.processingFormat)
        audioEngine.connect(reverbNode, to: audioEngine.mainMixerNode, format: audioFile?.processingFormat) 
    
        try? audioEngine.start()
    }

    func enableBackgroundPlay() {
        let session = AVAudioSession.sharedInstance()
                
        do {
            try session.setCategory(.playback, mode: .default)
            try session.setActive(true)
        } catch {
            print(error.localizedDescription)
        }
    }

func setupNotifications() {
        // Get the default notification center instance.
        let nc = NotificationCenter.default
        nc.addObserver(self,
                       selector: #selector(handleInterruption),
                       name: AVAudioSession.interruptionNotification,
                       object: AVAudioSession.sharedInstance())
    }

    @objc func handleInterruption(notification: Notification) {
        guard let userInfo = notification.userInfo,
              let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
              let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
                  return
              }
        
        // Switch over the interruption type.
        switch type {
        case .began:
            // An interruption began. Update the UI as necessary.
            print("Interruption Begun")
            musicPlayerControlsManager.isPlaying = false
            playerNode.pause()
        case .ended:
            // An interruption ended. Resume playback, if appropriate.

            guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
            let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
            
            print("Interruption Ended.")
            
            if options.contains(.shouldResume) {
                // An interruption ended. Resume playback.
                musicPlayerControlsManager.isPlaying = false 
                playerNode.play()
            } else {
                // An interruption ended. Don't resume playback.
            }

        default: ()
            
        }
    }
}
SwiftEnthusiast
  • 402
  • 2
  • 10

1 Answers1

2

You're on the right track in terms of handling the audio interruptions with AVAudioEngine. However, in your current implementation, when an interruption ends, you're directly trying to play the audio without checking if your AVAudioEngine is running or not.

When an audio interruption occurs (e.g., a phone call, another app playing audio, etc.), your AVAudioEngine stops running. Therefore, after the interruption, you need to restart your AVAudioEngine before playing the audio again.

Try modifying your interruption handling method like this:

@objc func handleInterruption(notification: Notification) {
    guard let userInfo = notification.userInfo,
        let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
        let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
            return
    }
    
    switch type {
    case .began:
        print("Interruption began")
        musicPlayerControlsManager.isPlaying = false
        playerNode.pause()
    case .ended:
        guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
        let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
        print("Interruption ended.")
        
        if options.contains(.shouldResume) {
            // Here, you should first check if the engine is running or not. If it's not running, you should restart it.
            if !audioEngine.isRunning {
                do {
                    try audioEngine.start()
                } catch {
                    print("Could not restart the audio engine: \(error)")
                }
            }
            musicPlayerControlsManager.isPlaying = true
            playerNode.play()
        }
    default: ()
    }
}

This should mitigate the delay you're experiencing as it ensures that the audio engine is running before attempting to play audio. It's important to handle this appropriately as not doing so might leave your audio engine in a non-running state after an interruption, which would then cause issues when trying to play audio.

If you're still noticing a delay, it may be due to the audio buffering process, especially if you're streaming the audio over the network. In that case, you might need to look into how you're buffering your audio data.

Please note that starting the AVAudioEngine or playing the audio may block the main thread if the audio file is not loaded completely or there are some issues with the audio file. You should consider performing these operations on a background thread if it's causing your UI to hang.

Emm
  • 1,963
  • 2
  • 20
  • 51
  • But the issue is when I press the play button in my app while Apple Music app is playing song in the background, the ended case of the interruption is never called. – SwiftEnthusiast May 27 '23 at 09:12
  • I’m playing local files from disk using the schedule file method of AVAudioPlayerNode. Do I have to reschedule the same music file when playing back? – SwiftEnthusiast May 27 '23 at 15:40