A State Variable Filter in Twenty Lines

If you need a filter for a synthesiser, a biquad is a reasonable first choice — until you modulate it. Biquads are designed around fixed coefficients, and sweeping the cutoff quickly with an envelope or LFO can cause zipper noise, clicks, or worse.

The state variable filter, discretised with trapezoidal integration (often called the TPT or “zero-delay feedback” SVF), handles modulation gracefully. It stays stable at any cutoff below Nyquist, and it produces low-pass, band-pass and high-pass outputs from the same two state variables.

The code

typedef struct {
    float ic1eq, ic2eq;     // integrator state
    float a1, a2, a3, k;    // coefficients
} SVF;

void svf_set(SVF *f, float cutoff, float q, float sampleRate) {
    float g = tanf((float)M_PI * cutoff / sampleRate);
    f->k  = 1.0f / q;
    f->a1 = 1.0f / (1.0f + g * (g + f->k));
    f->a2 = g * f->a1;
    f->a3 = g * f->a2;
}

void svf_process(SVF *f, float in, float *low, float *band, float *high) {
    float v3 = in - f->ic2eq;
    float v1 = f->a1 * f->ic1eq + f->a2 * v3;
    float v2 = f->ic2eq + f->a2 * f->ic1eq + f->a3 * v3;
    f->ic1eq = 2.0f * v1 - f->ic1eq;
    f->ic2eq = 2.0f * v2 - f->ic2eq;
    *low  = v2;
    *band = v1;
    *high = in - f->k * v1 - v2;
}

The derivation is in Andrew Simper’s technical papers on the Cytomic website, and in Vadim Zavalishin’s book The Art of VA Filter Design. Both are free and excellent.

Notes

  • tanf is the expensive part. If the cutoff changes every sample, a polynomial approximation is usually good enough.
  • A q of 0.707 gives a Butterworth response. Higher values add resonance.
  • Notch and peak responses are just combinations of the outputs: low + high and low - high.