Practical Considerations

Speed is critical in real-time applications, yet in some code I have seen around the web, there are obvious optimizations that were missed:

  • Reduce divisions by calculating the reciprocal once. For example, if you need to work out \(a/d\) and \(b/d\), it is better to calculate \(1/d\) first and then multiply \(a\) and \(b\) by this quantity because divisions are typically much slower than multiplications. Another obvious example is computing \(1/d^2\) from \(1/d\): square it, rather than dividing by \(d\). Even though a smart compiler might be able to do this, it is better to rely on yourself.

  • If you’re using math libraries to do trigonometry (rather than lookup tables), it may be worth rewriting equations that minimize the number of calls to the math libraries. For example, following the equations for the cookbook LPF directly will mean that both \(\sin\) and \(\cos\) must be computed. However, if an alternative derivation is used, then only \(\tan\) is needed. It’s not clear to me why formulas are given in a relatively inefficient form.

  • Common subexpressions should only be computed once. For example, in the Cookbook’s LPF, it’s faster to compute the \(b_i\)'s (which by the way correspond to the \(a_i\)'s in our notation) by working out \(b_0\) first, and then setting \(b_1 = 2 b_0\), \(b_2 = b_0\).

  • Some use the bilinear transform \(k \frac{z-1}{z+1}\) for some constant \(k\). This is pointless because \(k\) gets cancelled out by the prewarping. The explanation in the Cookbook does not suffer from this problem: there, the prewarping is viewed as part of the bilinear map.

  • When coding a specific 2-pole lowpass or highpass filter, a few lookups and multiplications can be avoided by leaving the numerator of the transfer function \(H(z)\) as \(z^2 + 2 z + 1\) and hardcoding this expression.

    For the cookbook LPF the filter takes the form

    \[ y[n] = a(x[n] + 2 x[n-1] + x[n-2] - b y[n-1] - c y[n-2]) \]

    where \(f_s\) is the sampling frequency, \(f\) is the cutoff frequency, and

    \[ \array { t &=& 1 / tan(\pi f / f_s) \\ a &=& 1 / (t^2 + t/q + 1) \\ b &=& 2 - 2 t^2 \\ c &=& t^2 - t/q + 1 \\ } \]

    (Of course \(t^2\) and \(t/q\) need only be computed once.)

    The cookbook HPF is similar:

    \[ y[n] = a(x[n] - 2 x[n-1] + x[n-2] - b y[n-1] - c y[n-2]) \]

    with

    \[ \array { t &=& tan(\pi f / f_s) \\ a &=& 1 / (t^2 + t/q + 1) \\ b &=& 2 - 2 t^2 \\ c &=& t^2 - t/q + 1 \\ } \]

    Cookbook bandpass:

    \[ y[n] = a(b(x[n] - x[n-2]) - c y[n-1] - d y[n-2]) \]

    where

    \[ \array { \omega &=& 2 \pi f / f_s \\ e &=& 0.5 W \omega / sin(\omega) \\ B &=& 2^e - 2^{-e} \\ t &=& tan(\omega / 2) \\ a &=& 1 / (1 + t^2 + B t) \\ b &=& B t \\ c &=& 2 t^2 - 2 \\ d &=& 1 + t^2 - B t \\ } \]

    \(W\) is the bandwidth in octaves. Note the value given to \(B\) is an approximation. For the bandpass filter with peak gain \(Q\), leave \(b\) out (i.e. \(b=1\)). The cookbook formulas may be better since we must compute \(sin(\omega)\) anyway.


Ben Lynn blynn@cs.stanford.edu 💡