Building a signal generator
At a glance
| Item |
Summary |
| Purpose |
Generates selectable waveforms in a custom real-time audio source node. |
| App architecture |
SwiftUI state flows through a Swift manager and Objective-C++ adapter to a C++ DSP kernel. |
| Main patterns |
Observable manager, language-bridge adapter, render callback, waveform strategy table, parameter ramping. |
| Project style |
Single app target organized by language boundary rather than feature folders. |
Project structure
SignalGenerator/
├── SignalGeneratorApp.swift
├── ContentView.swift
├── AudioManager.swift
├── SignalGenerator.h
├── SignalGenerator.mm
├── SignalGeneratorKernel.h
├── ParameterRamp.h
└── WaveFunction.h
Structure observations
- Swift owns presentation and engine composition; Objective-C++ is the narrow interoperability seam.
- Header-only C++ types isolate sample generation, ramping, and waveform selection from UI concerns.
Overall architecture
flowchart LR
UI["SwiftUI controls"] --> Manager["AudioManager"]
Manager --> Bridge["SignalGenerator ObjC++"]
Bridge --> Kernel["SignalGeneratorKernel C++"]
Kernel --> Ramp["ParameterRamp"]
Kernel --> Wave["WaveFunction table"]
Bridge --> Source["AVAudioSourceNode render block"]
Source --> Engine["AVAudioEngine → output"]
Reference code
SignalGenerator/AudioManager.swift:32 — the manager connects the render callback to the engine graph.
let srcNode = AVAudioSourceNode(renderBlock: signalGenerator.renderBlock)
let mainMixer = engine.mainMixerNode
let output = engine.outputNode
signalGenerator.setSampleRate(outputFormat.sampleRate)
engine.attach(srcNode)
engine.connect(srcNode, to: mainMixer, format: inputFormat)
engine.connect(mainMixer, to: output, format: outputFormat)
Interpretation
AudioManager is the application façade. Parameter changes cross the Objective-C interface, while audio-frame production remains in C++ and is invoked by AVAudioEngine’s real-time callback.
Ownership and state
classDiagram
SignalGeneratorApp *-- AudioManager : State owns
AudioManager *-- AVAudioEngine : owns
AudioManager *-- SignalGenerator : owns
SignalGenerator *-- SignalGeneratorKernel : C++ ivar
SignalGenerator *-- SignalGeneratorParameters : C++ ivar
AVAudioEngine --> SignalGenerator : invokes render block
Ownership evidence
SignalGenerator/AudioManager.swift:10 — the manager retains the bridge and engine as private implementation objects.
@Observable class AudioManager {
// ...
}
| Owner |
Object or state |
Relationship |
Mutation authority |
SignalGeneratorApp |
AudioManager |
Creates with @State and binds into the view |
AudioManager and bound UI properties |
AudioManager |
Engine, bridge, UI-facing parameters |
Creates and retains |
AudioManager |
SignalGenerator |
Kernel and pending parameter struct |
Embedded Objective-C++ ivars |
Bridge setters/render block |
SignalGeneratorKernel |
Phase, ramps, waveform function |
Value-owned DSP state |
Kernel methods on render path |
Class and protocol design
| Type |
Responsibility |
Depends on |
AudioManager |
Map observable controls to DSP setters and compose the audio graph |
AVAudioEngine, SignalGenerator |
SignalGenerator |
Present a Swift/Objective-C-compatible API and render block |
C++ kernel |
SignalGeneratorKernel |
Produce the next sample from smoothed parameters |
ParameterRamp, WaveFunction |
ParameterRamp |
Move parameter values without abrupt discontinuities |
C++ numeric state |
WaveFunction |
Select waveform calculation by enum |
Function-pointer table |
Access control
| Symbol |
Access |
Verified effect |
Likely rationale |
AudioManager.signalGenerator / engine |
private |
UI code cannot rewire the graph or access DSP internals. |
Keep graph invariants centralized. |
SignalGenerator header methods |
Objective-C interface |
Swift sees only render/setup/parameter operations. |
Make the bridge deliberately narrow. |
| Kernel implementation state |
C++ private |
Callers use setup/update/sample methods rather than phase fields. |
Protect DSP invariants. |
Waveform description witnesses |
public |
Satisfy public protocol requirements used by the UI. |
Language conformance requirement, not a public framework architecture. |
| Swift app types |
implicit internal |
Remain within the app module. |
The target is an app, not a library. |
Reference code
SignalGenerator/SignalGenerator.mm:19 — C++ ownership is hidden inside the adapter implementation.
@implementation SignalGenerator {
// ...
}
// ...
@end
Logic ownership and placement
| Logic |
Owning type or file |
Placement rationale |
| Control state and engine lifecycle |
AudioManager |
Swift-observable application layer. |
| Inter-language conversion and callback |
SignalGenerator.mm |
Only file that needs Objective-C and C++ together. |
| Per-frame DSP |
SignalGeneratorKernel.h |
Real-time logic stays independent of UI/runtime bridging. |
| Smoothing |
ParameterRamp.h |
Reusable numerical responsibility. |
| Waveform algorithms |
WaveFunction.h |
Compact strategy selection. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Observable façade |
SignalGenerator/AudioManager.swift:10 |
Gives UI one coherent audio API. |
| Adapter/bridge |
SignalGenerator/SignalGenerator.mm:45 |
Exposes a C++ kernel as an AVAudio source-node block. |
| Strategy table |
SignalGenerator/WaveFunction.h:13 |
Selects waveform behavior without a class hierarchy. |
| Parameter ramp |
SignalGenerator/ParameterRamp.h:12 |
Reduces artifacts from control changes. |
| Real-time callback |
SignalGenerator/SignalGenerator.mm:51 |
Lets AVAudioEngine request exactly the frames it needs. |
Naming conventions
- Manager and kernel suffixes communicate layer and role:
AudioManager, SignalGeneratorKernel.
- Setters mirror domain controls:
setFrequency, setAmplitude, setWaveform, setSampleRate.
- File extensions expose the language boundary:
.swift, .mm, and header-only C++ .h.
Waveform names domain choices; WaveFunction names executable strategies.
Architecture takeaways
- Put Objective-C++ in a narrow adapter so most app code remains idiomatic Swift or C++.
- Keep engine graph ownership in one manager and per-frame work in a small kernel.
- Copy UI parameters into render-owned state once per callback, not through repeated object lookup per sample.
- Use access control to prevent presentation code from mutating the audio graph directly.
Source map
| Source file |
Relevant symbols |
SignalGenerator/SignalGeneratorApp.swift |
App-owned AudioManager |
SignalGenerator/AudioManager.swift |
Observable state and engine graph |
SignalGenerator/SignalGenerator.h |
Bridge contract |
SignalGenerator/SignalGenerator.mm |
Bridge implementation and render block |
SignalGenerator/SignalGeneratorKernel.h |
DSP kernel |
SignalGenerator/ParameterRamp.h |
Parameter smoothing |
SignalGenerator/WaveFunction.h |
Waveform functions |