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

Envelope Shaping

An envelope generator shapes how a parameter changes over time. The classic ADSR (Attack, Decay, Sustain, Release) envelope is the heartbeat of synthesis.

graph LR
    subgraph "ADSR Envelope"
        A[Attack] --> D[Decay]
        D --> S[Sustain]
        S --> R[Release]
    end

Anatomy of ADSR

    │     ╱╲
    │    ╱  ╲_______
    │   ╱           ╲
    │  ╱             ╲
    │ ╱               ╲
────┴───────────────────────
    A   D    S     R
    ↑   ↑    ↑     ↑
   Gate On        Gate Off
StageDescriptionTypical Range
AttackTime to reach peak (0→5V)1ms - 10s
DecayTime to fall to sustain level1ms - 10s
SustainLevel held while gate is high0V - 5V
ReleaseTime to return to zero1ms - 10s

The Mathematics

Each stage is typically an exponential curve:

Attack (exponential rise): \[ v(t) = V_{max} \cdot (1 - e^{-t/\tau_a}) \]

Decay/Release (exponential fall): \[ v(t) = V_{start} \cdot e^{-t/\tau_d} \]

Where \( \tau \) is the time constant. Analog envelopes have this natural exponential shape—it’s how capacitors charge and discharge.

Building the Example

In this patch the four ADSR stages are not knob settings — they are CV inputs, each fed by an Offset module. The envelope shapes only the VCA, so what you hear is the pure volume contour:

Drag the four stages yourself and watch the contour respond in Envelopes Shape Time.

//! Tutorial: Envelope Shaping
//!
//! Demonstrates the ADSR envelope generator and how it shapes sound over time.
//! This is fundamental to giving synthesized sounds their character.
//!
//! # Why ADSR has four stages
//!
//! A held note isn't a static event — it has a beginning, a middle, and an
//! end, and each needs different timing:
//! - **Attack**: the time to rise from silence to full level once the gate
//!   opens. Fast (a few ms) reads as percussive/plucky; slow (100s of ms)
//!   reads as a swell/pad fade-in.
//! - **Decay**: the time to fall from that initial peak down to the
//!   **Sustain** level — not a duration but a *held level* (0-1), the
//!   volume the note stays at for as long as the gate remains open. This is
//!   what makes a plucked-string patch (high decay, low sustain — the pluck
//!   dies down to near-silence and stays there) sound different from an
//!   organ patch (sustain near 1.0 — it just holds).
//! - **Release**: the time to fall from wherever the level was when the
//!   gate closed back to zero. Short release = clipped/staccato; long
//!   release = notes bleed into each other.
//!
//! Quiver's `Adsr` maps each stage's CV input onto an exponential 1ms-10s
//! time range, so small CV changes near the low end make a big perceptual
//! difference (just like a real synth's time knobs).
//!
//! Run with: cargo run --example tutorial_envelope

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

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

    // Gate control - simulates key press
    let gate_cv = Arc::new(AtomicF64::new(0.0));
    let gate = patch.add("gate", ExternalInput::gate(Arc::clone(&gate_cv)));

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

    // ADSR envelope generator
    let env = patch.add("env", Adsr::new(sample_rate));

    // Amplifier controlled by envelope
    let vca = patch.add("vca", Vca::new());

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

    // Time-constant CVs. `Adsr` maps each 0-1 CV onto an exponential
    // 1ms-10s range via `time = 0.001 * 10000^cv`, so these values were
    // picked by solving that formula for the target times noted below —
    // slow enough that the millisecond checkpoints in the loops below land
    // inside each stage instead of after it has already finished.
    let attack_cv = patch.add("attack_cv", Offset::new(0.62)); // ~300ms attack
    let decay_cv = patch.add("decay_cv", Offset::new(0.5)); // ~100ms decay
    let sustain_cv = patch.add("sustain_cv", Offset::new(0.5)); // hold at 50% level
    let release_cv = patch.add("release_cv", Offset::new(0.64)); // ~350ms release

    // Connections
    patch.connect(gate.out("out"), env.in_("gate")).unwrap();
    patch
        .connect(attack_cv.out("out"), env.in_("attack"))
        .unwrap();
    patch
        .connect(decay_cv.out("out"), env.in_("decay"))
        .unwrap();
    patch
        .connect(sustain_cv.out("out"), env.in_("sustain"))
        .unwrap();
    patch
        .connect(release_cv.out("out"), env.in_("release"))
        .unwrap();
    patch.connect(vco.out("saw"), vca.in_("in")).unwrap();
    patch.connect(env.out("env"), vca.in_("cv")).unwrap();
    patch.connect(vca.out("out"), output.in_("left")).unwrap();

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

    println!("=== ADSR Envelope Demo ===\n");

    // Helper: run the patch for `n` samples and return the peak amplitude
    // seen. A *peak* (not just the last sample) is what we want here — a
    // single instantaneous sample would just be wherever the sawtooth
    // carrier happened to be in its cycle, not a meaningful envelope
    // reading.
    fn run_samples(patch: &mut Patch, n: usize) -> f64 {
        let mut peak = 0.0_f64;
        for _ in 0..n {
            let (left, _) = patch.tick();
            peak = peak.max(left.abs());
        }
        peak
    }

    // Start with gate off
    println!("Initial state (gate off):");
    let level = run_samples(&mut patch, 100);
    println!("  Envelope level: {:.3}V\n", level);

    // Gate ON - trigger attack
    println!("Gate ON - Attack phase begins");
    gate_cv.set(5.0);

    // Sample the attack: with a ~300ms attack time, these checkpoints show
    // the level climbing rather than already sitting at full scale.
    for ms in [10, 25, 50, 100, 200] {
        let samples = (sample_rate * ms as f64 / 1000.0) as usize;
        let level = run_samples(&mut patch, samples);
        println!("  {}ms: level = {:.2}V", ms, level);
    }

    // Let it reach sustain
    println!("\nDecay → Sustain:");
    let level = run_samples(&mut patch, (sample_rate * 0.5) as usize);
    println!("  Sustain level: {:.2}V\n", level);

    // Gate OFF - trigger release
    println!("Gate OFF - Release phase begins");
    gate_cv.set(0.0);

    // With a ~350ms release, the level should ease down across these
    // checkpoints instead of hitting zero immediately.
    for ms in [50, 100, 200, 500] {
        let samples = (sample_rate * ms as f64 / 1000.0) as usize;
        let level = run_samples(&mut patch, samples);
        println!("  +{}ms: level = {:.3}V", ms, level);
    }

    println!("\nThe envelope has completed its cycle.");
    println!("Attack→Decay→Sustain (while held) →Release (when released)");
}

