Sample CodeiOS, iPadOS, Mac CatalystReviewed 2026-07-21View on Apple Developer

Capturing Spatial Audio in your iOS app

At a glance

Item Summary
Purpose Capture first-order ambisonic audio, a stereo fallback, and spatial metadata into one movie, then play the recording.
App architecture SwiftUI composition with one observable capture owner and a separate playback view model.
Main patterns Observable model, AVFoundation delegate callbacks, queue confinement, and a small playback MVVM slice.
Project style One app target; files are named by primary view or model type with capture and playback kept separate.

Project structure

SpatialAudioDemo/
├── SpatialAudioDemoApp.swift       # Composition root
├── ContentView.swift               # Recording flow and waveform UI
├── AudioRecorder.swift             # Capture, writing, metadata, and meters
├── AudioPlayerView.swift           # Playback controls
├── AudioPlayerViewModel.swift      # AVPlayer state and observation
└── AudioVisualizerView.swift       # Small level visualization

Structure observations

  • AudioRecorder is the capture boundary; it intentionally combines session setup, writer setup, delegate processing, and waveform calculation.
  • Playback is a separate view/view-model pair, so capture resources do not leak into the player UI.
  • There is no app-defined service protocol or repository layer.

Overall architecture

Reference code

SpatialAudioDemo/ContentView.swift:14 and SpatialAudioDemo/ContentView.swift:82 — view-owned recorder

@State private var audioRecorder = AudioRecorder()

.onAppear() {
    audioRecorder.setupCaptureSession()
}

Interpretation

ContentView owns the feature lifecycle and sends user intents to AudioRecorder. The recorder is both observable app state and an AVFoundation adapter. A completed file URL is passed to the independently owned playback slice.

Ownership and state

Ownership evidence

SpatialAudioDemo/AudioRecorder.swift:17 — private capture and writer state

private var assetWriter: AVAssetWriter?
private var session: AVCaptureSession
private var stereoAudioDataOutput: AVCaptureAudioDataOutput?
private var spatialAudioDataOutput: AVCaptureAudioDataOutput?
private var sessionQueue: DispatchQueue
Owner Object or state Relationship Mutation authority
ContentView Recorder, timer, sheet state Creates and retains as SwiftUI state ContentView for UI state; recorder for capture state
AudioRecorder Session, outputs, writer inputs, metadata generator Creates and retains AudioRecorder, primarily on sessionQueue
AudioRecorder isRecording, amplitudes, output URL Publishes observable state Recorder callbacks; some UI code clears amplitudes
AudioPlayerView AudioPlayerViewModel Creates as @State View model methods and observers
AudioPlayerViewModel AVPlayer, observer token, subscriptions Creates and cleans up View model only

Class and protocol design

SpatialAudioDemo/AudioRecorder.swift:14 — framework protocol integration

@Observable
class AudioRecorder: NSObject,
    AVCaptureAudioDataOutputSampleBufferDelegate,
    @unchecked Sendable {
    // ...
}
Type Responsibility Depends on
AudioRecorder Configure audio capture, build the writer graph, process sample buffers, and publish meter state AVFoundation, CoreMedia, dispatch queues
AudioPlayerViewModel Load, observe, seek, play, and pause one audio file AVPlayer, Combine
ContentView Permission messaging, recording intent, timer, waveform, and player presentation AudioRecorder
LiveWaveformShape Convert amplitude history into a SwiftUI path SwiftUI

The sample uses AVFoundation’s delegate protocol, but defines no application protocol for interchangeable recorders or players. @unchecked Sendable is a concurrency assertion, not an access level.

Access control

Symbol Access Verified effect Likely rationale
Session, outputs, writer, writer inputs, and sessionQueue private Only AudioRecorder can reconfigure the capture/write graph. Preserves graph and queue invariants.
setupAssetWriterWithSpatialAndStereoAudioOutput private Only the first-buffer path can build writer inputs. Keeps a lifecycle helper out of the UI-facing API.
Player observer token, cancellables, and addPeriodicTimeObserver private AudioPlayerView cannot manipulate observation lifecycle. Prevents duplicate observers and centralizes cleanup.
ContentView recorder, timer, and sheet flags private State stays local to the view instance. Expresses view ownership.
AudioRecorder.fileURL declared public, effectively capped by internal enclosing type No other module can construct the internal AudioRecorder type as a public API. The explicit public is wider than this sample needs; likely documentation emphasis rather than a library boundary.
Unmodified app types and commands internal by default Collaborators in the app module can call them; external modules cannot. Suitable for a single-target sample.

Reference code

SpatialAudioDemo/AudioRecorder.swift:172 — hidden writer construction

private func setupAssetWriterWithSpatialAndStereoAudioOutput(
    _ spatialAudioOutput: AVCaptureAudioDataOutput,
    _ stereoAudioOutput: AVCaptureAudioDataOutput
) {
    // creates AVAssetWriter and its spatial, stereo, and metadata inputs
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Permission display, timer, and modal playback ContentView Presentation and feature flow
Capture-session configuration AudioRecorder.setupCaptureSession() Same owner as all mutable session resources
Spatial/stereo track associations AudioRecorder.setupAssetWriterWithSpatialAndStereoAudioOutput Writer invariant hidden behind one setup operation
Sample routing and metadata finalization AudioRecorder.captureOutput AVFoundation delegate boundary
RMS calculation and amplitude publication AudioRecorder.computeValuesForWaveFormUI Derived directly from captured stereo buffers
Playback observation and commands AudioPlayerViewModel Keeps AVPlayer mechanics out of SwiftUI layout code

Design patterns

Pattern Source evidence Purpose or tradeoff
Observable model SpatialAudioDemo/AudioRecorder.swift:14, SpatialAudioDemo/AudioPlayerViewModel.swift:12 SwiftUI reads state directly; the recorder consequently has both domain and presentation-facing responsibilities.
Delegate SpatialAudioDemo/AudioRecorder.swift:15, SpatialAudioDemo/AudioRecorder.swift:274 Receives synchronized framework callbacks without polling.
Queue confinement SpatialAudioDemo/AudioRecorder.swift:44, SpatialAudioDemo/AudioRecorder.swift:148 Serializes session/output work away from the main thread.
MVVM for playback SpatialAudioDemo/AudioPlayerView.swift:19, SpatialAudioDemo/AudioPlayerViewModel.swift:12 Separates controls from player lifecycle and observation.

Naming conventions

  • Types use role suffixes: View, ViewModel, and Recorder.
  • Commands use verbs and expose intent: setupCaptureSession, startRecording, stopRecording, seekToAndPlay.
  • Framework-backed properties name the owned object explicitly, such as assetWriterSpatialAudioInput.
  • Files match their main type; the small LiveWaveformShape stays beside its only consumer in ContentView.swift.

Architecture takeaways

  • Give one object exclusive ownership of a mutable capture/writer graph and its queue.
  • Keep playback resources separate from capture resources even in a small demonstration app.
  • Use private framework state and a small command surface to protect ordering requirements.
  • This is delegate-oriented framework integration, not a broadly protocol-oriented application design.

Source map

Source file Relevant symbols
SpatialAudioDemo/SpatialAudioDemoApp.swift SpatialAudioDemoApp
SpatialAudioDemo/ContentView.swift ContentView, LiveWaveformShape
SpatialAudioDemo/AudioRecorder.swift AudioRecorder, capture delegate methods
SpatialAudioDemo/AudioPlayerView.swift AudioPlayerView
SpatialAudioDemo/AudioPlayerViewModel.swift AudioPlayerViewModel
SpatialAudioDemo/AudioVisualizerView.swift AudioVisualizerView