Cookbook

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#

ParameterDefaultRangeMeaning
rate0.2 Hz0.01–20 HzSweep speed; the default completes a cycle every five seconds.
base90 Hz30–4,000 HzLowest requested cutoff.
depth2,800 Hz0–3,500 HzAmount added at the top of the sweep; base plus depth reaches at most 7,500 Hz.
resonance0.750–0.97Emphasis around the cutoff. Higher values ring more.
level0.70–1Gain 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#

A filterPlayground
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.