Using voice processing
At a glance
| Item |
Summary |
| Purpose |
Demonstrates voice-processing I/O, bypass behavior, recording/playback, and live power meters. |
| App architecture |
A UIKit controller owns session/UI lifecycle while an AudioEngine façade owns nodes, buffers, recording, and taps. |
| Main patterns |
Screen/service split, graph owner, read-only provider protocol, notifications, focused signal-processing helper. |
| Project style |
Flat single-target sample with audio service and small metering view/model helpers. |
Project structure
AVEchoTouch/
├── ViewController.swift
├── AudioEngine.swift
├── LevelMeterView.swift
├── PowerMeter.swift
├── MeterTable.swift
└── Audio/
├── Synth.aif
└── sampleVoice*.wav
Structure observations
AudioEngine centralizes AVAudioEngine graph/lifecycle behavior; the view controller retains AVAudioSession interruption and route UI policy.
- Meter calculation (
PowerMeter/MeterTable) is separate from meter drawing (LevelMeterView).
Overall architecture
flowchart LR
User["UIKit controls"] --> VC["ViewController"]
VC --> Session["AVAudioSession"]
VC --> Audio["AudioEngine"]
Audio --> Input["Voice-processing input node"]
Audio --> Speech["Speech player"]
Audio --> FX["FX player"]
Audio --> Recorded["Recorded-file player"]
Speech --> Mixer["Main mixer → output"]
FX --> Mixer
Recorded --> Mixer
Input --> File["Recorded AVAudioFile"]
Input --> Power["PowerMeter"]
Power --> Meter["LevelMeterView"]
Reference code
AVEchoTouch/AudioEngine.swift:95 — one service configures voice processing, node routing, recording, and metering taps.
func setup() {
let input = avAudioEngine.inputNode
try? input.setVoiceProcessingEnabled(true)
let mainMixer = avAudioEngine.mainMixerNode
avAudioEngine.connect(fxPlayer, to: mainMixer, format: fxBuffer.format)
avAudioEngine.connect(speechPlayer, to: mainMixer, format: speechBuffer.format)
avAudioEngine.connect(recordedFilePlayer, to: mainMixer, format: voiceIOFormat)
input.installTap(onBus: 0, bufferSize: 256, format: voiceIOFormat) { buffer, _ in
self.voiceIOPowerMeter.process(buffer: buffer)
}
}
Interpretation
The screen coordinates user-facing lifecycle and system notifications. AudioEngine is authoritative for graph objects and recording/playback state. Taps feed focused meter objects that expose only display-ready levels.
Ownership and state
classDiagram
ViewController *-- AudioEngine : creates
AudioEngine *-- AVAudioEngine : owns
AudioEngine *-- AVAudioPlayerNode : owns three
AudioEngine *-- AVAudioFile : owns while recording
AudioEngine *-- PowerMeter : owns three
LevelMeterView o-- AudioLevelProvider : observes
PowerMeter ..|> AudioLevelProvider
PowerMeter *-- MeterTable : owns
Ownership evidence
AVEchoTouch/ViewController.swift:71 — the screen constructs the service, injects its meter providers, then starts it.
audioEngine = try AudioEngine()
speechMeter.levelProvider = audioEngine.speechPowerMeter
fxMeter.levelProvider = audioEngine.fxPowerMeter
voiceIOMeter.levelProvider = audioEngine.voiceIOPowerMeter
setupAudioSession(sampleRate: audioEngine.voiceIOFormat.sampleRate)
audioEngine.setup()
audioEngine.start()
| Owner |
Object or state |
Relationship |
Mutation authority |
ViewController |
AudioEngine, outlets, notification response |
Creates and coordinates |
View controller for UI/session response |
AudioEngine |
Engine, nodes, buffers, file, recording flag |
Creates and retains |
AudioEngine methods/taps |
PowerMeter |
Per-channel power samples and conversion tables |
Owns calculation state |
Tap-driven process methods |
LevelMeterView |
Display link and LED layers |
Owns rendering state |
View; reads provider only |
Class and protocol design
| Type |
Responsibility |
Depends on |
ViewController |
Session setup, interruption/route recovery, UI commands |
AudioEngine |
AudioEngine |
Graph, voice-processing mode, record/play lifecycle |
AVAudioEngine and nodes |
AudioLevelProvider |
Read-only meter capability |
AudioLevels value |
PowerMeter |
Calculate RMS/peak values with Accelerate |
PCM buffers, MeterTable |
LevelMeterView |
Poll a provider and render LEDs |
AudioLevelProvider |
Access control
| Symbol |
Access |
Verified effect |
Likely rationale |
| Engine/nodes/buffers/file flags |
private |
The screen cannot rewire nodes or partially change recording resources. |
Preserve graph and lifecycle invariants. |
voiceIOFormat, isRecording, meter objects |
public private(set) |
Consumers read/configure UI from them but cannot replace them. |
Expose outputs while retaining authority. |
ViewController.audioEngine |
private |
Other screens cannot operate this screen’s service. |
Screen-scoped ownership. |
PowerMeter values/tables/calculator |
private |
Consumers see only normalized levels. |
Hide signal-processing state. |
| Types |
mostly implicit internal |
App-local collaboration despite a few explicit public properties. |
Single app target. |
Reference code
AVEchoTouch/AudioEngine.swift:24 — readable service outputs keep external setters closed.
public private(set) var voiceIOFormat: AVAudioFormat
public private(set) var isRecording = false
public private(set) var speechPowerMeter = PowerMeter()
public private(set) var fxPowerMeter = PowerMeter()
public private(set) var voiceIOPowerMeter = PowerMeter()
Logic ownership and placement
| Logic |
Owning type or file |
Placement rationale |
| UI/session notifications |
ViewController |
Presentation and app lifecycle policy. |
| Voice-processing graph and bypass |
AudioEngine |
Same owner as input/output nodes. |
| Recording and recorded playback |
AudioEngine |
Coupled file/node state. |
| RMS/peak calculation |
PowerMeter |
Focused Accelerate-based transformation. |
| Power-to-display conversion |
MeterTable |
Reusable lookup helper. |
| LED rendering |
LevelMeterView |
View-only concern. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Audio façade |
AVEchoTouch/AudioEngine.swift:11 |
Prevents UIKit from manipulating graph objects directly. |
| Provider protocol |
AVEchoTouch/LevelMeterView.swift:16 |
Meter view accepts any readable level source. |
| Observer |
AVEchoTouch/ViewController.swift:35 |
Responds to interruptions, routes, and media-service resets. |
| Signal-processing helper |
AVEchoTouch/PowerMeter.swift:37 |
Isolates format conversion and vDSP calculations. |
| Tap-based stream processing |
AVEchoTouch/AudioEngine.swift:112 |
Samples live node buffers without coupling meter rendering to the graph. |
Naming conventions
- Framework wrapper uses the direct role name
AudioEngine; views use UIKit ViewController/View suffixes.
- Commands state behavior:
bypassVoiceProcessing, toggleRecording, speechPlayerPlay.
- Readable state follows
is naming: isRecording, isPlaying, isNewRecordingAvailable.
- Meter types separate raw responsibility (
PowerMeter) from display (LevelMeterView).
Architecture takeaways
- Give one service ownership of the entire AVAudioEngine graph and recording lifecycle.
- Expose meter data through a tiny read-only protocol rather than sharing audio nodes.
- Keep session interruption policy at the app/screen boundary and graph recovery in the audio service.
- Separate signal calculation from visualization so each can evolve independently.
Source map
| Source file |
Relevant symbols |
AVEchoTouch/ViewController.swift |
Service ownership, session and notification handling |
AVEchoTouch/AudioEngine.swift |
Voice-processing graph, recording, playback |
AVEchoTouch/PowerMeter.swift |
PCM power calculation |
AVEchoTouch/LevelMeterView.swift |
Provider protocol and rendering |
AVEchoTouch/MeterTable.swift |
dB lookup conversion |