I need to play an arbitrary tone that is generated based on the sample rate of the output device. User is required to connect wired or wireless headphones in order to listen to the audio.
Since modern headphones can have native sample rate of 44100 or 48000, I would need to somehow reinitialize my AVAudioPlayerNode
with different AVAudioFormat
. Without doing anything, the audio starts to sound distorted. However, I'm facing various deadlocks when trying to attach
, detach
, connect
or disconnectNodeOutput
. Sometimes it would lock the execution for the first time I try to switch headphones (I have 2 headphones connected at the same time, one generic headset with SR of 44100 and Airpods with SR of 48000), and rarely, it would freeze on the second try.
Here is my code:
private let engine = AVAudioEngine()
private let audioUnit = MyAudioUnit()
init() {
let audioSession = AVAudioSession.sharedInstance()
let sampleRate = audioSession.sampleRate
format = AVAudioFormat(
commonFormat: .pcmFormatFloat32,
sampleRate: sampleRate,
channels: AVAudioChannelCount(audioSession.outputNumberOfChannels),
interleaved: false
)!
engine.attach(audioUnit)
engine.connect(
audioUnit,
to: engine.mainMixerNode,
format: format
)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleInterruption),
name: Notification.Name.AVAudioEngineConfigurationChange,
object: engine
)
}
@objc
private func handleInterruption(_ notification: Notification) {
DispatchQueue.main.async {
let audioSession = AVAudioSession.sharedInstance()
let sampleRate = audioSession.sampleRate
self.format = AVAudioFormat(
commonFormat: .pcmFormatFloat32,
sampleRate: sampleRate,
channels: AVAudioChannelCount(audioSession.outputNumberOfChannels),
interleaved: false
)!
self.engine.detach(self.audioUnit)
self.engine.attach(self.audioUnit)
self.engine.connect(
self.audioUnit,
to: self.engine.mainMixerNode,
format: self.format
)
}
}
// method called on main thread only
// BTW, using audioUnit.stop() instead of pause() would also introduce a deadlock
func setActive(_ active: Bool) {
...
if active {
try! engine.start()
audioUnit.play()
} else {
audioUnit.pause()
engine.stop()
engine.reset()
}
}
I've tried numerous variants for "reconnecting" the AVAudioEngine
but they all ended up in the following deadlock:
I've also tried to leave it on background thread, but it doesn't matter. As soon as the app tries to use the AVAudioEngine
again, everything gets stuck.
So, is there a proper way to reconnect the AVAudioPlayerNode
in order to update the sample rate? Or maybe it is possible to not depend on headphones native sample rate and make the audio sound normally on a static sample rate? Thanks in advance!