Virtual Analog Synth
Overview
example/va_synth/va_synth.cpp is the Polyphonic Synth grown into an
instrument of the late 1970s kind. The pool and the allocation are the ones
that page ends with; what changes is the voice inside it. Where that voice had
one oscillator and a State Variable Filter, this one has two oscillators a few cents apart, a
ladder filter with a character to choose, and an LFO moving the pulse width.
The notes change hands too: instead of a keyboard, it plays the MIDI files that
sit beside it, and the six clips the Virtual
Analog page plays are what it writes.
-
The colors carry over from the earlier tutorials.
-
Ten voices sit in the vector, each with two
Analog Oscillator, a ladder filter, twoEnvelope Generator, anLFO Generatorand aSoft Clip. -
The sum takes the patch’s own gain.
-
The buffer goes to a file through
Audio File, or to the device through the example’s ownplayer.
The parts are the same fine-grained blocks the earlier tutorials use. Three things are new: the notes come from a file, a patch is a plain struct of numbers so a sound can be written down and read back, and a voice that has gone quiet needs a little care before it is played again.
Playing a MIDI File
`q::midi_file` reads a standard MIDI file and plays it into a
MIDI Processor, the same processor a live input would feed, with each message
timed in samples at the sample rate given. Tracks are merged and the file’s
tempo is applied, so the notes arrive in the order they are played.
q::midi_file file{path};
if (!file)
return 1; // missing, or not a MIDI file
recorder rec;
file(sps, rec); // play it, at this sample rate
Rendering offline, the processor simply writes down what it hears, and the loop consumes the events afterwards:
struct recorder
{
void operator()(midi::message_base const&, std::size_t) {}
void operator()(midi::note_on msg, std::size_t time)
{
// A note on with no velocity is a note off, which is how a good
// many sequencers write them.
events.push_back({time, msg.key(),
msg.velocity() == 0? 0.0f : msg.velocity() / 127.0f});
}
void operator()(midi::note_off msg, std::size_t time)
{
events.push_back({time, msg.key(), 0.0f});
}
std::vector<note_event> events;
};
A live synth would act on the same calls as they arrive. Nothing about the processor changes between the two.
The Patch
A patch is a struct: what a front panel would set, in the units the panel shows. Writing one is writing a sound.
patch bass() // eighth notes, filter envelope led
{
patch p;
p.a_saw = 0.75f; p.b_pulse = 0.35f; p.osc_b_ratio = 0.5f;
p.width = 0.35f; p.osc_b_detune = 0.03f;
p.cutoff = 220.0f; p.env_amount = 3000.0f; p.resonance = 0.35f;
p.drive = 2.0f; p.key_track = 0.25f;
p.amp_a = 4_ms; p.amp_d = 250_ms; p.amp_s = -9_dB; p.amp_r = 120_ms;
p.flt_a = 3_ms; p.flt_d = 180_ms; p.flt_s = -26_dB; p.flt_r = 120_ms;
p.gain = 0.8f;
return p;
}
osc_b_ratio of 0.5 puts the second oscillator an octave down, the filter sits
low at 220 Hz, and its envelope adds 3 kHz on top of that, which is what makes
this kind of bass talk.
The Voice
A voice holds two Analog Oscillator, a ladder filter and two envelopes. Each oscillator gives its waveforms from one phase, and a patch mixes the ones it uses:
auto mix = _p.a_saw * _a.saw(_pa) + _p.a_pulse * _a.pulse(_pa)
+ _p.b_saw * _b.saw(_pb) + _p.b_tri * _b.triangle(_pb)
+ _p.b_pulse * _b.pulse(_pb);
if (_p.mixer_drive > 1.0f)
mix = _clip(mix * _p.mixer_drive); // the mixer, overdriven
auto e = _flt_env();
auto track = 1.0 + _p.key_track * ((_hz / 261.6) - 1.0);
auto fc = q::frequency{
std::clamp((_p.cutoff + _p.env_amount * e * e * _velocity) * track,
30.0, 16000.0)};
_filt.cutoff(fc, sps);
return _filt(mix * 0.35f) * _amp() * _velocity;
This is the same envelope-to-cutoff-and-level patch the
Square Synth introduced, with two differences: each destination now
has an envelope of its own, and the filter is a ladder rather than a State Variable Filter.
The filter envelope is squared so the sweep is heard evenly, and key_track
carries the cutoff up the keyboard. Which filter a patch takes is a choice of
character: OTA Ladder Filter for the smoother second-harmonic drive of the
variable gain instruments, Moog Ladder Filter with its saturation switched on for
the growl, and Ladder Filter at two poles for the brighter setting some of
them put on the panel.
A Voice That Goes Quiet Still Needs Care
Two details keep the notes clean, and both come from a voice being reused.
A silent voice stops being processed, so its filter freezes mid-ring and would resume as a step on the next note, which is heard as a tick. A real instrument has no such thing: its oscillators run all the time. Clearing the filter on a fresh note does the same job, and a retrigger while the voice is still sounding keeps its state, which is what makes a line smooth:
if (!active())
{
_filt = 0.0f;
_filt2 = 0.0f;
_moog = 0.0f;
}
The other is that every voice is built in place:
std::vector<voice> pool;
pool.reserve(voices);
for (std::size_t i = 0; i != voices; ++i)
pool.emplace_back(p);
Filling the pool by copying one prototype would work, with one catch: an envelope’s segments hold their ramps, and the copies would share them. The Polyphonic Synth builds its pool as a plain array of voices, which has the same effect for the same reason.
The FM Synth that follows keeps all of this and changes the voice once more, to six operators under an algorithm, where the sound is no longer a handful of panel settings but a patch to be compiled.
Running
cmake -B build
cmake --build build --target example_va_synth
build/example/va_synth/example_va_synth
With no argument the six clips play in turn through the default output device. Given a directory, they are written there as WAV files instead, which is how the ones the documentation plays are made:
build/example/va_synth/example_va_synth ~/clips
Components Used
| Component | Role |
|---|---|
Both oscillators: saw, pulse and triangle from one phase |
|
The filter, and its character |
|
The mixer overdriven into the filter |
|
|
|
Moves the pulse width |
|
Each oscillator’s pitch, set per note on |
|
`q::midi_file` |
The notes, read from a standard MIDI file |
|
|
Playing through the default output device |
Previous: Polyphonic Synth | Next: FM Synth