I have this simple Swift code that uses an AVAudioEngine + AVAudioPlayerNode to play an audio file on loop.
When I start the app the audio plays on the laptop speakers. If I switch my computer's output to a HomePod mini, the audio on the laptop stops but never plays on the HomePod mini (the mini also doesn't light up).
If I stop the app and run it again - the audio plays on the HomePod mini. If I switch back to the laptop - the audio stops until I restart the app, etc.
It seems the problem is with switching the output device during playback, but I cannot understand how to fix this. Below is my code:
class AppDelegate: NSObject, NSApplicationDelegate {
    
    var engine = AVAudioEngine()
    var player = AVAudioPlayerNode()
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Attach audio player
        engine.attach(player)
        
        // Load audio file
        let filePath = Bundle.main.path(forResource: "sheep1.m4a", ofType: nil)!
        let fileURL = URL(fileURLWithPath: filePath)
        let file = try! AVAudioFile(forReading: fileURL)
        
        // Load the audio buffer
        let fileFormat = file.processingFormat
        let fileFrameCount = UInt32(file.length)
        let buffer = AVAudioPCMBuffer(pcmFormat: fileFormat, frameCapacity: fileFrameCount)
        try! file.read(into: buffer!, frameCount: fileFrameCount)
        
        // Connect to the mixer
        let mainMixer = engine.mainMixerNode
        engine.connect(player, to: mainMixer, format: file.processingFormat)
        // Start the engine
        try! engine.start()
        
        // Play the audio
        player.scheduleBuffer(buffer!, at: nil, options: .loops, completionHandler: nil)
        player.play()
    }
}
I heard back from Apple tech support and as Dad commented here when the audio engine configuration changes there is a AVAudioEngineConfigurationChange notification being sent.
At this point the nodes are detached and the audio setup needs to be re-constructed and the engine restarted to start playing audio on the new output device.
I'm including below the complete AppDelegate that tests the original premise of the question. On starting the app I'm calling both setupAudio() to load the audio and playAudio() to start the playback.
Additionally each time the audio engine configuration changes I'm calling playAudio() to restart the playback:
@main
class AppDelegate: NSObject, NSApplicationDelegate {
    
    var engine = AVAudioEngine()
    var player = AVAudioPlayerNode()
    // Load audio file
    let file = try! AVAudioFile(forReading: URL(fileURLWithPath: Bundle.main.path(forResource: "sheep1.m4a", ofType: nil)!))
    var buffer: AVAudioPCMBuffer!
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Attach audio player
        setupAudio()
        playAudio()
    }
    
    /// Call this only ONCE
    func setupAudio() {
        engine.attach(player)
        // Load the audio buffer
        let fileFormat = file.processingFormat
        let fileFrameCount = UInt32(file.length)
        buffer = AVAudioPCMBuffer(pcmFormat: fileFormat, frameCapacity: fileFrameCount)
        file.framePosition = .zero
        try! file.read(into: buffer!, frameCount: fileFrameCount)
        // Observe for changes in the audio engine configuration
        NotificationCenter.default.addObserver(self,
           selector: #selector(handleInterruption),
           name: NSNotification.Name.AVAudioEngineConfigurationChange,
           object: nil
        )
    }
    
    /// Call this every time you want to restart audio
    func playAudio() {
        // Connect to the mixer
        let mainMixer = engine.mainMixerNode
        engine.connect(player, to: mainMixer, format: file.processingFormat)
        // Start the engine
        try! engine.start()
        
        // Play the audio
        player.scheduleBuffer(buffer, at: nil, options: .loops, completionHandler: nil)
        player.play()
    }
    
    @objc func handleInterruption(notification: Notification) {
        playAudio()
    }
}
This code works for me on Big Sur.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With