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

Your First Patch

Let’s build a complete synthesizer voice: an oscillator through a filter, shaped by an envelope. This is the classic subtractive synthesis signal path.

flowchart LR
    VCO[VCO] -->|saw| VCF[VCF]
    VCF -->|lowpass| VCA[VCA]
    ADSR[ADSR] -->|env| VCF
    ADSR -->|env| VCA
    GATE[Gate] -->|trigger| ADSR
    VCA --> OUT[Output]

The Complete Example

//! First Patch Example
//!
//! A complete subtractive synthesizer voice demonstrating the core
//! Quiver workflow: VCO → VCF → VCA with ADSR envelope shaping.
//!
//! Run with: cargo run --example first_patch

use quiver::prelude::*;
use std::sync::Arc;

fn main() {
    // CD-quality sample rate
    let sample_rate = 44100.0;

    // Create our patch (virtual modular case)
    let mut patch = Patch::new(sample_rate);

    // External control: gate signal for envelope triggering
    let gate_cv = Arc::new(AtomicF64::new(0.0));

    // Add modules to the patch
    let gate = patch.add("gate", ExternalInput::gate(Arc::clone(&gate_cv)));
    let vco = patch.add("vco", Vco::new(sample_rate));
    let vcf = patch.add("vcf", Svf::new(sample_rate));
    let vca = patch.add("vca", Vca::new());
    let env = patch.add("env", Adsr::new(sample_rate));
    let output = patch.add("output", StereoOutput::new());

    // Patch cables: signal flow
    // Gate triggers the envelope
    patch.connect(gate.out("out"), env.in_("gate")).unwrap();

    // VCO → VCF → VCA → Output (main audio path)
    patch.connect(vco.out("saw"), vcf.in_("in")).unwrap();
    patch.connect(vcf.out("lp"), vca.in_("in")).unwrap();
    patch.connect(vca.out("out"), output.in_("left")).unwrap();
    patch.connect(vca.out("out"), output.in_("right")).unwrap();

    // Envelope modulates both filter and amplitude
    patch.connect(env.out("env"), vcf.in_("cutoff")).unwrap();
    patch.connect(env.out("env"), vca.in_("cv")).unwrap();

    // Compile the patch for processing
    patch.set_output(output.id());
    patch.compile().unwrap();

    println!(
        "Patch compiled: {} modules, {} cables",
        patch.node_count(),
        patch.cable_count()
    );
    println!();

    // Play a note: gate on
    println!("Note ON - Gate rises to +5V");
    gate_cv.set(5.0);

    // Process attack phase (0.5 seconds)
    let attack_samples = (sample_rate * 0.5) as usize;
    let mut peak = 0.0_f64;

    for _ in 0..attack_samples {
        let (left, _) = patch.tick();
        peak = peak.max(left.abs());
    }
    println!("  Attack complete, peak level: {:.2}V", peak);

    // Release the note: gate off
    println!("Note OFF - Gate falls to 0V");
    gate_cv.set(0.0);

    // Process release phase
    let release_samples = (sample_rate * 1.0) as usize;
    let mut release_peak = 0.0_f64;

    for _ in 0..release_samples {
        let (left, _) = patch.tick();
        release_peak = release_peak.max(left.abs());
    }
    println!("  Release complete, final level: {:.4}V", release_peak);

    println!();
    println!("Subtractive synthesis voice complete!");
}

Understanding the Code

Creating a Patch

let mut patch = Patch::new(44100.0);

The Patch is your virtual modular case. The sample rate (44100 Hz = CD quality) determines timing precision for all modules.

Adding Modules

let vco = patch.add("vco", Vco::new(44100.0));
let vcf = patch.add("vcf", Svf::new(44100.0));

Each module gets a unique name and returns a NodeHandle. This handle lets you reference the module’s ports.

Making Connections

patch.connect(vco.out("saw"), vcf.in_("in")).unwrap();

The syntax mirrors real patching:

  • vco.out("saw") — the sawtooth output jack
  • vcf.in_("in") — the filter’s audio input jack

Note: We use in_() instead of in() because in is a Rust keyword.

Compiling the Patch

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

Compilation:

  1. Performs topological sort (determines processing order)
  2. Validates all connections
  3. Detects any cycles (feedback loops)

Processing Audio

let (left, right) = patch.tick();

Each tick() advances the patch by one sample, returning stereo output.

Signal Flow in Detail

StageModuleFunction
1VCOGenerates raw waveform (saw wave)
2VCFFilters harmonics (lowpass)
3VCAControls amplitude
4ADSRShapes volume over time
5OutputRoutes to stereo outputs

The envelope simultaneously controls:

  • Filter cutoff — brighter attack, darker sustain
  • VCA level — shapes volume contour

This dual modulation creates the characteristic “filter sweep” sound of analog synths.

What’s Happening Mathematically

The signal chain computes:

$$\text{output}(t) = \text{env}(t) \cdot \text{LPF}(\text{saw}(t), \text{env}(t) \cdot f_c)$$

Where:

  • $\text{saw}(t)$ is the sawtooth oscillator at time $t$
  • $\text{LPF}$ is the lowpass filter
  • $\text{env}(t)$ is the envelope value
  • $f_c$ is the base cutoff frequency

The envelope modulating both the filter and amplitude creates the classic synth timbre.

Experimenting

Try these modifications:

  1. Different waveform: Change vco.out("saw") to vco.out("sqr") for a hollow, clarinet-like tone

  2. Add an LFO: Modulate the filter for a rhythmic wobble

  3. Change envelope times: Longer attack for pads, shorter for percussion


Next: Understanding Signal Flow