Parameter Modulation with LFOs

This tutorial covers using LFOs and envelopes to modulate block parameters.

Prior knowledge: This tutorial builds on:

Low-Frequency Oscillators (LFOs)

LFOs generate control signals for modulating parameters like pitch, volume, and filter cutoff.

#![allow(unused)]
fn main() {
use bbx_dsp::{
    blocks::LfoBlock,
    graph::GraphBuilder,
    waveform::Waveform,
};

let mut builder = GraphBuilder::<f32>::new(44100.0, 512, 2);

// Create an LFO at 4 Hz with 0.5 depth
// Parameters: frequency (Hz), depth (0.0-1.0), waveform, optional seed
let lfo = builder.add(LfoBlock::new(4.0, 0.5, Waveform::Sine, None));
}

Vibrato (Pitch Modulation)

Modulate oscillator frequency for vibrato using the modulate() method:

#![allow(unused)]
fn main() {
use bbx_dsp::{
    blocks::{LfoBlock, OscillatorBlock},
    graph::GraphBuilder,
    waveform::Waveform,
};

let mut builder = GraphBuilder::<f32>::new(44100.0, 512, 2);

// LFO for vibrato (5 Hz, moderate depth)
let vibrato_lfo = builder.add(LfoBlock::new(5.0, 0.3, Waveform::Sine, None));

// Oscillator
let osc = builder.add(OscillatorBlock::new(440.0, Waveform::Sine, None));

// Connect LFO to modulate oscillator frequency
builder.modulate(vibrato_lfo, osc, "frequency");

let graph = builder.build();
}

Typical vibrato settings:

  • Rate: 4-7 Hz
  • Depth: 0.1-0.5 for subtle vibrato
  • Parameter: "frequency" or "pitch_offset"

Tremolo (Amplitude Modulation)

Modulate gain for tremolo effect:

#![allow(unused)]
fn main() {
use bbx_dsp::{
    blocks::{GainBlock, LfoBlock, OscillatorBlock},
    graph::GraphBuilder,
    waveform::Waveform,
};

let mut builder = GraphBuilder::<f32>::new(44100.0, 512, 2);

let osc = builder.add(OscillatorBlock::new(440.0, Waveform::Sine, None));

// LFO for tremolo (6 Hz, full depth)
let tremolo_lfo = builder.add(LfoBlock::new(6.0, 1.0, Waveform::Sine, None));

// Gain block
let gain = builder.add(GainBlock::new(-6.0, None));

// Connect oscillator to gain
builder.connect(osc, 0, gain, 0);

// Modulate gain level with LFO
builder.modulate(tremolo_lfo, gain, "level");

let graph = builder.build();
}

LFO Parameters

ParameterTypeRangeDescription
frequencyf640.01 - max*Rate in Hz
depthf640.0 - 1.0Modulation intensity
seedOption<u64>AnyFor deterministic output

*Max frequency is sample_rate / (2 * buffer_size) due to control-rate operation (e.g., ~43 Hz at 44.1 kHz with 512-sample buffers).

Envelope Generator

ADSR envelopes control how parameters change over time. For a practical application with VCA, see Building a Terminal Synthesizer - Part 2.

#![allow(unused)]
fn main() {
use bbx_dsp::{blocks::EnvelopeBlock, graph::GraphBuilder};

let mut builder = GraphBuilder::<f32>::new(44100.0, 512, 2);

// Create an ADSR envelope
// Parameters: attack, decay, sustain (level), release (all in seconds except sustain)
let envelope = builder.add(EnvelopeBlock::new(
    0.01,   // Attack: 10ms
    0.1,    // Decay: 100ms
    0.7,    // Sustain: 70%
    0.3,    // Release: 300ms
));
}

Combining Modulation Sources

Layer multiple modulation sources:

