Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Basic Subtractive Synthesis

Subtractive synthesis is the foundation of analog synthesizers. Start with a harmonically rich waveform, then sculpt it by filtering away frequencies.

flowchart LR
    OSC[Oscillator<br/>Rich harmonics] --> FILTER[Filter<br/>Remove harmonics]
    FILTER --> AMP[Amplifier<br/>Shape volume]
    AMP --> OUT[Output]

    style OSC fill:#4a9eff,color:#fff
    style FILTER fill:#f9a826,color:#000
    style AMP fill:#50c878,color:#fff

The Physics of Waveforms

Different waveforms have different harmonic content:

WaveformHarmonicsSound Character
SineFundamental onlyPure, flute-like
TriangleOdd harmonics (weak)Soft, clarinet-like
SawtoothAll harmonicsBright, brassy
SquareOdd harmonics (strong)Hollow, woody

The mathematical representation:

Sawtooth wave: $$x(t) = \frac{2}{\pi} \sum_{k=1}^{\infty} \frac{(-1)^{k+1}}{k} \sin(2\pi k f t)$$

This infinite sum of harmonics is what gives the sawtooth its brightness.

Building the Patch

//! Tutorial: Basic Subtractive Synthesis
//!
//! This example demonstrates the fundamentals of subtractive synthesis:
//! starting with a harmonically rich oscillator and shaping it with a filter.
//!
//! Run with: cargo run --example tutorial_subtractive

use quiver::prelude::*;

fn main() {
    let sample_rate = 44100.0;
    let mut patch = Patch::new(sample_rate);

    // The oscillator: source of harmonics
    let vco = patch.add("vco", Vco::new(sample_rate));

    // The filter: subtracts harmonics
    let vcf = patch.add("vcf", Svf::new(sample_rate));

    // Output stage
    let output = patch.add("output", StereoOutput::new());

    // Offset module to set filter cutoff (in CV range)
    // 5V corresponds to a medium-high cutoff frequency
    let cutoff = patch.add("cutoff", Offset::new(5.0));

    // Connect: Saw wave → Filter → Output
    patch.connect(vco.out("saw"), vcf.in_("in")).unwrap();
    patch.connect(cutoff.out("out"), vcf.in_("cutoff")).unwrap();
    patch.connect(vcf.out("lp"), output.in_("left")).unwrap();

    patch.set_output(output.id());
    patch.compile().unwrap();

    // Generate samples and analyze harmonic content
    println!("=== Subtractive Synthesis Demo ===\n");

    // Collect one period of audio (assuming ~261Hz C4)
    let period_samples = (sample_rate / 261.63) as usize;
    let mut samples: Vec<f64> = Vec::new();

    for _ in 0..period_samples * 10 {
        let (left, _) = patch.tick();
        samples.push(left);
    }

    // Analyze the filtered output
    let peak = samples.iter().map(|s| s.abs()).fold(0.0_f64, f64::max);
    let rms = (samples.iter().map(|s| s * s).sum::<f64>() / samples.len() as f64).sqrt();

    println!("Sawtooth → Lowpass Filter");
    println!("  Peak amplitude: {:.2}V", peak);
    println!("  RMS level: {:.2}V", rms);
    println!("  Samples generated: {}", samples.len());

    // Compare with unfiltered saw
    let mut raw_patch = Patch::new(sample_rate);
    let raw_vco = raw_patch.add("vco", Vco::new(sample_rate));
    let raw_out = raw_patch.add("output", StereoOutput::new());
    raw_patch
        .connect(raw_vco.out("saw"), raw_out.in_("left"))
        .unwrap();
    raw_patch.set_output(raw_out.id());
    raw_patch.compile().unwrap();

    let mut raw_samples: Vec<f64> = Vec::new();
    for _ in 0..period_samples * 10 {
        let (left, _) = raw_patch.tick();
        raw_samples.push(left);
    }

    let raw_peak = raw_samples.iter().map(|s| s.abs()).fold(0.0_f64, f64::max);
    let raw_rms =
        (raw_samples.iter().map(|s| s * s).sum::<f64>() / raw_samples.len() as f64).sqrt();

    println!("\nRaw Sawtooth (unfiltered)");
    println!("  Peak amplitude: {:.2}V", raw_peak);
    println!("  RMS level: {:.2}V", raw_rms);

    println!("\nThe filter has smoothed the waveform by removing high harmonics.");
    println!("Notice the lower RMS - less high-frequency energy means a softer sound.");
}

Understanding the Filter

The state-variable filter (SVF) in Quiver simultaneously outputs:

  • Lowpass — removes high frequencies
  • Bandpass — isolates a frequency band
  • Highpass — removes low frequencies
  • Notch — removes a specific band
graph TB
    subgraph "SVF Outputs"
        IN[Audio In] --> SVF[State Variable<br/>Filter]
        SVF --> LP[Lowpass]
        SVF --> BP[Bandpass]
        SVF --> HP[Highpass]
        SVF --> NOTCH[Notch]
    end

Filter Response

The lowpass filter attenuates frequencies above the cutoff:

$$H(f) = \frac{1}{\sqrt{1 + (f/f_c)^{2n}}}$$

Where $f_c$ is cutoff frequency and $n$ is filter order.

Quiver’s SVF is 12dB/octave (2-pole), meaning frequencies one octave above cutoff are reduced by 12dB.

Resonance

Resonance (Q) boosts frequencies near cutoff:

graph LR
    subgraph "Resonance Effect"
        FLAT[Low Q<br/>Flat response]
        PEAK[High Q<br/>Resonant peak]
    end

At maximum resonance, the filter self-oscillates, becoming a sine wave generator.

Experimenting

  1. Try different waveforms: Change "saw" to "sqr" or "tri"
  2. Adjust cutoff: Lower values = darker, muffled sound
  3. Add resonance: Creates a vowel-like quality
  4. Mix waveforms: Combine saw and sqr for thickness

Classic Tones

Synth SoundWaveformFilterCharacter
Moog BassSawLP, low cutoffFat, warm
Oberheim PadSaw + Saw (detuned)LP, med cutoffLush, wide
TB-303 AcidSawLP, high resonanceSquelchy
CS-80 BrassSawLP, following envelopeBrassy attack

Next: Envelope Shaping