Polyphonic Synth
Overview
example/poly_synth/poly_synth.cpp is a sibling of the Square Synth:
the same per-note voice (an oscillator through an ADSR-swept resonant low-pass
and amplifier), but played many at once, so chords and overlapping notes ring
together. Here the voice’s oscillator is a bandwidth-limited sawtooth rather
than a square, sixteen of them ready to form a small choir.
Polyphony needs no special library support. A voice is just an instance of the same fine-grained building blocks the monophonic synth uses, so the synth simply keeps a fixed pool of voices and a small allocator.
The Voice
A voice bundles the per-note state: a phase_iterator, an adsr_envelope_gen
(Envelope Generator), and a State Variable Filter. It is the monophonic synth in miniature.
void on(q::frequency freq, float velocity)
{
_velocity = velocity;
_phase.set(freq, _sps);
_env.attack(); // retriggers cleanly even on a stolen voice
}
void off() { _env.release(); }
bool active() const { return !_env.in_idle_phase(); }
float operator()()
{
auto env = _env() * _velocity;
_filter.cutoff(q::frequency{60.0 + (env * 6000.0)}, _sps);
return _filter(q::saw(_phase++)) * env;
}
The only new idea versus the monophonic synth is that there are many voices,
each carrying its own oscillator, envelope, and filter state. A voice is "free"
when its envelope has returned to idle (in_idle_phase()).
The Voice Pool
The synth holds a fixed pool (16 voices). A note-on takes a free voice, or, if all are busy, steals the oldest:
voice& allocate(std::uint8_t key)
{
auto free = std::ranges::find_if(
_voices, [](voice const& v) { return !v.active(); });
voice& v = (free != _voices.end())
? *free
: *std::ranges::min_element(_voices, {}, &voice::_order);
v._key = key;
v._order = ++_order;
return v;
}
Stealing a still-sounding voice re-triggers it. Because the envelope’s attack ramps from the voice’s current level (rather than restarting from zero), the steal is click-free, the same property that keeps fast arpeggios clean on a single voice. A note-off releases every voice holding that key.
The Mix
Each frame sums the active voices, applies a little headroom gain, and soft-clips the result with a Clip:
auto mix = 0.0f;
for (auto& v : _voices)
if (v.active())
mix += v();
left[frame] = right[frame] = _clip(mix * master_gain);
master_gain (0.3) leaves headroom for several simultaneous voices; the soft
clip catches the peaks of dense chords gracefully rather than hard-clipping.
Running
cmake -B build
cmake --build build --target example_poly_synth
build/example/poly_synth/example_poly_synth
Pick a MIDI input and an audio output at the prompts (see List Devices), then play chords. Hold a sustain and add notes; play more than sixteen at once to hear the oldest voices stolen. Ctrl-C to quit.
Components Used
| Component | Role |
|---|---|
Band-limited sawtooth, one per voice: |
|
Per-voice resonant low-pass, swept by the envelope |
|
|
|
Each voice’s pitch, set per note-on |
|
|
|
Note on/off dispatch to voice allocation |
|
The incoming MIDI byte stream |
|
Chosen output device, processing loop |
Previous: Square Synth | Next: Grain Freeze