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