iOS Audio Disruptions
How the app keeps audio and the microphone running when iOS takes focus away, and why
Assets/Plugins/iOS/AppBackground.mm exists. The other half of the story — how the microphone
session is configured and recovered — lives in Assets/Plugins/iOS/Microphone.mm and its comments.
The disruption classes
iOS delivers overlapping but distinct signals depending on what took the app’s focus:
| Disruption | UIKit notifications | AVAudioSession interruption |
|---|---|---|
Control Center, notification shade, system alert |
|
None — the session stays active and audible. |
Home, app switch, screen lock |
|
On suspension the session is deactivated; an interruption may be delivered on return. |
Phone call, Siri, another app’s exclusive audio |
Depends on overlay |
|
The overlay class is the interesting one: nothing about it requires audio to stop — music apps play through Control Center — but two separate Unity behaviors used to stop it anyway, producing a paused app while the overlay was open and a double freeze with a quiet, misrouted window on return.
What Unity does on resign-active
Two independent mechanisms react to UIApplicationWillResignActiveNotification:
-
The trampoline pauses the player.
UnityAppController.mm applicationWillResignActive:callsUnitySetPlayerPause(kUnityPauseModePause, …)unlessiosUseCustomAppBackgroundBehavioris set. This freezes rendering, scripts and FMOD’s mixer. -
FMOD tears down its audio session. The engine’s CoreAudio backend (
fmod_output_coreaudio_objc.oinsideUnityRuntime.framework) registers its own observers for resign-active, become-active, enter-background, enter-foreground and screen-connect at audio init, unconditionally — no project setting gates them. Disassembly showsresignActive:is a single call toOutputCoreAudio::shutdownAudioSessionInternal()(which stops the outputAudioUnit) andbecomeActive:a single call torestoreAudioSessionInternal(false).
The restore path is what made returning from Control Center so rough. restoreAudioSessionInternal
runs setupAudioSession, which rewrites the shared AVAudioSession: setCategory:withOptions:,
setActive:, setPreferredSampleRate:, setPreferredIOBufferDuration: and
overrideOutputAudioPort:. Two consequences:
-
setCategory:withOptions:resets the session mode toDefault(documented AVFoundation side effect) and applies FMOD’s options — wiping theVoiceChatmode andDefaultToSpeakeroption thatMicrophone.mmmaintains.PlayAndRecordwithout those routes toward the built-in receiver at call gain, which is the "very quiet playback" window. -
Each session rewrite with a live
VoiceProcessingIOunit is a synchronous audio-hardware reconfiguration on the main thread — several hundred milliseconds. FMOD’s rewrite was one freeze; the microphone recovery inEnsureRunningdetecting the clobbered mode and rewriting the session back was the second.
Home → return only ever showed one freeze because FMOD’s restore runs on
WillEnterForeground, behind the launch snapshot, where it is invisible; Control Center has no
foreground transition, so both settles happened on screen.
What we do about it
AppBackground.mm removes both mechanisms for the overlay class and nothing else:
-
iosUseCustomAppBackgroundBehavior: 1(set inProjectSettings.assetand the iOS build profile) stops the trampoline from pausing the player on resign-active. Because something must still pause it on a real background,AppBackground.mmobservesUIApplicationDidEnterBackgroundNotificationand callsUnitySetPlayerPausethere. Resume is left to the trampoline:applicationDidBecomeActive:unpauses any pause the app did not set explicitly, which is the same path stock Unity uses for its own resign-active pause. -
At startup a constructor swizzles
-[OutputCoreAudioObjC resignActive:]andbecomeActive:to no-ops. The disassembled method bodies contain nothing but the shutdown/restore calls, so a no-op removes exactly that behavior.enterBackground:/enterForeground:are left intact, so real backgrounding keeps its teardown and restore, and genuine interruptions (calls, Siri) are still handled by FMOD’s andMicrophone.mm’s `AVAudioSessionInterruptionNotificationobservers.
Net behavior: overlays no longer touch the player or the audio session at all — playback and microphone capture continue while Control Center is open, and closing it has nothing to recover, so no freezes and no quiet window. Backgrounding behaves as it always has.
Why swizzling and not the alternatives
-
Linking FMOD’s globals (
FMOD::gOutputObjCis an exported data symbol; observers could be removed withremoveObserver:name:) fails hard at link time if Unity ever renames the symbol, and silently un-applies itself whenever FMOD re-runsactivate:— it re-registers the observers on every output init, including after a media-services reset. -
Patching the engine binary in the editor install or post-build achieves the same as the swizzle with strictly worse maintainability.
-
The swizzle resolves everything at runtime and fails open: if a Unity upgrade renames the class or selectors, the handlers log a warning and the app degrades to the old pause-on-overlay behavior instead of breaking the build. After an upgrade, check the device log for
[AppBackground]warnings and re-verify the Control Center round-trip.