#![allow(unused)]
fn main() {
use bbx_dsp::{
    blocks::{GainBlock, LfoBlock, OscillatorBlock},
    graph::GraphBuilder,
    waveform::Waveform,
};

let mut builder = GraphBuilder::<f32>::new(44100.0, 512, 2);

// Slow LFO for overall movement
let slow_lfo = builder.add(LfoBlock::new(0.5, 0.3, Waveform::Sine, None));

// Fast LFO for vibrato
let fast_lfo = builder.add(LfoBlock::new(5.0, 0.2, Waveform::Sine, None));

// Oscillator with vibrato
let osc = builder.add(OscillatorBlock::new(440.0, Waveform::Sine, None));
builder.modulate(fast_lfo, osc, "frequency");

// Gain with slow amplitude modulation
let gain = builder.add(GainBlock::new(-6.0, None));
builder.connect(osc, 0, gain, 0);
builder.modulate(slow_lfo, gain, "level");

let graph = builder.build();
}

Modulation Depth

Control modulation intensity through the LFO's depth parameter:

#![allow(unused)]
fn main() {
use bbx_dsp::{
    blocks::LfoBlock,
    graph::GraphBuilder,
    waveform::Waveform,
};

let mut builder = GraphBuilder::<f32>::new(44100.0, 512, 2);

// Subtle vibrato: low depth
let subtle_lfo = builder.add(LfoBlock::new(5.0, 0.1, Waveform::Sine, None));

// Intense wobble: high depth
let intense_lfo = builder.add(LfoBlock::new(2.0, 1.0, Waveform::Sine, None));
}

Practical Examples

Filter Sweep (Wah Effect)

Modulate filter cutoff for classic wah/sweep effects:

#![allow(unused)]
fn main() {
use bbx_dsp::{
    blocks::{LfoBlock, LowPassFilterBlock, OscillatorBlock},
    graph::GraphBuilder,
    waveform::Waveform,
};

let mut builder = GraphBuilder::<f32>::new(44100.0, 512, 2);

// Rich harmonic source
let osc = builder.add(OscillatorBlock::new(110.0, Waveform::Sawtooth, None));

// Resonant low-pass filter
let filter = builder.add(LowPassFilterBlock::new(800.0, 4.0));

// LFO to sweep the cutoff
let lfo = builder.add(LfoBlock::new(0.5, 3000.0, Waveform::Sine, None));

// Build chain
builder.connect(osc, 0, filter, 0);
builder.modulate(lfo, filter, "cutoff");

let graph = builder.build();
}

See the 10_filter_modulation example for a working demonstration.

Wobble Bass

#![allow(unused)]
fn main() {
use bbx_dsp::{
    blocks::{GainBlock, LfoBlock, OscillatorBlock},
    graph::GraphBuilder,
    waveform::Waveform,
};

let mut builder = GraphBuilder::<f32>::new(44100.0, 512, 2);

// Sub bass oscillator
let osc = builder.add(OscillatorBlock::new(55.0, Waveform::Saw, None));

// Slow LFO for amplitude wobble
let wobble_lfo = builder.add(LfoBlock::new(2.0, 0.8, Waveform::Sine, None));

// Gain block for wobble effect
let gain = builder.add(GainBlock::new(-6.0, None));
builder.connect(osc, 0, gain, 0);
builder.modulate(wobble_lfo, gain, "level");

let graph = builder.build();
}

Auto-Pan

#![allow(unused)]
fn main() {
use bbx_dsp::{
    blocks::{LfoBlock, OscillatorBlock, PannerBlock},
    graph::GraphBuilder,
    waveform::Waveform,
};

let mut builder = GraphBuilder::<f32>::new(44100.0, 512, 2);

let osc = builder.add(OscillatorBlock::new(440.0, Waveform::Sine, None));

// LFO for pan position (slow sweep)
let pan_lfo = builder.add(LfoBlock::new(0.25, 1.0, Waveform::Sine, None));

// Panner block
let pan = builder.add(PannerBlock::new(0.0));
builder.connect(osc, 0, pan, 0);

// Modulate pan position
builder.modulate(pan_lfo, pan, "position");

let graph = builder.build();
}

See the 07_stereo_panner example for a multi-layer drone with independent pan modulation.

Next Steps