Resonant lowpass filter DSP tutorial with a playable example
Follow a signal through a resonant lowpass and hear how a moving cutoff changes its brightness.
Anyone who wants to shape a sound rather than make one.
A filter gives a signal a memory of its recent past. That memory lets it respond differently to fast and slow changes: a lowpass keeps slower motion while reducing the fast motion we hear as brightness.
Feed this example a saw or another harmonically rich input. A triangle LFO moves the cutoff up and down, so the sound repeatedly opens and closes. The patch computes lowpass, bandpass and highpass values, and sends the lowpass to the output.
Controls#
| Parameter | Default | Range | Meaning |
|---|---|---|---|
rate | 0.2 Hz | 0.01–20 Hz | Sweep speed; the default completes a cycle every five seconds. |
base | 90 Hz | 30–4,000 Hz | Lowest requested cutoff. |
depth | 2,800 Hz | 0–3,500 Hz | Amount added at the top of the sweep; base plus depth reaches at most 7,500 Hz. |
resonance | 0.75 | 0–0.97 | Emphasis around the cutoff. Higher values ring more. |
level | 0.7 | 0–1 | Gain before the bounded output shaper. |
in and out are mono audio streams. Lower depth to zero to hear a fixed
cutoff controlled by base.
The complete patch#
processor SweepFilter
{
input stream float in;
param float rate = 0.2f [0.01f, 20.0f] smooth 15.0f;
param float base = 90.0f [30.0f, 4000.0f] smooth 15.0f;
param float depth = 2800.0f [0.0f, 3500.0f] smooth 15.0f;
param float resonance = 0.75f [0.0f, 0.97f] smooth 15.0f;
param float level = 0.7f [0.0f, 1.0f] smooth 20.0f;
output stream float out;
float lfo = 0.0f;
float lp = 0.0f;
float bp = 0.0f;
void main()
{
loop
{
let lstep = rate / processor.frequency;
let adv = lfo + lstep;
lfo = adv > 1.0f ? adv - 1.0f : adv;
let tri = lfo < 0.5f ? lfo * 2.0f : 2.0f - lfo * 2.0f;
let cutoff = min(base + tri * tri * depth,
processor.frequency * 0.18f);
let f = 6.2831853f * cutoff / processor.frequency;
let q = max(1.0f - resonance, 0.03f);
lp = lp + f * bp;
let hp = in - lp - q * bp;
bp = bp + f * hp;
let y = lp * level;
let c = y > 1.0f ? 1.0f : (y < -1.0f ? -1.0f : y);
out <- (c * 1.5f - c * c * c * 0.5f) * 0.9f;
advance();
}
}
}Follow the signal#
The LFO has one state variable, lfo. It moves from 0 to 1 and back through
the triangle expression. Squaring the triangle spends more of the sweep near
the lower cutoff. The equation base + tri * tri * depth is a curved sweep in
Hz, not exponential pitch modulation.
lp and bp are the filter's two memories. The lowpass update uses the
previous bandpass value; the highpass expression subtracts the lowpass and a
damped bandpass from the input; the bandpass update uses that highpass result.
The order matters because each assignment changes what later expressions read.
The coefficient f uses the approximation 2π * cutoff / sampleRate. The
cutoff is capped at 18% of the sample rate, but this remains a simple teaching
filter, not a general guarantee of accuracy at every frequency. A more complete
filter design would examine stability and tuning across its full control range.
Finally, the patch clamps and soft-shapes the lowpass output to leave headroom. Resonance can produce peaks much larger than the incoming signal; the output stage keeps those peaks bounded.
Try it#
Set depth to 0, move base, then increase resonance. First learn what a
fixed filter does; then bring the sweep back. To compare modes, change
let y = lp * level; to use bp or hp instead.
If it sounds unchanged: a sine well below the cutoff has little high frequency content to remove. Use a saw or noise to hear the difference.
Next: an envelope, so it can be played rather than just heard.