AudioPlayer: skip mixer→output reconnection when the output node reports an invalid format - #979
CharlesWiltgen wants to merge 2 commits into
Conversation
`handleAudioEngineConfigurationChange` reconnects the main mixer to the output node using the output node's hardware format. During a route change, audio interruption, or session deactivation that format can transiently report 0 Hz / 0 channels. `-[AVAudioEngine connect:to:format:]` then fails its `IsFormatSampleRateAndChannelCountValid(format)` precondition and raises an Objective-C exception. Because the handler is `noexcept`, the exception cannot propagate and the process terminates via `std::terminate` → SIGABRT. Only reconfigure the connection while the output node reports a usable format. AVAudioEngine posts a further configuration change notification once valid output hardware is available, and the connection is updated then. Sibling of the `[engine_ stop]` guard in sbooth#907 — the next precondition in the same method.
|
Thank you for this PR. Apparently this is a known quirk of I wonder whether instead of reconnecting everything but the output node it would be preferable to either 1) ignore the notification when an invalid output format is observed and wait for the follow-on notification with a valid format or 2) check whether the audio session is active and handle the notification differently in that case? |
|
IMHO a good path forward would be to merge this PR as it stands and treat (1) as an optional follow-up. I'd rule out (2), because (1) is close to what the patch already does: the guard skips the entire reconfiguration block, Thank you, @sbooth! [*Human written/reviewed, sanity-checked with AI] |
|
I finally got around to this: please give #1000 a try |
|
Fixed in #1000 |
(Hey @sbooth! Although this PR is heavily AI-assisted, I, a RealHuman™, spent a couple hours building it in order to fix a crashing bug that I'm seeing in my SFBAudioEngine-using app in the field. I've tried to make it as easy as possible to reproduce and merge. Please let me know if you need anything else!)
Summary
AudioPlayer::handleAudioEngineConfigurationChangereconnects the main mixer node to the output node using the output node's current output format. When the output route is being torn down — a route change, an interruption, or the audio session being deactivated — that format transiently reports 0 Hz and/or 0 channels.-[AVAudioEngine connect:to:format:]then fails its internal preconditionIsFormatSampleRateAndChannelCountValid(format)and raises an Objective-C exception. BecausehandleAudioEngineConfigurationChangeis declarednoexcept, the exception cannot unwind and the process is terminated (std::terminate→SIGABRT).This change guards the reconnection: the mixer→output connection is only rebuilt while the output node reports a usable format. When the format is invalid the reconfiguration is skipped and logged;
AVAudioEngineposts a freshAVAudioEngineConfigurationChangeNotificationonce valid output hardware is available, and that pass performs the reconnection. The existing "restart the engine if it was previously running" logic is unchanged, so playback still resumes.Crash signature
Root cause
The guard asks only whether the format differs, never whether it is usable. An invalid
0/0format always differs from the previously valid mixer format, so the mismatch branch is always taken and the invalid format flows straight intoconnect:to:format:.This is the sibling of the guard a few lines above —
— the next precondition in the same method that a transient hardware state can trip.
That guard (#907) made
disconnectNodeInput:survivable; this one makes the followingconnect:to:format:survivable.How it reproduces
My app is an iOS music player that deactivates its
AVAudioSessionwhile anSFBAudioPlayeris still attached, on backgrounding, so that Control Center reflects true rendering state rather thanplaybackRate. Deactivating the session tears down the output route, so-[outputNode outputFormatForBus:0]reports0 Hz / 0 chat exactly the moment the resulting configuration-change notification is delivered.That ordering appears to be why this has gone unreported: most clients keep the session active for the lifetime of the player, so they never observe the output node in this state.
Deterministic repro:
SFBAudioPlayer.(
[[AVAudioSession sharedInstance] setActive:NO ...]) — e.g. fromUIApplicationWillResignActiveNotification.process aborts.
Field data: a low but steady rate across several users, unchanged over four consecutive app releases, on both iOS 26.x and iOS 27, across four iPhone hardware generations, so it is neither OS-specific nor device-specific. Every sampled crash ends with the app transitioning to background, i.e. at session deactivation.
Testing
swift build --target CSFBAudioEnginesucceeds on the patched branch (Swift 6.4 / Xcode 27, macOS 27), with no new warnings.connect:to:format:in the handler, and the format originates from the hardware output bus.Notes
os_log_errorso the condition is visible rather than silent.