I’m using AVAudioEngine
to get a stream of AVAudioPCMBuffers
from the device’s microphone using the usual installTap(onBus:)
setup.
To distribute the audio stream to other parts of the program, I’m sending the buffers to a Combine publisher similar to the following:
private let publisher = PassthroughSubject<AVAudioPCMBuffer, Never>()
I’m starting to suspect I have some kind of concurrency or memory management issue with the buffers, because when consuming the buffers elsewhere I’m getting a range of crashes that suggest some internal pointer in a buffer is NULL (specifically, I’m seeing crashes in vDSP.convertElements(of:to:)
when I try to read samples from the buffer using, say, floatChannelData
).
These crashes are in production and fairly rare — I can’t reproduce them locally.
I never modify the audio buffers, only read them for analysis.
My question is: should it be possible to put AVAudioPCMBuffers
into a Combine pipeline? Does the AVAudioPCMBuffer
class not retain/release the underlying AudioBufferList
’s memory the way I’m assuming? Is this a fundamentally flawed approach?
My rule inside audio callback functions, blocks, or taps to always immediately copy any data to be processed out of the PCM buffers into your own private sample buffers. You can then safely publish your copy, and/or process the copied data at you own convenience.
This is because the underlying PCM buffers might be being updated in a separate RemoteIO Audio Unit thread running inside a hard real-time (Mach kernel) context. And thus its possible that some audio unit implementations might potentially be ignoring all concurrency locks in order to attempt to meet their hard real-time completion targets.