LfoBlock

Low-frequency oscillator for parameter modulation.

Overview

LfoBlock generates low-frequency control signals for modulating parameters like pitch, amplitude, filter cutoff, and panning. Unlike audio-rate oscillators, LFOs operate at control rate (typically < 20 Hz), producing smooth, slowly-varying signals.

Mathematical Foundation

What is Modulation?

Modulation is the process of varying one signal (the carrier) using another signal (the modulator). In synthesis:

  • Carrier: The audio signal being modified (e.g., an oscillator)
  • Modulator: The control signal doing the modifying (the LFO)
  • Modulation depth: How much the modulator affects the carrier

Phase Accumulation

Like audio oscillators, LFOs use a phase accumulator:

$$ \phi[n] = \phi[n-1] + \Delta\phi $$

where the phase increment is:

$$ \Delta\phi = \frac{2\pi f_{LFO}}{f_s} $$

The key difference is that $f_{LFO}$ is typically 0.01-20 Hz rather than 20-20000 Hz.

Output Scaling

The raw oscillator output $w(\phi) \in [-1, 1]$ is scaled by depth:

$$ y[n] = d \cdot w(\phi[n]) $$

where $d$ is the depth parameter (0.0 to 1.0).

The final output range is $[-d, +d]$, centered at zero.

Control Rate vs Audio Rate

Audio-rate modulation (sample-by-sample):

  • Frequency: 20 Hz - 20 kHz
  • Creates new frequencies (sidebands)
  • Used for FM synthesis, ring modulation

Control-rate modulation (per-buffer):

  • Frequency: 0.01 Hz - ~20 Hz
  • Smoothly varies parameters
  • Used for vibrato, tremolo, auto-pan

Control-rate processing is more efficient because it computes one value per buffer rather than one per sample.

Maximum LFO Frequency

Due to control-rate operation, the maximum useful LFO frequency is limited by the Nyquist criterion for control signals:

$$ f_{max} = \frac{f_s}{2 \cdot B} $$

where $B$ is the buffer size.

For 44.1 kHz sample rate and 512-sample buffers: $$ f_{max} = \frac{44100}{2 \times 512} \approx 43 \text{ Hz} $$

Above this frequency, the LFO output will alias in the control domain.

Common Modulation Effects

EffectTarget ParameterTypical RateTypical Depth
VibratoPitch/Frequency4-7 Hz0.1-0.5
TremoloAmplitude/Gain4-10 Hz0.3-1.0
Auto-panPan position0.1-1 Hz0.5-1.0
Filter sweepFilter cutoff0.05-2 Hz0.3-0.8
Wobble bassFilter cutoff1-4 Hz0.5-1.0

Creating an LFO

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

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

let lfo = builder.add(LfoBlock::new(5.0, 0.5, Waveform::Sine, None));
}

For non-sine waveforms:

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

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

let lfo = builder.add(LfoBlock::new(2.0, 0.8, Waveform::Triangle, None));
}

Port Layout

PortDirectionDescription
0Modulation OutputControl signal (-depth to +depth)

Parameters

ParameterTypeRangeDefaultDescription
frequencyf640.01 - ~43 Hz1.0Oscillation rate
depthf640.0 - 1.01.0Output amplitude
waveformWaveform-SineShape of modulation
seedOption<u64>AnyNoneRandom seed (for Noise)

Waveforms

WaveformCharacterUse Case
SineSmooth, naturalVibrato, tremolo
TriangleLinear, symmetricPitch wobble
SawtoothRising rampFilter sweeps
SquareAbrupt on/offGated effects
NoiseRandomOrganic variation

Modulation Output

The LFO output ranges from -1.0 to 1.0 (scaled by depth). The receiving block interprets this:

  • Pitch: Maps to frequency deviation
  • Amplitude: Maps to gain change
  • Pan: Maps to position change

Usage Examples

Vibrato (Pitch Modulation)

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

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

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

builder.modulate(lfo, osc, "frequency");
}

Tremolo (Amplitude Modulation)

#![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));
let lfo = builder.add(LfoBlock::new(6.0, 1.0, Waveform::Sine, None));
let gain = builder.add(GainBlock::new(-6.0, None));

builder.connect(osc, 0, gain, 0);
builder.modulate(lfo, gain, "level_db");
}

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));
let lfo = builder.add(LfoBlock::new(0.25, 1.0, Waveform::Sine, None));
let pan = builder.add(PannerBlock::new(0.0));

builder.connect(osc, 0, pan, 0);
builder.modulate(lfo, pan, "position");
}

Filter Sweep

#![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);

let osc = builder.add(OscillatorBlock::new(440.0, Waveform::Saw, None));
let filter = builder.add(LowPassFilterBlock::new(1000.0, 4.0));
let lfo = builder.add(LfoBlock::new(0.1, 0.8, Waveform::Sine, None));

builder.connect(osc, 0, filter, 0);
builder.modulate(lfo, filter, "cutoff");
}

Square LFO for Gated 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::Saw, None));
let lfo = builder.add(LfoBlock::new(4.0, 1.0, Waveform::Square, None));
let gain = builder.add(GainBlock::new(0.0, None));

builder.connect(osc, 0, gain, 0);
builder.modulate(lfo, gain, "level_db");
}

Rate Guidelines

ApplicationRate RangeNotes
Vibrato4-7 HzNatural vocal/string range
Tremolo4-10 HzFaster = more intense
Auto-pan0.1-1 HzSlower = more subtle
Filter wobble1-4 HzDubstep/bass music
Slow evolution0.01-0.1 HzPad textures

Implementation Notes

  • Operates at control rate (per-buffer, not per-sample)
  • Phase is continuous across buffer boundaries
  • Uses band-limited waveforms (PolyBLEP) to reduce aliasing
  • Deterministic output when seed is provided
  • SIMD-optimized for non-Noise waveforms

Further Reading

  • Roads, C. (1996). The Computer Music Tutorial, Chapter 5: Modulation Synthesis. MIT Press.
  • Puckette, M. (2007). Theory and Techniques of Electronic Music, Chapter 7. World Scientific.
  • Russ, M. (2012). Sound Synthesis and Sampling, Chapter 3: Modifiers. Focal Press.