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
flowchart LR
Source["Rhythm.caf"] --> File["AVAudioFile input"]
File --> Player["AVAudioPlayerNode"]
Player --> Reverb["AVAudioUnitReverb"]
Reverb --> Mixer["AVAudioEngine main mixer"]
Mixer --> Buffer["Manual render buffer"]
Buffer --> Output["AVAudioFile output"]
Output --> Finder["Reveal processed file"]
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
classDiagram
Playground *-- AVAudioEngine : creates
Playground *-- AVAudioFile : input and output
Playground *-- AVAudioPlayerNode : creates
Playground *-- AVAudioUnitReverb : creates
Playground *-- AVAudioPCMBuffer : creates
AVAudioEngine o-- AVAudioPlayerNode : attached
AVAudioEngine o-- AVAudioUnitReverb : attached
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: AVAudioFormatLogic 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
sequenceDiagram
participant Page as Playground
participant Engine as AVAudioEngine
participant Buffer as PCM buffer
participant File as Output file
loop Until manual sample time reaches source length
Page->>Engine: renderOffline(frames, to: buffer)
Engine-->>Page: rendering status
alt success
Page->>File: write(buffer)
else temporary status
Page->>Page: retry next iteration
end
end
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. framesToRendercommunicates 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 |