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

Filter Modulation

Modulation brings patches to life. When we connect an LFO (Low Frequency Oscillator) to the filter cutoff, static becomes dynamic—a still photograph becomes a movie.

flowchart LR
    LFO[LFO<br/>~2Hz] -->|mod| VCF[Filter<br/>Cutoff]
    VCO[VCO] -->|audio| VCF
    VCF --> OUT[Output]

    style LFO fill:#f9a826,color:#000

LFO: The Modulation Source

An LFO is simply an oscillator running at sub-audio rates:

Audio OscillatorLFO
20Hz - 20kHz0.01Hz - 30Hz
Creates pitchCreates movement
You hear itYou feel its effect
graph LR
    subgraph "LFO Waveforms"
        SIN[Sine<br/>Smooth sweep]
        TRI[Triangle<br/>Linear sweep]
        SAW[Saw<br/>Ramp + drop]
        SQR[Square<br/>Two states]
    end

The Mathematics of Modulation

Filter cutoff with LFO modulation:

$$f_c(t) = f_{center} + f_{depth} \cdot \text{LFO}(t)$$

Where:

  • $f_{center}$ is the base cutoff frequency
  • $f_{depth}$ is the modulation depth (how far it sweeps)
  • $\text{LFO}(t)$ oscillates between -1 and +1

Building the Patch

//! Tutorial: Filter Modulation
//!
//! Demonstrates LFO modulation of filter cutoff - the classic "wobble"
//! that brings patches to life.
//!
//! Run with: cargo run --example tutorial_filter_mod

use quiver::prelude::*;

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

    // Sound source - sawtooth oscillator
    let vco = patch.add("vco", Vco::new(sample_rate));

    // LFO for modulation (runs at sub-audio rate)
    let lfo = patch.add("lfo", Lfo::new(sample_rate));

    // Filter - we'll modulate its cutoff
    let vcf = patch.add("vcf", Svf::new(sample_rate));

    // Base cutoff offset
    let cutoff_base = patch.add("cutoff_base", Offset::new(3.0));

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

    // Audio path: VCO → Filter → Output
    patch.connect(vco.out("saw"), vcf.in_("in")).unwrap();
    patch.connect(vcf.out("lp"), output.in_("left")).unwrap();

    // Modulation: LFO → Filter cutoff (with base offset)
    patch
        .connect(cutoff_base.out("out"), vcf.in_("cutoff"))
        .unwrap();
    patch.connect(lfo.out("sin"), vcf.in_("fm")).unwrap();

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

    println!("=== Filter Modulation Demo ===\n");
    println!("LFO modulating filter cutoff creates the classic 'wobble' effect.\n");

    // Generate 2 seconds of audio to hear multiple LFO cycles
    let duration = 2.0;
    let total_samples = (sample_rate * duration) as usize;

    // Track the signal envelope over time
    let block_size = (sample_rate / 10.0) as usize; // 100ms blocks
    let mut time = 0.0;

    println!("Time(s)  | Peak Level | Character");
    println!("---------|------------|----------");

    for block in 0..(total_samples / block_size) {
        let mut peak = 0.0_f64;

        for _ in 0..block_size {
            let (left, _) = patch.tick();
            peak = peak.max(left.abs());
        }

        // Describe the sound character based on peak
        let character = if peak > 4.0 {
            "Bright (filter open)"
        } else if peak > 2.0 {
            "Medium"
        } else {
            "Dark (filter closed)"
        };

        if block % 5 == 0 {
            println!("{:7.2}  | {:10.2}V | {}", time, peak, character);
        }

        time += block_size as f64 / sample_rate;
    }

    println!("\nThe LFO creates a periodic sweep of the filter,");
    println!("cycling between bright (open) and dark (closed) states.");
    println!("\nTry different LFO waveforms:");
    println!("  - sin: smooth, natural sweep");
    println!("  - tri: linear ramp up and down");
    println!("  - saw: slow rise, fast drop");
    println!("  - sqr: instant toggle between states");
}

Modulation Depth and Attenuverters

The amount of modulation matters:

DepthEffect
10%Subtle shimmer
25%Noticeable movement
50%Dramatic sweep
100%Extreme wah-wah

Quiver cables support attenuation:

// Connect with 50% modulation depth
patch.connect_with(
    lfo.out("sin"),
    vcf.in_("cutoff"),
    Cable::new().with_attenuation(0.5),
)?;

Waveform Shapes

Each LFO waveform creates a different movement:

Sine Wave

Smooth, natural sweeping—good for gentle effects.

    ╱╲    ╱╲    ╱╲
   ╱  ╲  ╱  ╲  ╱  ╲
──╱────╲╱────╲╱────╲──

Triangle Wave

Linear sweeping—predictable, good for trills.

   ╱╲    ╱╲    ╱╲
  ╱  ╲  ╱  ╲  ╱  ╲
─╱────╲╱────╲╱────╲─

Sawtooth Wave

Rises slowly, drops instantly—creates rhythmic “pumping.”

   ╱│   ╱│   ╱│
  ╱ │  ╱ │  ╱ │
─╱──│─╱──│─╱──│──

Square Wave

Instant alternation between two states—tremolo/vibrato effect.

 ┌──┐  ┌──┐  ┌──┐
 │  │  │  │  │  │
─┘  └──┘  └──┘  └─

Rate and Depth Interaction

quadrantChart
    title LFO Character
    x-axis Slow Rate --> Fast Rate
    y-axis Subtle Depth --> Deep Depth
    quadrant-1 Vibrato/Tremolo
    quadrant-2 Slow Sweep
    quadrant-3 Subtle Texture
    quadrant-4 Frantic Motion
RateDepthClassic Use
0.5Hz30%Slow filter sweep
2Hz10%Subtle shimmer
6Hz50%Dubstep wobble
8Hz5%Guitar vibrato

Multiple Modulation Sources

Combine LFO with envelope for evolving sounds:

flowchart TD
    LFO[LFO<br/>Ongoing movement]
    ENV[Envelope<br/>Per-note shape]
    SUM((Σ))
    VCF[Filter Cutoff]

    LFO --> SUM
    ENV --> SUM
    SUM --> VCF

The envelope provides the initial “brightness burst,” while the LFO adds continuous movement during sustain.


Next: Building a Sequenced Bass