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
tanfis the expensive part. If the cutoff changes every sample, a polynomial approximation is usually good enough.- A
qof 0.707 gives a Butterworth response. Higher values add resonance. - Notch and peak responses are just combinations of the outputs:
low + highandlow - high.