Carving music from a raw, harmonically rich waveform — band-limited oscillators, a resonant filter, an envelope, and the voice allocation that makes it polyphonic. The instrument at the heart of the software-synth era, rebuilt from first principles and playable in your browser.
Additive synthesis builds a timbre up sine by sine. Subtractive synthesis does the opposite: it starts with a waveform that is already too rich — a sawtooth or a pulse, dense with harmonics — and removes what it does not want with a filter. Sculpture, not construction.
The appeal is economy. A single sawtooth at fundamental $f_0$ contains every harmonic $n f_0$ with amplitude falling as $1/n$; a square (50% pulse) contains only the odd harmonics, also decaying as $1/n$. From that one dense source, a resonant low-pass filter with a moving cutoff can imitate a startling range of instruments and, more importantly, invent sounds that no acoustic instrument makes. The filter is played as expressively as the keyboard: its cutoff swept by an envelope on every note, wobbled by a low-frequency oscillator, resonating at the edge of self-oscillation.
The canonical signal path — oscillator, filter, amplifier, each with its own modulation — has barely changed since 1964. It is the grammar every hardware and software synthesiser still speaks:
This page builds each block honestly. The oscillators are band-limited with PolyBLEP so they do not alias; the filter is a topology-preserving state-variable filter derived by the trapezoidal (bilinear) integrator; the amplifier is shaped by an exponential ADSR; and a voice allocator hands notes to a pool of identical voices so the instrument is polyphonic. Everything in the live demo runs in an AudioWorklet — the sample-by-sample code on screen is the code you hear.
This is an instrument (a VSTi), not an effect: it is the sound source. So instead of a "drop a file / loop / microphone" input, you play it — with an on-screen keyboard, or your computer keys (rows z x c v… and q w e r…).
02From resonators to Vital
Subtractive synthesis did not begin with electronics. Hermann von Helmholtz strapped tuned resonators to his ears in the 1860s to analyse which partials a vowel contained; the synthesiser inverts his apparatus, using an electrically tunable resonance to impose a spectrum. The through-line from his brass spheres to a modern wavetable plug-in is remarkably direct.
1863
Helmholtz, On the Sensations of Tone. Tuned resonators isolate partials; the formant theory of timbre is born — the intellectual seed of "filtering a rich source".
1964
Robert Moog demonstrates voltage control. The VCO, VCF and VCA respond to control voltages at 1 volt/octave; patch cords make the modular synthesiser. The subtractive paradigm is fixed.
1970
Minimoog Model D. The modular collapsed into a hard-wired, portable, playable instrument with a four-pole ladder filter — the definitive lead and bass voice for decades.
1978
Sequential Prophet-5. Microprocessor-scanned polyphony (five voices) plus fully recallable patches. Voice allocation becomes a software problem.
1983
MIDI 1.0. A 31.25 kbaud serial standard lets any keyboard drive any synth. Note-on / note-off / velocity — the messages our demo still speaks.
1983
Yamaha DX7. FM synthesis briefly dethrones subtractive with glassy, metallic timbres analogue filters could not make — the counterpoint that defined the decade's pop.
1996–99
Steinberg VST (1996) and VST 2.0 with VSTi (1999). The synthesiser becomes software hosted inside a DAW. Propellerhead Reason (2000) models a whole rack.
2007–14
LennarDigital Sylenth1, NI Massive, Xfer Serum. Anti-aliased virtual-analogue and wavetable synths make software the default. PolyBLEP and oversampling become table stakes.
2019–
Open source: Matt Tytel's Vital, the Surge community synth, and Dexed (a DX7 clone) put professional-grade instruments and their source code in everyone's hands.
The filter is the soul of the instrument. Everything else just gives it something to chew on.— a paraphrase of studio folklore, true of every subtractive synth
03Band-limited oscillators & PolyBLEP
A sawtooth is trivial to generate and impossible to generate correctly. The naïve version aliases badly; making it band-limited without a Fourier series is the first real DSP problem a synthesiser poses.
The naïve oscillator and why it aliases
A phase accumulator advances a normalised phase $p\in[0,1)$ by $\Delta = f_0/f_s$ each sample. The obvious sawtooth reads $s = 2p-1$. It looks perfect on paper, but the ideal saw has energy at every harmonic $n f_0$; those above the Nyquist frequency $f_s/2$ do not vanish, they fold back to $f_s - n f_0$ as inharmonic aliases. At $f_0 = 2\,\text{kHz}$ and $f_s = 44.1\,\text{kHz}$ you hear a metallic, detuned grit that gets worse as you play higher — the tell-tale sound of a broken oscillator.
$$s_{\text{saw}}(t)=\frac{2}{\pi}\sum_{n=1}^{\infty}\frac{(-1)^{n+1}}{n}\sin(2\pi n f_0 t),\qquad s_{\text{square}}(t)=\frac{4}{\pi}\sum_{k=0}^{\infty}\frac{\sin\!\big(2\pi(2k{+}1)f_0 t\big)}{2k+1}$$
PolyBLEP: patching the discontinuity
Aliasing enters only at the waveform's discontinuities — the instantaneous jump of a saw, the two edges of a square. A band-limited step (BLEP) is the difference between an ideal, brick-wall-limited step and the naïve step; adding it around each jump cancels most of the aliasing. The PolyBLEP (Välimäki & Huovilainen, 2007) approximates that correction with a cheap two-piece polynomial spanning one sample either side of the discontinuity. With $t$ the phase measured from the jump and $\Delta$ the phase increment:
For the sawtooth you subtract one PolyBLEP at the wrap point. For the square you subtract one at $p=0$ and add one at $p=0.5$ (the falling and rising edges). The triangle is then obtained for free by leaky-integrating the band-limited square — integration is a $1/n$ low-pass, so a band-limited square integrates to a band-limited triangle. The sine needs no correction at all.
// one sample of a band-limited oscillator (JS, runs in the worklet)
function polyBlep(t, dt){
if (t < dt) { t /= dt; return t + t - t*t - 1.0; }
else if (t > 1 - dt){ t = (t - 1)/dt; return t*t + t + t + 1.0; }
return 0.0;
}
// saw:
let s = 2*p - 1; // naive ramp
s -= polyBlep(p, dt); // correct the wrap
// square (with a second edge half a cycle away):
let sq = (p < 0.5) ? 1 : -1;
sq += polyBlep(p, dt);
sq -= polyBlep((p + 0.5) % 1, dt);
// triangle = leaky integral of the band-limited square:
triState = dt * sq + (1 - dt) * triState; // ~one-pole integrator
let tri = triState * 4; // rescale to ±1
The exact code the live demo runs. Each voice keeps its own p, triState per oscillator.
naïve vs PolyBLEP sawtooth — drag the frequency slider to hear (see) aliasing appear
The upper trace is the time-domain waveform; the lower is its magnitude spectrum. With PolyBLEP off and the frequency high, watch the forest of aliased partials appear between the true harmonics — inharmonic, and audibly foul. Switch it on and the noise floor drops away.
Derivation — why a step is the thing to band-limit
Any of these classic waveforms is piecewise-linear, so its second derivative is a train of Dirac deltas and its first derivative is a train of steps. Bandlimiting is a linear operation, so bandlimiting the waveform is equivalent to replacing each ideal step in the derivative domain by a band-limited step and re-integrating. The ideal band-limited step is the integral of a sinc — a long, ringing function; BLEP methods store a windowed, oversampled table of exactly that. PolyBLEP is the minimal version: it fits the correction with the lowest-order polynomial that is continuous and has the right area over $\pm 1$ sample, trading a little residual aliasing for near-zero cost. The residual you added above, $2u-u^2-1$ on the leading side and $2u+u^2+1$ on the trailing side, is precisely that minimum-phase two-sample patch.
Our voice runs two such oscillators. A detune in cents multiplies the second oscillator's frequency by $2^{c/1200}$; an octave switch multiplies by a power of two; a mix crossfades them. Two nearly-identical saws a few cents apart beat slowly against each other — the "supersaw" thickness that defines a decade of dance music.
04The amplitude envelope (ADSR)
An oscillator alone drones. The envelope generator gives every note a life: an attack transient, a body, and a release tail. The four-stage ADSR — Attack, Decay, Sustain, Release — is the 1970s standard, and its exponential segments are what make it sound natural.
A linear ramp reaching its target sounds artificial; real acoustic decays are exponential, straight lines on a decibel scale. The classic analogue ADSR charges and discharges a capacitor toward a target voltage, which is a one-pole system. In discrete time we approach a target $T$ from the current value $y$ each sample with a fixed coefficient:
where $\tau$ is the segment time-constant in seconds. During attack the target is set slightly above 1 (say $1.2$) and the stage ends when $y$ crosses $1$, which gives the characteristic fast-then-slowing rise; during decay the target is the sustain level $S$; sustain simply holds $S$ while the key is down; release aims at $0$ once the key lifts. The exponential coefficient means "time" is really the time to traverse most of the distance — the standard, musical convention.
// per-sample ADSR (one state machine per voice)
switch (stage){
case ATTACK: env += aCoef * (1.2 - env);
if (env >= 1.0){ env = 1.0; stage = DECAY; } break;
case DECAY: env += dCoef * (sustain - env);
if (env <= sustain + 1e-4){ stage = SUSTAIN; } break;
case SUSTAIN: env = sustain; break; // hold
case RELEASE: env += rCoef * (0.0 - env);
if (env <= 1e-4){ env = 0; stage = IDLE; } break;
}
// aCoef = 1 - exp(-1/(attackSeconds * sampleRate)), etc.
The amp envelope multiplies the voice output; a second, identical ADSR modulates filter cutoff (§5).
ADSR contour — adjust the demo's sliders and this redraws live
A short attack and short decay to a low sustain gives a percussive "pluck"; a long attack and long release gives an evolving "pad". Because the release is a separate exponential started at key-off from wherever the envelope happened to be, notes overlap gracefully and voices can be reused only once their release has fully decayed — a fact the voice allocator (§6) depends on.
05The resonant filter — a TPT state-variable
The filter is where a subtractive synth earns its keep. We want a resonant, sweepable low-pass whose cutoff can be modulated to audio rates without instability and whose resonance can climb to self-oscillation. The topology-preserving transform (TPT) state-variable filter, derived with the trapezoidal integrator, delivers exactly that.
From analogue prototype to trapezoidal integrators
The analogue state-variable filter is two integrators in a loop. Its low-pass transfer function is the textbook biquad
Discretising each integrator with the trapezoidal rule (the bilinear transform applied locally, not to the whole transfer function) preserves the filter's topology — the same low-pass, band-pass and high-pass taps exist in the digital version — and, crucially, keeps it stable and tuned even when the cutoff is swept every sample. Prewarping the cutoff with $g=\tan(\pi f_c/f_s)$ makes the digital cutoff land exactly on $f_c$. The result (Zavalishin; Simper's "cytomic" form) is this per-sample update, with two state variables $s_1,s_2$:
// one sample through the TPT / cytomic SVF (per voice)
const v3 = input - s2;
const v1 = a1*s1 + a2*v3;
const v2 = s2 + a2*s1 + a3*v3;
s1 = 2*v1 - s1; // trapezoidal integrator state update
s2 = 2*v2 - s2;
const low = v2;
const band = v1;
const high = input - k*v1 - v2; // note: HP = in - k·BP - LP
const notch = input - k*v1; // (LP + HP), if wanted
// our voice uses `low`; k = 1/Q sets resonance.
Unconditionally stable for any $f_c\in(0,f_s/2)$ and any $Q>0$; no per-sample tan needed if you only update coefficients when the cutoff changes — but modulating it every sample is safe.
As $k\to 0$ (i.e. $Q\to\infty$) the resonance peak grows without bound and the filter self-oscillates, producing a pure sine at the cutoff frequency — a sound source in its own right. In the demo the resonance is capped just short of that so a raised env amount can sweep the cutoff dramatically without the filter screaming.
magnitude response |H(f)| — cutoff and resonance track the live synth
The curve above is the true low-pass magnitude of the analogue prototype at the current cutoff and $Q$ (the TPT filter matches it at and below cutoff by construction). Raise resonance and the peak at $f_c$ rises; sweep the cutoff with the filter envelope and the whole curve slides. In the demo, the filter cutoff is driven by both a dedicated ADSR (the "env amount") and the LFO (§7):
Derivation — the trapezoidal integrator and the "instantaneous response"
An ideal integrator $H(s)=1/s$ discretised by the bilinear transform $s\leftarrow \tfrac{2}{T}\tfrac{1-z^{-1}}{1+z^{-1}}$ becomes $y[n]=y[n-1]+\tfrac{T}{2}(x[n]+x[n-1])$ — the trapezoidal rule. Writing the integrator output as its previous state plus a term proportional to the present input creates an instantaneous (zero-delay) feedback path: the two integrators in the SVF each depend on the other's current output. Zavalishin's insight was to solve that $2\times 2$ instantaneous system algebraically once, yielding the closed-form $a_1,a_2,a_3$ above. Because the algebra is exact, there is no delay-free-loop approximation and no tuning error — the filter is "topology-preserving". This is why $g=\tan(\pi f_c/f_s)$ rather than the naïve $2\pi f_c/f_s$: the tangent is the exact bilinear prewarping that undoes the frequency compression of the transform.
A Moog-style four-pole ladder filter is the other classic choice (Stilson & Smith; Huovilainen). It cascades four one-pole sections with a global feedback whose gain sets resonance, and it needs a nonlinearity in the loop to tame self-oscillation. The TPT SVF is cheaper, cleaner and easier to make bullet-proof, so it is what this instrument uses.
06Polyphony & voice allocation
Everything so far describes one voice: two oscillators, two envelopes, a filter. A polyphonic synth is simply a pool of identical voices plus the bookkeeping that decides which voice plays which note — the voice allocator.
On a note-on the allocator looks for an idle voice; if all are busy it "steals" one — usually the oldest, or the one nearest the end of its release. On a note-off it finds the voice holding that note and moves its envelopes into the release stage. Each voice remembers the MIDI note it owns and a monotonically increasing "age" stamp so stealing is deterministic:
noteOn(note, vel){
let v = this.voices.find(v => v.stage === IDLE); // free voice?
if (!v) v = this.voices.reduce((a,b) => // else steal oldest
a.age < b.age ? a : b);
v.start(note, vel, this.now++); // age = now++
}
noteOff(note){
for (const v of this.voices)
if (v.note === note && v.stage !== RELEASE) v.release();
}
Held notes retriggering the same pitch, sustain-pedal behaviour and unison are layered on top of this core.
The number of voices is a direct CPU trade: eight is comfortable for a browser, sixteen is generous. Unison stacks several detuned voices per note for thickness, multiplying the cost. Because many voices can sound at once, the master must guard against clipping — we scale each voice by roughly $1/\sqrt{N_{\text{active}}}$ and pass the sum through a soft limiter (§8) so a fistful of notes stays inside $[-1,1]$.
voice pool activity — each cell is a voice; height tracks its envelope, colour marks active/stealing
07Modulation — the LFO
A synth without modulation is static. The low-frequency oscillator (LFO) — typically 0.1–20 Hz, below hearing — is the movement generator: routed to pitch it makes vibrato; routed to filter cutoff it makes the classic wah / wobble.
The LFO is just another oscillator, usually a sine or triangle, sampled once per sample (or once per block) and scaled by a depth. Routed to pitch it multiplies each oscillator's frequency by $2^{d\,\ell[n]/12}$ for a depth $d$ in semitones; routed to filter it enters the cutoff exponent of §5. A single global LFO shared across voices keeps the whole chord moving together — the usual musical choice — though per-voice LFOs (as in the JUCE listing) allow independent movement.
$$\ell[n]=\sin(2\pi f_{\text{lfo}} n / f_s),\qquad f_{\text{pitch}}=f_0\cdot 2^{\,d\,\ell[n]/12}$$
Vibrato depth of a semitone or less reads as expressive; a few semitones becomes a siren. On the filter, a slow LFO of moderate depth over a resonant low-pass is the single most recognisable "synth" gesture there is. Try it in the demo below.
08Live demo — a playable polyphonic synth
Everything above, wired together and running in an AudioWorklet: two PolyBLEP oscillators, a TPT state-variable filter with its own envelope, an amp ADSR, a global LFO, eight-voice polyphony with stealing, and a soft limiter on the master. Play it with the on-screen keys, your computer keyboard, or load a preset and turn knobs.
Subtractor · VSTiDSP & MUSIC — WEB AUDIO
Oscillators
osc 1
osc 2
detune
osc mix
sub
noise
Filter (TPT SVF, low-pass)
cutoff
reso
env amt
Amp envelope
attack
decay
sustain
release
Filter env · LFO · Master
flt decay
lfo rate
lfo amt
lfo dest
master
outvoices
presets
output spectrum (FFT)
live filter response |H(f)|
Keyboard map. The lower row plays the current octave (z=C, s=C♯, x=D, d=D♯, c=E, v=F, g=F♯, b=G, h=G♯, n=A, j=A♯, m=B); the upper row (q 2 w 3 e r 5 t 6 y 7 u) plays an octave higher. Notes highlight as they sound. Everything is polyphonic; hold a chord and sweep the cutoff.
Audio starts only after you click power on (browsers block autoplay). If you hear nothing, raise master, and check that cutoff is not floored.
09The same instrument in JUCE
In C++ the JUCE framework gives the polyphony and MIDI plumbing for free through juce::Synthesiser, which owns a set of SynthesiserVoice objects and a matching SynthesiserSound. You write the per-voice DSP; JUCE does the voice allocation, note routing and block splitting. The mapping to the JavaScript above is one-to-one.
// ---- the sound: says "yes, I can play this note/channel" ----
struct SubSound : public juce::SynthesiserSound {
bool appliesToNote (int) override { return true; }
bool appliesToChannel (int) override { return true; }
};
// ---- one voice: 2 PolyBLEP oscillators -> TPT ladder -> ADSRs ----
class SubVoice : public juce::SynthesiserVoice {
public:
SubVoice (juce::AudioProcessorValueTreeState& s) : apvts (s) {
ladder.setMode (juce::dsp::LadderFilterMode::LPF24);
}
bool canPlaySound (juce::SynthesiserSound* s) override {
return dynamic_cast<SubSound*> (s) != nullptr;
}
void prepare (const juce::dsp::ProcessSpec& spec){
ladder.prepare (spec); fs = spec.sampleRate;
ampEnv.setSampleRate (fs); fltEnv.setSampleRate (fs);
}
void startNote (int midiNote, float vel,
juce::SynthesiserSound*, int) override {
level = vel;
freq = juce::MidiMessage::getMidiNoteInHertz (midiNote);
phase1 = phase2 = 0.0;
ampEnv.noteOn(); fltEnv.noteOn();
}
void stopNote (float, bool allowTailOff) override {
if (allowTailOff) { ampEnv.noteOff(); fltEnv.noteOff(); }
else { clearCurrentNote(); ampEnv.reset(); }
}
void pitchWheelMoved (int) override {}
void controllerMoved (int, int) override {}
// --- the DSP block: JUCE has already allocated us to a note ---
void renderNextBlock (juce::AudioBuffer<float>& out,
int start, int num) override {
if (! ampEnv.isActive()) return;
updateParams(); // read APVTS knobs
for (int i = 0; i < num; ++i){
const double d1 = freq / fs;
const double d2 = freq * detuneRatio / fs;
float s1 = blepSaw (phase1, d1); // hand-written PolyBLEP
float s2 = blepSaw (phase2, d2);
float osc = (1.0f-mix)*s1 + mix*s2;
float fenv = fltEnv.getNextSample();
float a = ampEnv.getNextSample();
// cutoff modulated by the filter envelope + LFO
float fc = juce::jlimit (20.0f, float(fs*0.49f),
baseCut * std::pow (2.0f, envAmt*fenv + lfoAmt*lfo()));
ladder.setCutoffFrequencyHz (fc);
float v = ladder.processSample (osc, 0) * a * level * 0.25f;
for (int ch = 0; ch < out.getNumChannels(); ++ch)
out.addSample (ch, start + i, v);
phase1 += d1; if (phase1 >= 1.0) phase1 -= 1.0;
phase2 += d2; if (phase2 >= 1.0) phase2 -= 1.0;
}
if (! ampEnv.isActive()) clearCurrentNote();
}
private:
static float polyBlep (double t, double dt){
if (t < dt) { t /= dt; return float(t+t - t*t - 1.0); }
if (t > 1.0 - dt) { t = (t-1.0)/dt; return float(t*t + t+t + 1.0); }
return 0.0f;
}
float blepSaw (double ph, double dt){
return float(2.0*ph - 1.0) - polyBlep (ph, dt);
}
juce::AudioProcessorValueTreeState& apvts;
juce::dsp::LadderFilter<float> ladder; // or a hand-written TPT SVF
juce::ADSR ampEnv, fltEnv;
double fs = 44100.0, phase1 = 0, phase2 = 0, freq = 440;
float level = 1, mix = .5f, detuneRatio = 1.003f,
baseCut = 1200.f, envAmt = 2.f, lfoAmt = 0.f;
/* updateParams(), lfo() read the APVTS and a shared LFO */
};
// ---- the processor wires MIDI straight into the Synthesiser ----
void SubSynthAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer,
juce::MidiBuffer& midi) {
buffer.clear();
synth.renderNextBlock (buffer, midi, 0, buffer.getNumSamples());
// soft limiter on the master bus guards against polyphonic clipping
limiter.process (juce::dsp::ProcessContextReplacing<float> (block));
}
// ---- parameters via APVTS ----
juce::AudioProcessorValueTreeState::ParameterLayout createLayout(){
using P = juce::AudioParameterFloat; using R = juce::NormalisableRange<float>;
juce::AudioProcessorValueTreeState::ParameterLayout L;
L.add (std::make_unique<P>("cutoff","Cutoff", R(20.f,20000.f,1.f,0.3f), 1200.f));
L.add (std::make_unique<P>("reso","Resonance", R(0.f,1.f), 0.3f));
L.add (std::make_unique<P>("attack","Attack", R(0.001f,3.f,0,0.4f), 0.01f));
/* decay, sustain, release, envAmt, detune, mix, lfoRate, lfoAmt … */
return L;
}
// ---- editor exposes a MIDI keyboard, like the on-screen one above ----
class SubSynthEditor : public juce::AudioProcessorEditor {
juce::MidiKeyboardState kbState;
juce::MidiKeyboardComponent keyboard { kbState,
juce::MidiKeyboardComponent::horizontalKeyboard };
// keyboard feeds kbState -> processor.midiCollector, exactly as
// the browser keyboard feeds noteOn()/noteOff() into the worklet.
};
Structurally identical to the demo: JUCE's Synthesiser is the voice allocator of §6; LadderFilter (or a hand-rolled TPT SVF) is §5; juce::ADSR is §4; blepSaw is §3. Substitute juce::dsp::Oscillator if you prefer wavetables to hand-written BLEP.
Block
This demo (JS worklet)
JUCE (C++)
Voice pool / stealing
voices[] + find/reduce
juce::Synthesiser
Oscillator
polyBlep() saw/sq/tri
blepSaw() / dsp::Oscillator
Filter
TPT SVF (cytomic)
dsp::LadderFilter / TPT SVF
Envelopes
exponential ADSR state-machine
juce::ADSR ×2
Parameters
port messages from knobs
AudioProcessorValueTreeState
Keyboard / MIDI
DOM keys + keydown
MidiKeyboardComponent
Master safety
tanh soft limiter
dsp::Limiter
10Context, comparison & references
Subtractive is one of several synthesis paradigms. It endures because it is intuitive — a filter you can hear yourself opening — and cheap. Here is where it sits, and where to read further.
Paradigm
Source → shaping
Strengths
Weaknesses
Subtractive
rich osc → filter
intuitive, warm, cheap, expressive filter
limited to what a filter can carve
Additive
sum of sines
exact spectral control
many partials to manage; costly
FM / PM
modulate phase
metallic, bell, e-piano timbres
unintuitive; hard to dial in
Wavetable
scan through waveforms
evolving, modern (Serum, Vital)
needs anti-aliased tables
Sample / granular
play/scatter recordings
realism, texture
storage; less "synthetic" control
Modern software synths blur these lines: Serum and Vital are wavetable engines wrapped in a thoroughly subtractive control surface — the filter, the ADSRs, the LFOs are all here. Learn subtractive and you can drive almost anything.
References
Välimäki, V. & Huovilainen, A. — "Antialiasing Oscillators in Subtractive Synthesis", IEEE Signal Processing Magazine, 24(2), 2007. (The BLEP / PolyBLEP methods used in §3.)
Zavalishin, V. — The Art of VA Filter Design, Native Instruments, rev. 2.1.2. (TPT integrators and the state-variable filter of §5.)
Simper, A. — "Solving the continuous SVF equations using trapezoidal integration", Cytomic technical paper, 2013. (The exact per-sample update coded in §5.)
Stilson, T. & Smith, J. O. — "Analyzing the Moog VCF with Considerations for Digital Implementation", ICMC 1996. (The four-pole ladder.)
Huovilainen, A. — "Non-linear Digital Implementation of the Moog Ladder Filter", DAFx 2004.
Moog, R. A. — "Voltage-Controlled Electronic Music Modules", JAES, 1965. (The VCO/VCF/VCA architecture of §1.)
Smith, J. O. — Physical Audio Signal Processing and Introduction to Digital Filters, CCRMA (online). (Bilinear transform, integrators.)
Helmholtz, H. von — On the Sensations of Tone, 1863 (English tr. Ellis, 1875).
Pirkle, W. — Designing Software Synthesizer Plugins in C++, Routledge, 2021.
MIDI Manufacturers Association — MIDI 1.0 Detailed Specification, 1983.