Automatic Audio Switching Between Devices
Connected AirPods — audio switches to them. Disconnected — returns to speaker. Connected Bluetooth headset during call — call doesn't interrupt. Sounds like basic OS functionality, but in production without explicit AVAudioSession event handling everything works unpredictably.
How iOS Manages Audio Routing
AVAudioSession — central object. By default iOS switches output device when route changes. Problem — app may not know about it, and current AVAudioPlayer or AVAudioEngine continues on "old" route until next playback operation.
For explicit control — AVAudioSession.routeChangeNotification:
NotificationCenter.default.addObserver(
self,
selector: #selector(handleRouteChange(_:)),
name: AVAudioSession.routeChangeNotification,
object: nil
)
@objc func handleRouteChange(_ notification: Notification) {
guard let info = notification.userInfo,
let reasonValue = info[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue)
else { return }
switch reason {
case .newDeviceAvailable:
// AirPods connected — switch to them
resumePlaybackIfNeeded()
case .oldDeviceUnavailable:
// Headphones disconnected — pause or switch to speaker
if let previousRoute = info[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
let wasHeadphones = previousRoute.outputs.contains {
$0.portType == .headphones || $0.portType == .bluetoothA2DP
}
if wasHeadphones { pausePlayback() }
}
case .categoryChange:
reconfigureEngine()
default: break
}
}
oldDeviceUnavailable with pause — standard behavior users expect (Spotify, Apple Music). Without pause audio keeps playing in speaker after accidental headphone disconnection.
AirPods and Automatic Device Switching
AirPods Pro/Max support Automatic Switching — transition between iPhone, iPad, Mac. App can't control this switching but can react to consequences. When AirPods switch between devices app gets routeChangeNotification with reason override or categoryChange.
Subtlety: after route change AVAudioSession.currentRoute updates not instantly. On oldDeviceUnavailable still 50–100ms route shows old device. Need short Task.sleep(nanoseconds: 100_000_000) or check on next runloop cycle.
AVAudioEngine: Rebuilding Graph on Route Change
If app uses AVAudioEngine with effects (equalizer, reverb), route change can reset session. Sign — AVAudioEngine.isRunning returns false after routeChangeNotification.
Correct pattern: subscribe to AVAudioEngineConfigurationChange and reconnect graph:
NotificationCenter.default.addObserver(
forName: .AVAudioEngineConfigurationChange,
object: audioEngine, queue: .main
) { [weak self] _ in
self?.rebuildAudioGraph()
try? self?.audioEngine.start()
}
rebuildAudioGraph() — disconnect all nodes, change outputNode (now points to new device), reconnect. Without this step AVAudioPlayerNode continues playing but silent — quiet, no errors in logs.
Android: AudioManager and AudioDeviceCallback
On Android route management via AudioManager.AudioDeviceCallback:
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
audioManager.registerAudioDeviceCallback(object : AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<AudioDeviceInfo>) {
val bluetooth = addedDevices.firstOrNull {
it.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP ||
it.type == AudioDeviceInfo.TYPE_BLE_HEADSET
}
bluetooth?.let { switchToDevice(it) }
}
override fun onAudioDevicesRemoved(removedDevices: Array<AudioDeviceInfo>) {
pauseIfHeadphonesRemoved(removedDevices)
}
}, Handler(Looper.getMainLooper()))
AudioManager.setPreferredDevice() (API 28+) allows forcing device choice. Android 12+ added setCommunicationDevice() specifically for calls — don't confuse with regular playback.
Common Mistakes
- Don't call
AVAudioSession.setActive(true)after reconfig — get silent playback without error - Don't handle
categoryChangeon incoming call — phone call changes session category, after finish need restore it - Change route on background thread —
AVAudioSessionoperations must be on main thread or explicitly synchronized
Timeline
Basic route change handling (iOS): 3–5 days. Full implementation with AVAudioEngine, Android support, all scenarios (calls, BT headsets, AirPods): 2–3 weeks. Cost calculated individually.







