Use for AVFoundation audio reference: AVAudioSession categories/modes/options, AVAudioEngine pipelines/taps/conversion, DAC output behavior, and iOS 26+ input/spatial audio features.
Install
npx skillscat add derklinke/codex-config/ios-avfoundation-ref Install via the SkillsCat registry.
SKILL.md
AVFoundation Audio Reference
Quick Start
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()AVAudioSession
Categories
| Category | Typical use | Silent switch | Background |
|---|---|---|---|
.ambient |
non-primary sounds | silenced | no |
.soloAmbient |
default ambient | silenced | no |
.playback |
music/podcast/video audio | ignored | yes |
.record |
capture-only | n/a | yes |
.playAndRecord |
call/chat/duplex | ignored | yes |
.multiRoute |
advanced multi-output | ignored | yes |
Modes
| Mode | Typical use |
|---|---|
.default |
general |
.voiceChat |
VoIP echo control |
.videoChat |
FaceTime-like |
.gameChat |
game voice |
.videoRecording |
camera recording |
.measurement |
minimal processing |
.moviePlayback |
video playback |
.spokenAudio |
spoken-word content |
Options
- Mixing:
.mixWithOthers,.duckOthers,.interruptSpokenAudioAndMixWithOthers - Bluetooth:
.allowBluetooth,.allowBluetoothA2DP,.bluetoothHighQualityRecording(iOS 26+) - Routing:
.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:
let optionsValue = (userInfo[AVAudioSessionInterruptionOptionKey] as? UInt) ?? 0
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) { player.play() }
@unknown default:
break
}
}Route changes
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 }
if reason == .oldDeviceUnavailable {
player.pause() // e.g., unplugged headphones
}
}AVAudioEngine
Basic graph
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()Common node roles
| Node | Role |
|---|---|
AVAudioPlayerNode |
playback |
AVAudioInputNode |
mic input |
AVAudioOutputNode |
output endpoint |
AVAudioMixerNode |
mixing |
AVAudioUnitEQ / Reverb / Delay / Distortion / TimePitch |
effects |
Tap for analysis
let input = engine.inputNode
let format = input.outputFormat(forBus: 0)
input.installTap(onBus: 0, bufferSize: 1024, format: format) { buffer, _ in
guard let ch = buffer.floatChannelData?[0] else { return }
let n = Int(buffer.frameLength)
var sum: Float = 0
for i in 0..<n { sum += ch[i] * ch[i] }
let rms = sqrt(sum / Float(n))
let dB = 20 * log10(rms)
DispatchQueue.main.async { self.levelMeter = dB }
}
input.removeTap(onBus: 0)Format conversion
let inputFormat = engine.inputNode.outputFormat(forBus: 0)
let outputFormat = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 48_000, channels: 1, interleaved: false)!
let converter = AVAudioConverter(from: inputFormat, to: outputFormat)!
let out = AVAudioPCMBuffer(pcmFormat: outputFormat, frameCapacity: AVAudioFrameCount(outputFormat.sampleRate * 0.1))!
var error: NSError?
converter.convert(to: out, error: &error) { _, status in
status.pointee = .haveData
return inputBuffer
}DAC / Bit-Perfect Output
- iOS typically passes source sample rate to compatible USB DACs without forced resampling.
- Match processing graph to hardware rate where possible.
let hwRate = AVAudioSession.sharedInstance().sampleRate
let format = AVAudioFormat(standardFormatWithSampleRate: hwRate, channels: 2)Route inspection
for output in AVAudioSession.sharedInstance().currentRoute.outputs {
print("Output: \(output.portName), type: \(output.portType)")
}| Source rate | Typical behavior |
|---|---|
| 44.1/48/96/192 kHz | passthrough on compatible path |
| DSD | unsupported directly |
iOS 26+ Input Selection
AVInputPickerInteraction
import AVKit
let picker = AVInputPickerInteraction()
picker.delegate = self
button.addInteraction(picker)
// on action: picker.present()Features: system input picker, live levels, per-app remembered selection.
iOS 26+ AirPods High-Quality Recording
try AVAudioSession.sharedInstance().setCategory(
.playAndRecord,
options: [.bluetoothHighQualityRecording, .allowBluetoothA2DP]
)
let capture = AVCaptureSession()
capture.configuresApplicationAudioSessionForBluetoothHighQualityRecording = trueFallback occurs on unsupported hardware.
Spatial Audio Capture (iOS 26+)
FOA capture
let audioInput = AVCaptureDeviceInput(device: audioDevice)
audioInput.multichannelAudioMode = .firstOrderAmbisonicsAVAssetWriter path
- capture FOA + stereo + metadata tracks
- use
AVCaptureSpatialAudioMetadataSampleGeneratorfor metadata samples
let foaOutput = AVCaptureAudioDataOutput()
foaOutput.spatialAudioChannelLayoutTag = kAudioChannelLayoutTag_HOA_ACN_SN3D
let stereoOutput = AVCaptureAudioDataOutput()
stereoOutput.spatialAudioChannelLayoutTag = kAudioChannelLayoutTag_StereoTypical file composition:
- stereo compatibility track
- APAC spatial track
- metadata track
ASAF / APAC
| Component | Purpose |
|---|---|
| ASAF | authoring/production format |
| APAC | positional delivery codec |
APAC supports channels/objects/HOA/dialogue/binaural, adapts to head tracking, and is used in immersive media workflows.
Playback is standard AVPlayer for supported files.
Cinematic Audio Mix
import Cinematic
let asset = AVURLAsset(url: spatialAudioURL)
let info = try await CNAssetSpatialAudioInfo(asset: asset)
let mix = info.audioMix(effectIntensity: 0.5, renderingStyle: .cinematic)
playerItem.audioMix = mixRendering styles include .cinematic, .studio, .inFrame, plus extraction-oriented modes.
For non-AVPlayer paths, apply metadata to audio units via spatialAudioMixMetadata.
Common Patterns
Background playback
try AVAudioSession.sharedInstance().setCategory(.playback)
// Info.plist: UIBackgroundModes includes audioDuck others
try AVAudioSession.sharedInstance().setCategory(.playback, options: .duckOthers)
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)Bluetooth handling
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, options: [.allowBluetooth, .allowBluetoothA2DP])
let route = AVAudioSession.sharedInstance().currentRoute
let hasBT = route.outputs.contains { $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP }Anti-Patterns
- Wrong category for feature intent (e.g., music app using
.ambient). - No interruption observer.
- Installing taps without removing them.
- Incompatible manual formats causing graph failures.
- Configuring session but never calling
setActive(true).
Resources
- WWDC: 2025-251, 2025-403, 2019-510
- Docs:
/avfoundation,/avkit,/cinematic - Targets: iOS 12+ core audio; iOS 26+ for input picker/spatial/HQ AirPods capture