| 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
import AVFoundation
try AVAudioSession.sharedInstance().setCategory(
.playback,
mode: .default,
options: [.mixWithOthers, .allowBluetooth]
)
try AVAudioSession.sharedInstance().setActive(true)
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()
import AVKit
let picker = AVInputPickerInteraction()
picker.delegate = self
myButton.addInteraction(picker)
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
.mixWithOthers
.duckOthers
.interruptSpokenAudioAndMixWithOthers
.allowBluetooth
.allowBluetoothA2DP
.bluetoothHighQualityRecording
.defaultToSpeaker
.allowAirPlay
Interruption Handling
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:
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
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:
player.pause()
case .newDeviceAvailable:
break
case .categoryChange:
break
default:
break
}
}
AVAudioEngine
Basic Pipeline
let engine = AVAudioEngine()
let player = AVAudioPlayerNode()
let reverb = AVAudioUnitReverb()
reverb.loadFactoryPreset(.largeHall)
reverb.wetDryMix = 50
engine.attach(player)
engine.attach(reverb)
engine.connect(player, to: reverb, format: nil)
engine.connect(reverb, to: engine.mainMixerNode, format: nil)
engine.prepare()
try engine.start()
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)
let inputNode = engine.inputNode
let format = inputNode.outputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { buffer, time in
guard let channelData = buffer.floatChannelData?[0] else { return }
let frameLength = Int(buffer.frameLength)
var sum: Float = 0
for i in 0..<frameLength {
sum += channelData[i] * channelData[i]
}
let rms = sqrt(sum / Float(frameLength))
let dB = 20 * log10(rms)
DispatchQueue.main.async {
self.levelMeter = dB
}
}
inputNode.removeTap(onBus: 0)
Format Conversion
let inputFormat = engine.inputNode.outputFormat(forBus: 0)
let outputFormat = AVAudioFormat(
commonFormat: .pcmFormatInt16,
sampleRate: 48000,
channels: 1,
interleaved: false
)!
let converter = AVAudioConverter(from: inputFormat, to: outputFormat)!
let outputBuffer = AVAudioPCMBuffer(
pcmFormat: outputFormat,
frameCapacity: AVAudioFrameCount(outputFormat.sampleRate * 0.1)
)!
var error: NSError?
converter.convert(to: outputBuffer, error: &error) { inNumPackets, outStatus in
outStatus.pointee = .haveData
return inputBuffer
}
Bit-Perfect Audio / DAC Output
iOS Behavior
iOS provides bit-perfect output by default to USB DACs — no resampling occurs. The DAC receives the source sample rate directly.
let player = AVAudioPlayerNode()
Avoiding Resampling
let hardwareSampleRate = AVAudioSession.sharedInstance().sampleRate
let format = AVAudioFormat(
standardFormatWithSampleRate: hardwareSampleRate,
channels: 2
)
USB DAC Routing
let currentRoute = AVAudioSession.sharedInstance().currentRoute
for output in currentRoute.outputs {
print("Output: \(output.portName), Type: \(output.portType)")
}
try AVAudioSession.sharedInstance().setPreferredInput(usbPort)
Sample Rate Considerations
| Source | iOS Behavior | Notes |
|---|
| 44.1 kHz | Passthrough | CD quality |
| 48 kHz | Passthrough | Video standard |
| 96 kHz | Passthrough | Hi-res |
| 192 kHz | Passthrough | Hi-res |
| DSD | Not supported | Use DoP or convert |
iOS 26+ Input Selection
AVInputPickerInteraction
Native input device selection with live metering:
import AVKit
class RecordingViewController: UIViewController {
let inputPicker = AVInputPickerInteraction()
override func viewDidLoad() {
super.viewDidLoad()
try? AVAudioSession.sharedInstance().setCategory(.playAndRecord)
try? AVAudioSession.sharedInstance().setActive(true)
inputPicker.delegate = self
selectMicButton.addInteraction(inputPicker)
}
@IBAction func selectMicTapped(_ sender: UIButton) {
inputPicker.present()
}
}
extension RecordingViewController: AVInputPickerInteractionDelegate {
}
Features:
- Live sound level metering
- Microphone mode selection
- System remembers selection per app
iOS 26+ AirPods High Quality Recording
LAV-microphone equivalent quality for content creators:
try AVAudioSession.sharedInstance().setCategory(
.playAndRecord,
options: [
.bluetoothHighQualityRecording,
.allowBluetoothA2DP
]
)
let captureSession = AVCaptureSession()
captureSession.configuresApplicationAudioSessionForBluetoothHighQualityRecording = true
Notes:
- Uses dedicated Bluetooth link optimized for AirPods
- Falls back to HFP if device doesn't support HQ mode
- Supports AirPods stem controls for start/stop recording
Spatial Audio Capture (iOS 26+)
First Order Ambisonics (FOA)
Record 3D spatial audio using device microphone array:
let audioInput = AVCaptureDeviceInput(device: audioDevice)
audioInput.multichannelAudioMode = .firstOrderAmbisonics
AVAssetWriter Spatial Audio Setup
let foaOutput = AVCaptureAudioDataOutput()
foaOutput.spatialAudioChannelLayoutTag = kAudioChannelLayoutTag_HOA_ACN_SN3D
let stereoOutput = AVCaptureAudioDataOutput()
stereoOutput.spatialAudioChannelLayoutTag = kAudioChannelLayoutTag_Stereo
let metadataGenerator = AVCaptureSpatialAudioMetadataSampleGenerator()
func captureOutput(_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection) {
metadataGenerator.append(sampleBuffer)
}
let metadataSample = metadataGenerator.createMetadataSample()
Output File Structure
Spatial audio files contain:
- Stereo AAC track — Compatibility fallback
- APAC track — Spatial audio (FOA)
- Metadata track — Audio Mix tuning parameters
File formats: .mov, .mp4, .qta (QuickTime Audio, iOS 26+)
ASAF / APAC (Apple Spatial Audio)
Overview
| Component | Purpose |
|---|
| ASAF | Apple Spatial Audio Format — production format |
| APAC | Apple Positional Audio Codec — delivery codec |
APAC Capabilities
- Bitrates: 64 kbps to 768 kbps
- Supports: Channels, Objects, Higher Order Ambisonics, Dialogue, Binaural
- Head-tracked rendering adaptive to listener position/orientation
- Required for Apple Immersive Video
Playback
let player = AVPlayer(url: spatialAudioURL)
player.play()
Platform Support
All Apple platforms except watchOS support APAC playback.
Audio Mix (Cinematic Framework)
Separate and remix speech vs ambient sounds in spatial recordings:
AVPlayer Integration
import Cinematic
let asset = AVURLAsset(url: spatialAudioURL)
let audioInfo = try await CNAssetSpatialAudioInfo(asset: asset)
let intensity: Float = 0.5
let style = CNSpatialAudioRenderingStyle.cinematic
let audioMix = audioInfo.audioMix(
effectIntensity: intensity,
renderingStyle: style
)
playerItem.audioMix = audioMix
Rendering Styles
| Style | Effect |
|---|
.cinematic | Balanced speech/ambient |
.studio | Enhanced speech clarity |
.inFrame | Focus on visible speakers |
| + 6 extraction modes | Speech-only, ambient-only stems |
AUAudioMix (Direct AudioUnit)
For apps not using AVPlayer:
let audioInfo = try await CNAssetSpatialAudioInfo(asset: asset)
let remixMetadata = audioInfo.spatialAudioMixMetadata as CFData
Common Patterns
Background Audio Playback
try AVAudioSession.sharedInstance().setCategory(.playback)
let nowPlayingInfo: [String: Any] = [
MPMediaItemPropertyTitle: "Song Title",
MPMediaItemPropertyArtist: "Artist",
MPNowPlayingInfoPropertyElapsedPlaybackTime: player.currentTime,
MPMediaItemPropertyPlaybackDuration: duration
]
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
Ducking Other Audio
try AVAudioSession.sharedInstance().setCategory(
.playback,
options: .duckOthers
)
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
Bluetooth Device Handling
try AVAudioSession.sharedInstance().setCategory(
.playAndRecord,
options: [.allowBluetooth, .allowBluetoothA2DP]
)
let route = AVAudioSession.sharedInstance().currentRoute
let hasBluetoothOutput = route.outputs.contains {
$0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP
}
Anti-Patterns
Wrong Category
try AVAudioSession.sharedInstance().setCategory(.ambient)
try AVAudioSession.sharedInstance().setCategory(.playback)
Missing Interruption Handling
NotificationCenter.default.addObserver(
forName: AVAudioSession.interruptionNotification,
)
Tap Memory Leaks
engine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { ... }
deinit {
engine.inputNode.removeTap(onBus: 0)
}
Format Mismatch Crashes
engine.connect(playerNode, to: mixerNode, format: wrongFormat)
engine.connect(playerNode, to: mixerNode, format: nil)
Forgetting to Activate Session
try AVAudioSession.sharedInstance().setCategory(.playback)
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