Sample CodeReviewed 2026-07-21View on Apple Developer

Performing offline audio processing

At a glance

Item Summary
Purpose Renders a source file through a reverb effect faster than real time and writes the result to disk.
App architecture A single playground page implements a top-level load-configure-render-save pipeline.
Main patterns Linear data pipeline, manually driven rendering, status-based control flow.
Project style Code-light archive: one executable playground page plus an audio resource and explanatory images; no app target or custom classes.

Project structure

├── contents.xcplayground
├── Pages/
│   ├── Sample Code.xcplaygroundpage/Contents.swift
│   └── LICENSE.xcplaygroundpage/Contents.swift
└── Resources/
    ├── Rhythm.caf
    ├── offline.png
    └── realtime.png

Structure observations

  • The architecture is intentionally procedural and pedagogical; top-level constants become the shared pipeline state.
  • The single source page interleaves documentation and executable stages instead of defining reusable production types.

Overall architecture

Reference code

Pages/Sample Code.xcplaygroundpage/Contents.swift:59 — top-level code builds the processing graph explicitly.

let engine = AVAudioEngine()
let player = AVAudioPlayerNode()
let reverb = AVAudioUnitReverb()
engine.attach(player)
engine.attach(reverb)
reverb.loadFactoryPreset(.mediumHall)
engine.connect(player, to: reverb, format: format)
engine.connect(reverb, to: engine.mainMixerNode, format: format)
player.scheduleFile(sourceFile, at: nil)

Interpretation

The page shows a data-flow architecture rather than an object graph. The app, represented by top-level playground code, owns every resource and advances the engine manually until the source duration is exhausted.

Ownership and state

Ownership evidence

Pages/Sample Code.xcplaygroundpage/Contents.swift:108 — the page creates and retains the render buffer and destination file for the loop.

let buffer = AVAudioPCMBuffer(
    pcmFormat: engine.manualRenderingFormat,
    frameCapacity: engine.manualRenderingMaximumFrameCount)!
let outputFile: AVAudioFile
Owner Object or state Relationship Mutation authority
Playground page Files, engine, nodes, buffer Top-level creation and lifetime Sequential page code
AVAudioEngine Attached graph and manual sample time Framework-managed after configuration Playground through engine API
Output file Processed frames Receives each successful buffer Render loop

Class and protocol design

There are no custom classes or protocols. The sample composes AVFAudio framework types directly because its teaching goal is the manual-rendering contract, not an extensible app architecture.

Type Responsibility Depends on
AVAudioFile Source and destination persistence File URLs and audio settings
AVAudioEngine Graph composition and offline rendering Player, reverb, mixer
AVAudioPCMBuffer Bounded transfer unit between engine and output file Manual-rendering format

Access control

The playground declares only top-level constants, so Swift’s default access is internal; there are no private, fileprivate, public, or open app symbols. The meaningful boundary is lexical/sequential: later stages can use earlier resources, and nothing is exported as a reusable module API.

Reference code

Pages/Sample Code.xcplaygroundpage/Contents.swift:44 — source resources are immutable top-level bindings after initialization.

let sourceFile: AVAudioFile
let format: AVAudioFormat

Logic ownership and placement

Logic Owning type or file Placement rationale
Source loading Top-level page stage Establishes format for all later stages.
Graph configuration Top-level page stage Makes the tutorial data flow visible.
Manual-rendering setup Top-level page stage Switches the engine contract before start.
Render/retry/write loop Top-level page stage Owns progress and destination writes.
Final cleanup/reveal Top-level page tail Ends resources after the pipeline completes.

Design patterns

Pattern Source evidence Purpose or tradeoff
Linear pipeline Pages/Sample Code.xcplaygroundpage/Contents.swift:38 Makes dependencies and stage order explicit.
Manual pump Pages/Sample Code.xcplaygroundpage/Contents.swift:126 The caller, rather than hardware, drives audio production.
Status-driven retry Pages/Sample Code.xcplaygroundpage/Contents.swift:133 Distinguishes success, temporary inability, missing input, and fatal error.
Bounded buffering Pages/Sample Code.xcplaygroundpage/Contents.swift:128 Caps each render call at buffer capacity.

Main application flow

Reference code

Pages/Sample Code.xcplaygroundpage/Contents.swift:126 — the loop writes only successful renders and retries temporary statuses.

while engine.manualRenderingSampleTime < sourceFile.length {
    let framesToRender = min(AVAudioFrameCount(frameCount), buffer.frameCapacity)
    switch try engine.renderOffline(framesToRender, to: buffer) {
    case .success: try outputFile.write(from: buffer)
    case .insufficientDataFromInputNode, .cannotDoInCurrentContext: break
    case .error: fatalError("The manual rendering failed.")
    }
}

Naming conventions

  • Resource names are concrete and pipeline-oriented: sourceFile, outputFile, engine, player, reverb, buffer.
  • framesToRender communicates a bounded per-iteration value.
  • The page uses framework terminology directly, avoiding wrapper vocabulary in a code-light tutorial.

Architecture takeaways

  • A code-light playground can reveal a framework contract more clearly than a layered app.
  • Configure the whole graph and manual format before starting the engine.
  • Treat render status as control flow; write output only on success and retry temporary states.
  • In a production app, the page’s stages are natural extraction points for a processor/service type.

Source map

Source file Relevant symbols
Pages/Sample Code.xcplaygroundpage/Contents.swift Entire load, graph, manual-render, and save pipeline