AVAudioEngine when connected to Airplay

Background

We're writing a small recording app - think Voice Memos for the sake of argument. In our app, users should always record with the built-in iPhone microphone.

Our Problem

Our setup works fine when using just the speakers or in combination with Bluetooth headsets. However, it doesn't work well with Airplay. One of two things can happen:

  • The app records just silence
  • The app crashes when trying to connect the inputNode to the recorderNode (see code below), complaining that IsFormatSampleRateAndChannelCountValid == false

Our testing environment is an iPhone Xs, connected to an Airplay 2 compatible Sonos amp.

Code

We use the following code to set up the AVAudioSession (simplified, without error handling):

let session = AVAudioSession.sharedInstance()
try session.setCategory(.playAndRecord, options: [.defaultToSpeaker, .allowBluetoothA2DP, .allowAirPlay])
try AVAudioSession.sharedInstance().setActive(true)

Every time we record, we configure the audio session to use the built-in mic, and then create a fresh AVAudioEngine.

let session = AVAudioSession.sharedInstance()
let builtInMicInput = session.availableInputs!.first(where: { $0.portType == .builtInMic })

try session.setPreferredInput(builtInMicInput)
let sampleRate: Double = 44100
let numChannels: AVAudioChannelCount = isStereoEnabled ? 2 : 1
let recordingOutputFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: sampleRate, channels: numChannels, interleaved: false)!

let engine = AVAudioEngine()
let recorderNode = AVAudioMixerNode()

// This sets the input volume of those nodes in their destination node (mainMixerNode) to 0.
// The raw outputVolume of these nodes remains 1, so when you tap them you still get the samples.
// If you set outputVolume = 0 instead, the taps would only receives zeros.
recorderNode.volume = 0

engine.attach(recorderNode)

engine.connect(engine.mainMixerNode,  to: engine.outputNode,   format: engine.outputNode.inputFormat(forBus: 0))
engine.connect(recorderNode,      to: engine.mainMixerNode,  format: recordingOutputFormat)
engine.connect(engine.inputNode,    to: recorderNode,      format: engine.inputNode.inputFormat(forBus: 0))

// and later
try engine.start()

We install a tap on the recorderNode to save the recorded audio into a file. The tap works fine and is out of scope for this question, and thus not included here.

Questions

  • How do we route/configure the audio engine correctly to avoid this problem?
  • Do you have any advice on how to debug such issues in the future? Which variables/states should we inspect?

Thank you so much in advance!

Replies

When connecting to an "AirPlay 2", you loose audio Input as the corresponding AVAudioSession is incompatible with Recording/PlayAndRecord categories; at least in the AVAudioEngine context.

This is what we are experiencing at least when AirPlay 2 is getting connected. We have submitted Feedback FB8996889 to Apple on this issue after discussions during WWDC.

But some Apps seem to go around this by using low-level CoreAudio AUnits. Any feedback on this would be very welcome!