AVAudioEngineConfigurationChange Clearing AVPlayerNode

Hi all,

I am working on an app where I have live prompts playing, in addition to a voice channel that sometimes becomes active. Right now I am using two different AVAudioSession Configurations so what we only switch to a mic enabled mode when we actually need input from the mic. These are defined below.

When just using the device hardware, everything works as expected and the modes change and the playback continues as needed. However when using bluetooth devices such as AirPods where the switch from AD2P to HFP is needed, I am getting a AVAudioEngineConfigurationChange notification. In response I am tearing down the engine and creating a new one with the same 2 player nodes. This does work fine and there are no crashes, except all the audio I have scheduled on a player node has now been cleared. All the completion blocks marked with ".dataPlayedBack" return the second this event happens, and leaves me in a state where I now have a valid engine setup again but have no idea what actually played, or was errantly marked as such.

Is this the expected behavior when getting a configuration change notification?

Adding some information below to my audio graph for context:

All my parts of the graph, I disconnect when getting this event and do the same to the new engine

    private var inputEngine: AVAudioEngine
    private var audioEngine: AVAudioEngine
    private let voicePlayerNode: AVAudioPlayerNode
    private let promptPlayerNode: AVAudioPlayerNode

        audioEngine.attach(voicePlayerNode)
        audioEngine.attach(promptPlayerNode)

        audioEngine.connect(
            voicePlayerNode,
            to: audioEngine.mainMixerNode,
            format: voiceNodeFormat
        )

        audioEngine.connect(
            promptPlayerNode,
            to: audioEngine.mainMixerNode,
            format: nil
        )

An example of how I am scheduling playback, and where that completion is firing even if it didn't actually play.

      private func scheduleVoicePlayback(_ id: AudioPlaybackSample.Id, buffer: AVAudioPCMBuffer) async throws {
        guard !voicePlayerQueue.samples.contains(where: { $0 == id }) else {
            return
        }

        seprateQueue.append(buffer)

        if !isVoicePlaying {
            activateAudioSession()
        }

        voicePlayerQueue.samples.append(id)

        if !voicePlayerNode.isPlaying {
            voicePlayerNode.play()
        }

        if let convertedBuffer = buffer.convert(to: voiceNodeFormat) {
            await voicePlayerNode.scheduleBuffer(convertedBuffer, completionCallbackType: .dataPlayedBack)
        } else {
            throw AudioPlaybackError.failedToConvert
        }

        voiceSampleHasBeenPlayed(id)
    }

And lastly my audio session configuration if its useful.

extension AVAudioSession {
    static func setDefaultCategory() {
        do {
            try sharedInstance().setCategory(
                .playback,
                options: [
                    .duckOthers, .interruptSpokenAudioAndMixWithOthers
                ]
            )
        } catch {
            print("Failed to set default category? \(error.localizedDescription)")
        }
    }

    static func setVoiceChatCategory() {
        do {
            try sharedInstance().setCategory(
                .playAndRecord,
                options: [
                    .defaultToSpeaker,
                    .allowBluetooth,
                    .allowBluetoothA2DP,
                    .duckOthers,
                    .interruptSpokenAudioAndMixWithOthers
                ]
            )
        } catch {
            print("Failed to set category? \(error.localizedDescription)")
        }
    }
}
AVAudioEngineConfigurationChange Clearing AVPlayerNode
 
 
Q