--- name: axiom-avfoundation-ref description: Reference — AVFoundation audio APIs, AVAudioSession categories/modes, AVAudioEngine pipelines, bit-perfect DAC output, iOS 26+ spatial audio capture, ASAF/APAC, Audio Mix with Cinematic framework license: MIT metadata: version: "1.0.0" --- # AVFoundation Audio Reference ## Quick Reference ```swift // AUDIO SESSION SETUP import AVFoundation try AVAudioSession.sharedInstance().setCategory( .playback, // or .playAndRecord, .ambient mode: .default, // or .voiceChat, .measurement options: [.mixWithOthers, .allowBluetooth] ) try AVAudioSession.sharedInstance().setActive(true) // AUDIO ENGINE PIPELINE let engine = AVAudioEngine() let player = AVAudioPlayerNode() engine.attach(player) engine.connect(player, to: engine.mainMixerNode, format: nil) try engine.start() player.scheduleFile(audioFile, at: nil) player.play() // INPUT PICKER (iOS 26+) import AVKit let picker = AVInputPickerInteraction() picker.delegate = self myButton.addInteraction(picker) // In button action: picker.present() // AIRPODS HIGH QUALITY (iOS 26+) try AVAudioSession.sharedInstance().setCategory( .playAndRecord, options: [.bluetoothHighQualityRecording, .allowBluetoothA2DP] ) ``` --- ## AVAudioSession ### Categories | Category | Use Case | Silent Switch | Background | |----------|----------|---------------|------------| | `.ambient` | Game sounds, not primary | Silences | No | | `.soloAmbient` | Default, interrupts others | Silences | No | | `.playback` | Music player, podcast | Ignores | Yes | | `.record` | Voice recorder | — | Yes | | `.playAndRecord` | VoIP, voice chat | Ignores | Yes | | `.multiRoute` | DJ apps, multiple outputs | Ignores | Yes | ### Modes | Mode | Use Case | |------|----------| | `.default` | General audio | | `.voiceChat` | VoIP, reduces echo | | `.videoChat` | FaceTime-style | | `.gameChat` | Voice chat in games | | `.videoRecording` | Camera recording | | `.measurement` | Flat response, no processing | | `.moviePlayback` | Video playback | | `.spokenAudio` | Podcasts, audiobooks | ### Options ```swift // Mixing .mixWithOthers // Play with other apps .duckOthers // Lower other audio while playing .interruptSpokenAudioAndMixWithOthers // Pause podcasts, mix music // Bluetooth .allowBluetooth // HFP (calls) .allowBluetoothA2DP // High quality stereo .bluetoothHighQualityRecording // iOS 26+ AirPods recording // Routing .defaultToSpeaker // Route to speaker (not receiver) .allowAirPlay // Enable AirPlay ``` ### Interruption Handling ```swift NotificationCenter.default.addObserver( forName: AVAudioSession.interruptionNotification, object: nil, queue: .main ) { notification in guard let userInfo = notification.userInfo, let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return } switch type { case .began: // Pause playback player.pause() case .ended: guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return } let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) if options.contains(.shouldResume) { player.play() } @unknown default: break } } ``` ### Route Change Handling ```swift NotificationCenter.default.addObserver( forName: AVAudioSession.routeChangeNotification, object: nil, queue: .main ) { notification in guard let userInfo = notification.userInfo, let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt, let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return } switch reason { case .oldDeviceUnavailable: // Headphones unplugged — pause playback player.pause() case .newDeviceAvailable: // New device connected break case .categoryChange: // Category changed by system or another app break default: break } } ``` --- ## AVAudioEngine ### Basic Pipeline ```swift let engine = AVAudioEngine() // Create nodes let player = AVAudioPlayerNode() let reverb = AVAudioUnitReverb() reverb.loadFactoryPreset(.largeHall) reverb.wetDryMix = 50 // Attach to engine engine.attach(player) engine.attach(reverb) // Connect: player → reverb → mixer → output engine.connect(player, to: reverb, format: nil) engine.connect(reverb, to: engine.mainMixerNode, format: nil) // Start engine.prepare() try engine.start() // Play file let url = Bundle.main.url(forResource: "audio", withExtension: "m4a")! let file = try AVAudioFile(forReading: url) player.scheduleFile(file, at: nil) player.play() ``` ### Node Types | Node | Purpose | |------|---------| | `AVAudioPlayerNode` | Plays audio files/buffers | | `AVAudioInputNode` | Mic input (engine.inputNode) | | `AVAudioOutputNode` | Speaker output (engine.outputNode) | | `AVAudioMixerNode` | Mix multiple inputs | | `AVAudioUnitEQ` | Equalizer | | `AVAudioUnitReverb` | Reverb effect | | `AVAudioUnitDelay` | Delay effect | | `AVAudioUnitDistortion` | Distortion effect | | `AVAudioUnitTimePitch` | Time stretch / pitch shift | ### Installing Taps (Audio Analysis) ```swift let inputNode = engine.inputNode let format = inputNode.outputFormat(forBus: 0) inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { buffer, time in // Process audio buffer guard let channelData = buffer.floatChannelData?[0] else { return } let frameLength = Int(buffer.frameLength) // Calculate RMS level var sum: Float = 0 for i in 0..UIBackgroundModes // audio // 3. Set Now Playing info (recommended) let nowPlayingInfo: [String: Any] = [ MPMediaItemPropertyTitle: "Song Title", MPMediaItemPropertyArtist: "Artist", MPNowPlayingInfoPropertyElapsedPlaybackTime: player.currentTime, MPMediaItemPropertyPlaybackDuration: duration ] MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo ``` ### Ducking Other Audio ```swift try AVAudioSession.sharedInstance().setCategory( .playback, options: .duckOthers ) // When done, restore others try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) ``` ### Bluetooth Device Handling ```swift // Allow all Bluetooth try AVAudioSession.sharedInstance().setCategory( .playAndRecord, options: [.allowBluetooth, .allowBluetoothA2DP] ) // Check current Bluetooth route let route = AVAudioSession.sharedInstance().currentRoute let hasBluetoothOutput = route.outputs.contains { $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP } ``` --- ## Anti-Patterns ### Wrong Category ```swift // WRONG — music player using ambient (silenced by switch) try AVAudioSession.sharedInstance().setCategory(.ambient) // CORRECT — music needs .playback try AVAudioSession.sharedInstance().setCategory(.playback) ``` ### Missing Interruption Handling ```swift // WRONG — no interruption observer // Audio stops on phone call and never resumes // CORRECT — always handle interruptions NotificationCenter.default.addObserver( forName: AVAudioSession.interruptionNotification, // ... handle began/ended ) ``` ### Tap Memory Leaks ```swift // WRONG — tap installed, never removed engine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { ... } // CORRECT — remove tap when done deinit { engine.inputNode.removeTap(onBus: 0) } ``` ### Format Mismatch Crashes ```swift // WRONG — connecting nodes with incompatible formats engine.connect(playerNode, to: mixerNode, format: wrongFormat) // Crash! // CORRECT — use nil for automatic format negotiation, or match exactly engine.connect(playerNode, to: mixerNode, format: nil) ``` ### Forgetting to Activate Session ```swift // WRONG — configure but don't activate try AVAudioSession.sharedInstance().setCategory(.playback) // Audio doesn't work! // CORRECT — always activate try AVAudioSession.sharedInstance().setCategory(.playback) try AVAudioSession.sharedInstance().setActive(true) ``` --- ## Resources **WWDC**: 2025-251, 2025-403, 2019-510 **Docs**: /avfoundation, /avkit, /cinematic --- **Targets:** iOS 12+ (core), iOS 26+ (spatial features) **Frameworks:** AVFoundation, AVKit, Cinematic (iOS 26+) **History:** See git log for changes