Run it with cargo run --example tutorial_envelope.

Envelope as Modulation Source

The envelope doesn’t just control volume. Route it to:

flowchart TD
    ADSR[ADSR Envelope]
    ADSR -->|brightness| VCF[Filter Cutoff]
    ADSR -->|volume| VCA[Amplifier]
    ADSR -->|depth| FM[FM Amount]
    ADSR -->|color| PWM[Pulse Width]

Filter Envelope

Routing envelope to filter creates the classic “brightness sweep”:

  • Plucky bass: Fast attack, fast decay, low sustain
  • Brass stab: Medium attack, fast decay, medium sustain
  • String pad: Slow attack, slow decay, high sustain

Dual Envelope Routing

Different amounts to different destinations:

DestinationAmountEffect
VCA100%Full volume control
VCF50%Subtle brightness sweep
Pitch5%Pitch “blip” on attack

Musical Applications

Plucky Synth Bass

Attack:  5ms   (instant)
Decay:   200ms (quick fall)
Sustain: 30%   (some body)
Release: 100ms (clean cutoff)

Swelling Pad

Attack:  2s    (slow fade in)
Decay:   500ms (gentle settle)
Sustain: 80%   (full and rich)
Release: 3s    (long tail)

Percussive Hit

Attack:  1ms   (instant)
Decay:   50ms  (very fast)
Sustain: 0%    (no sustain)
Release: 50ms  (immediate)

Envelope Stages Visualization

sequenceDiagram
    participant G as Gate
    participant E as Envelope

    Note over G,E: Note On
    G->>E: Gate HIGH (+5V)
    E->>E: Attack phase (rising)
    E->>E: Decay phase (falling)
    E->>E: Sustain phase (holding)

    Note over G,E: Note Off
    G->>E: Gate LOW (0V)
    E->>E: Release phase (falling to 0)

Next: Filter Modulation