apulSoft Blog

Feb 24th, 2021 - dsp math filters lessonslearned

Stable Biquad Modulation

A biquad is a structure to calculate recursive filters (so-called IIR filters). For digital biquad filtering it is common practice to apply the following steps to each sample:

$1.\ y_n = b_0 x_n + d_1$
$2.\ d_1 = b_1 x_n - a_1 y_n + d_2$
$3.\ d_2 = b_2 x_n - a_2 y_n$

$x_n$ is the input sample n, $y_n$ the output sample n, $a_(0,1,2)$ and $b_(1,2)$ are the normalized biquad coefficients (more about these on another day) and $d_(1,2)$ are the "state" variables of the biquad or its accumulation registers. They contain some history of the past samples and the coefficients. This is called direct-form II evaluation of a digital biquad filter.

Here are some examples of direct-form II implementations:

JUCE Library DSP module
Ear Level Engineering Blog
JOS@CCRMA Stanford

When I created apQualizr version 1 and 2, I initially used similar code because it is the recommended way to do this, the most efficient form requiring the least amount of operations and memory.

apQualizr2 added a lot of modulation possibilities to the mix and when testing out the modulation system, I found out my filters tend to explode under fast modulation.

In general, when biquad bands increased their frequency, levels would shoot up and go crazy if the increase was fast. At the time I ended up limiting the modulation change speed to battle this problem.

I later realized that the issues come from the $d_(1,2)$ state. The problem is that these are results of recursive multiplications with the coefficients, and they are sort of unitless and can sometimes get huge or tiny. If the coefficients change, they need time to stabilize again. I tried to scale them during modulation with limited success, it is just unclear what they stand for as values.

Breakthrough came from going back to the basics. For some reason it is called direct-form II and at one point I wondered about form I:

$1.\ y_n = b_0 x_n + b_1 x_(n-1) + b_2 x_(n-2) - a_1 y_(n-1) - a_2 y_(n-2)$
$2.\ x_(n-2) = x_(n-1)$
$3.\ y_(n-2) = y_(n-1)$
$4.\ x_(n-1) = x_n$
$5.\ y_(n-1) = y_n$

Instead of the "baked" $d_(1,2)$, this has past input $x_(n-1,n-2)$ and output $y_(n-1,n-2)$ values as its state. It needs four state values and some shifting of these on every step, but the huge advantage is that the stored values by definition always have a reasonable size similar to current input and output. This is why changing the coefficients under modulation works when using direct-form I. The additional cost is manageable, it's the same number of math operations and some more memory shifting, but the whole thing works nicely using SIMD (SSE/NEON) instructions and the state can easily be kept in registers, but that will be another blog entry in the future.

Warp-up: If you want modulated biquads, use direct-form I and not direct-form II.