Polyphonic Synth
Overview
example/poly_synth/poly_synth.cpp takes the voice the
Square Synth just built, an oscillator through an ADSR-swept
resonant low-pass and amplifier, and keeps sixteen of them, so chords and
overlapping notes ring together. The only change inside the voice is its
oscillator, a bandwidth-limited sawtooth rather than a square.
-
The colors are the ones the Square Synth uses.
-
allocate()gives a note a voice, free if there is one and the oldest otherwise. -
The array holds sixteen of them.
-
Each frame sums the voices that are active, applies a headroom gain, and soft-clips the result.
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. Two questions are new, and they are the whole of this page: which voice a note goes to, and what happens to the one it displaces.
The Voice
A voice bundles the per-note state: a phase_iterator, an
adsr_envelope_gen, 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.
Read the figure left to right: each note takes a voice, and the dashed stretch after it is the release, still sounding. When B arrives, every voice is still active, the released one included, so the oldest goes and its release is cut short. A voice counts as busy until its envelope reaches idle, which is later than the note-off that released it. That is why a chord played over a fading chord can run a pool out of voices, and why a generous pool costs little: a voice that is idle does nothing.
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.
That is a synthesizer: voices, allocation, and a mix. What it does not yet have is a sound worth naming, which is where the Virtual Analog Synth goes next: a second oscillator, a filter with character, and a patch that writes the whole thing down.
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: Virtual Analog Synth