Pause engine before reconfiguring graph in configuration-change handler#906
Pause engine before reconfiguring graph in configuration-change handler#906CharlesWiltgen wants to merge 1 commit into
Conversation
…-change handler
The handler at AudioPlayer.mm:2199 assumes "AVAudioEngine stops itself
when a configuration change occurs" and clears its internal flags on
that basis. On iOS 26.4.x the notification can be delivered while
engine_.isRunning is still true; disconnectNodeInput: then raises
NSException ("required condition is false: !IsRunning()") which
propagates through this noexcept C++ frame to std::terminate.
Pause the engine before reconfiguring, and wrap the AVFAudio calls in
@try/@catch so any future precondition violations bail out gracefully.
The next play() rebuilds the graph from scratch.
Observed in production on iPhone 16 / iOS 26.4.2 (TestFlight crash log
attached to the upstream issue).
|
This is an interesting issue. Thank you for the report! According to Apple's documentation:
which is the basis of my comment AVAudioEngine stops itself when a configuration change occurs. The fact that the documented behavior is not what you're observing is puzzling to say the least. I'll post an inquiry on Apple's developer forums to see if I can get any clarification on behavioral changes. |
|
Link to developer forums inquiry: https://developer.apple.com/forums/thread/825253 |
|
I've drafted #907 in an attempt to address this issue but I'm going to wait for more information (hopefully) before merging. I'm reluctant to add ObjC exception handling code to catch internal AVFAudio assertions for preconditions and such. |
|
Understood, thank you! |
|
No reply from Apple yet. Is it possible that the I think the safest thing to do is to stop the audio engine from within the configuration change notification once |
|
Okay, cool! That addresses the observed It does mean that unguarded calls to |
[Hey @sbooth! This is obviously an AI-assisted PR, but I wanted to let you know that I'm Human™ and that I've done my best to validate that this addresses a real (if rare) issue. Thank you for SFBAudioEngine, and I hope this is useful!]
Problem
sfb::AudioPlayer::handleAudioEngineConfigurationChange(AudioPlayer.mm:2199) assumes:// AVAudioEngine stops itself when a configuration change occurs…and proceeds to clear its internal
Flags::engineIsRunning | Flags::isPlayingflags on that basis, then calls[engine_ disconnectNodeInput:outputNode bus:0]if the output format has changed.On iOS 26.4.x this assumption does not hold universally. The configuration-change notification can be delivered while
engine_.isRunningis stilltrue.disconnectNodeInput:then raises anNSException:Because the surrounding C++ method is
noexceptwith no@try/@catch, the exception propagates through C++ frames toCPPExceptionTerminate()→std::terminate()→ SIGABRT.Crash report
Observed on iPhone 16 / iOS 26.4.2, ARM64, TestFlight build of a downstream app. Stack:
App had been running ~5.5 hours before the crash, consistent with an audio-session event trigger (interruption, route change, headphone connect/disconnect, CarPlay) rather than a startup condition.
Fix
Two changes inside the
engineMutex_-held block:engine_.isRunningis stilltruewhen the handler runs, call[engine_ pause]beforedisconnectNodeInput:. This addresses the root cause — the documented "engine auto-stops" invariant doesn't hold here.@try/@catch (NSException *). Defensive belt-and-suspenders against any future precondition that AVFAudio decides to enforce. On catch, log viaos_log_errorandreturn. The nextplay()rebuilds the graph from scratch, so bailout state is recoverable.The same
@try/@catchis applied around the subsequent[engine_ startAndReturnError:&startError]for symmetry — that call also dispatches into AVFAudio internals that can raise.Behavior changes
engine_.pauseis called first if the engine is unexpectedly still running.pausepreserves engine state, so the existingstartAndReturnError:at line 2257 still resumes correctly.std::terminate(); now logged and the handler bails. The engine's flags have already been cleared (matching the pre-patch state assumption that the engine had stopped). The nextplay()rebuilds the graph.Tested
main(ab6a858).Related