LowPassFilterBlock

SVF-based low-pass filter with cutoff and resonance control.

Overview

LowPassFilterBlock is a State Variable Filter (SVF) using the TPT (Topology Preserving Transform) algorithm. It provides:

  • Stable filtering at all cutoff frequencies
  • No delay-free loops
  • Consistent behavior regardless of sample rate

Mathematical Foundation

What is a Low-Pass Filter?

A low-pass filter attenuates frequencies above a specified cutoff frequency $f_c$ while allowing lower frequencies to pass through. The rate of attenuation above the cutoff is determined by the filter's order:

  • First-order: -6 dB/octave (-20 dB/decade)
  • Second-order: -12 dB/octave (-40 dB/decade)

This SVF implementation is second-order, providing -12 dB/octave rolloff.

Transfer Function

A second-order low-pass filter has the general continuous-time transfer function:

$$ H(s) = \frac{\omega_c^2}{s^2 + \frac{\omega_c}{Q}s + \omega_c^2} $$

where:

  • $s$ is the complex frequency variable
  • $\omega_c = 2\pi f_c$ is the cutoff angular frequency
  • $Q$ is the quality factor (resonance)

The Q Factor (Resonance)

The quality factor $Q$ controls the filter's behavior near the cutoff frequency:

Q ValueCharacterPeak at Cutoff
0.5Heavily damped, gradual rolloffNo peak
0.707Butterworth (maximally flat passband)No peak
1.0Slight resonanceSmall peak
2.0+Pronounced resonanceNoticeable peak
10.0Near self-oscillationLarge peak

The Butterworth response ($Q = \frac{1}{\sqrt{2}} \approx 0.707$) is called "maximally flat" because it has no ripple in the passband—the flattest possible response before rolloff.

At high Q values, the filter emphasizes frequencies near the cutoff, creating the classic "resonant sweep" sound when the cutoff is modulated.

The TPT/SVF Algorithm

The Topology Preserving Transform discretizes analog filter structures while maintaining their essential characteristics. Unlike naive discretization methods, TPT:

  1. Preserves the analog frequency response shape
  2. Avoids the "cramping" effect near Nyquist
  3. Has no delay-free loops (important for real-time stability)

Coefficient Calculation

The key coefficient $g$ uses the tangent pre-warping formula:

$$ g = \tan\left(\frac{\pi f_c}{f_s}\right) $$

This maps the analog cutoff frequency to the correct digital frequency, compensating for the frequency warping inherent in bilinear transform discretization.

The damping factor $k$ is the inverse of Q:

$$ k = \frac{1}{Q} $$

From these, we compute the filter coefficients:

$$ \begin{aligned} a_1 &= \frac{1}{1 + g(g + k)} \ a_2 &= g \cdot a_1 \ a_3 &= g \cdot a_2 \end{aligned} $$

State Variable Processing

The SVF maintains two state variables, $ic_1$ and $ic_2$, representing the integrator states. For each input sample $v_0$:

$$ \begin{aligned} v_3 &= v_0 - ic_2 \ v_1 &= a_1 \cdot ic_1 + a_2 \cdot v_3 \ v_2 &= ic_2 + a_2 \cdot ic_1 + a_3 \cdot v_3 \end{aligned} $$

The state variables are then updated using the trapezoidal integration rule:

$$ \begin{aligned} ic_1 &\leftarrow 2v_1 - ic_1 \ ic_2 &\leftarrow 2v_2 - ic_2 \end{aligned} $$

The low-pass output is $v_2$.

The elegant aspect of this structure is that it simultaneously computes low-pass, high-pass, and band-pass outputs—though this implementation only uses the low-pass.

Frequency Response

The filter's magnitude response $|H(f)|$ describes how much each frequency is attenuated:

$$ |H(f)| = \frac{1}{\sqrt{(1 - (f/f_c)^2)^2 + (f/(f_c \cdot Q))^2}} $$

Key points:

  • At $f = 0$: $|H| = 1$ (unity gain at DC)
  • At $f = f_c$: $|H| = Q$ (gain equals Q at cutoff)
  • At $f \gg f_c$: $|H| \approx (f_c/f)^2$ (second-order rolloff)

Resonance Peak Compensation

At high Q values, the resonance peak can cause the output to exceed unity gain significantly. This implementation applies a compensation factor to limit the peak while preserving the filter character:

$$ \text{compensation} = \begin{cases} 1.0 & \text{if } Q \leq 1 \ \frac{2}{Q} \cdot \text{blend} & \text{if } Q > 1 \end{cases} $$

This keeps the maximum output level manageable (target peak ≤ 2.0) while maintaining the resonant character.

Creating a Low-Pass Filter

Using the builder:

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

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

// Create with cutoff at 1000 Hz, resonance at 0.707 (Butterworth)
let filter = builder.add(LowPassFilterBlock::new(1000.0, 0.707));
}

Direct construction:

#![allow(unused)]
fn main() {
use bbx_dsp::blocks::LowPassFilterBlock;

let filter = LowPassFilterBlock::<f32>::new(1000.0, 0.707);
}

Port Layout

PortDirectionDescription
0InputAudio input
0OutputFiltered audio

Parameters

ParameterTypeRangeDefault
Cutofff6420-20000 Hz-
Resonancef640.5-10.00.707

Resonance Values

ValueCharacter
0.5Heavily damped
0.707Butterworth (flat)
1.0Slight peak
2.0+Pronounced peak
10.0Near self-oscillation

Usage Examples

Basic Filtering

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

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

let source = builder.add(OscillatorBlock::new(440.0, Waveform::Saw, None));
let filter = builder.add(LowPassFilterBlock::new(2000.0, 0.707));

// Connect oscillator to filter
builder.connect(source, 0, filter, 0);
}

Synthesizer Voice

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

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

// Typical synth voice with envelope-modulated filter
let osc = builder.add(OscillatorBlock::new(440.0, Waveform::Saw, None));
let filter = builder.add(LowPassFilterBlock::new(1000.0, 2.0));  // Resonant
let env = builder.add(EnvelopeBlock::new(0.01, 0.2, 0.5, 0.3));
let amp = builder.add(GainBlock::new(-6.0, None));

// Connect: Osc -> Filter -> Gain
builder.connect(osc, 0, filter, 0);
builder.connect(filter, 0, amp, 0);
}

Filter Sweep with LFO

#![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));  // High resonance
let lfo = builder.add(LfoBlock::new(0.5, 1.0, Waveform::Sine, None));  // Slow sweep

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

Implementation Notes

  • Uses TPT SVF algorithm for numerical stability
  • Multi-channel processing (up to MAX_BLOCK_OUTPUTS channels)
  • Independent state per channel
  • Coefficients recalculated per buffer (supports modulation)
  • Denormals flushed to prevent CPU stalls

Further Reading

  • Zavalishin, V. (2012). The Art of VA Filter Design. Native Instruments. PDF
  • Smith, J.O. (2007). Introduction to Digital Filters. online
  • Pirkle, W. (2019). Designing Audio Effect Plugins in C++, Chapter 11: Filters.
  • Butterworth, S. (1930). "On the Theory of Filter Amplifiers." Experimental Wireless, 7, 536-541.