docs: condense implementation comments
Some checks failed
Build Linux Binaries / linux/amd64 (push) Has been cancelled
Build Linux Binaries / linux/arm64 (push) Has been cancelled

This commit is contained in:
2026-07-23 13:37:05 +02:00
parent 575e2907d0
commit 4f71b784fe
22 changed files with 102 additions and 507 deletions

View File

@@ -35,14 +35,7 @@ final class AudioSessionManager {
name: AVAudioSession.routeChangeNotification, object: nil)
}
/// The single end-to-end audio recovery path, driven by *intent* (`IOSAudioEngine.isConnected`)
/// not by session bookkeeping flags that can drift out of sync (e.g. an interruption ended
/// without `.shouldResume`, which used to leave `isSessionActive` false forever). Safe to call
/// speculatively: the underlying calls are idempotent (AVAudioSession.setActive(true),
/// `IOSAudioRouter.applyConfiguration` has a re-entrancy guard, `IOSAudioEngine.reconfigure`
/// no-ops when not connected). Call this whenever the audio environment changes in a way that
/// could have stopped the engine interruption end, route change, AVAudioEngine
/// configuration-change and we still want audio back.
/// Idempotently restores audio after an interruption or external route change.
func recoverAudio() {
guard IOSAudioEngine.shared.isConnected else { return }
do {
@@ -158,27 +151,9 @@ final class AudioSessionManager {
IOSAudioRouter.shared.refreshRoutes()
NotificationCenter.default.post(name: .voiceCatDeviceListChanged, object: nil)
// Recover audio on every externally-initiated route change. `.categoryChange`,
// `.routeConfigurationChange`, and `.override` are fired by our OWN calls:
// - `.categoryChange` / `.routeConfigurationChange` applyConfiguration()'s
// setCategory / setPreferredInput / ...
// - `.override` applyA2dpSpeakerFallback()'s overrideOutputAudioPort(.speaker),
// which fires on every AirPods disconnect (and reconnect) on an A2DP preset.
// Acting on any of these would create a tight ping-pong loop with the re-entrancy
// guard (handleRouteChange recoverAudio applyA2dpSpeakerFallback
// overrideOutputAudioPort .override routeChange recoverAudio ...). The
// `.override` skip is what fixes the AirPods-disconnect reinitialize loop: each
// iteration also calls IOSAudioEngine.reconfigure() rebuild() (a full
// stop/restart of AVAudioEngine), which is the audible cycling. IOSAudioRouter's
// guard is the backstop that bounds it to ONE extra iteration, but skipping these
// three reasons avoids even that, so we reconfigure only in response to genuine
// environmental changes.
//
// The recovery set below (oldDeviceUnavailable, newDeviceAvailable, wakeFromSleep,
// noSuitableRouteForCategory, unknown) covers headphone/AirPods/wired unplug-replug
// the previously-reported "audio dies when headphones disconnect" bug. If an
// override ever actually stops the AVAudioEngine, the
// AVAudioEngineConfigurationChange handler in IOSVoiceProcessingEngine catches it.
// Ignore notifications caused by our own configuration calls; rebuilding for them
// recursively emits more route changes. Engine-configuration notifications remain
// the recovery path if a self-initiated change actually stops AVAudioEngine.
if reason != .categoryChange && reason != .routeConfigurationChange && reason != .override {
recoverAudio()
}