valp1

valp1

Overview

valp1 implements a one-pole virtual-analog lowpass filter, based on the implementation defined in "The Art of VA Filter Design" by Vadim Zavalishin (DSP engineer at Native Instruments and creator of Reaktor). This particular filter id discretized using the topology-preserving bilinear transform, abbreviated as TPBLT or TPT.

The scope of this document mostly aims to talk about the direct implementation, rather than the steps leading up to it. Those missing steps and mathematical notation are very important for actually grokking how this filter works.

Think of this C implementation as the corpse a fallen Gazelle in the desert, picked clean to the bone so nothing is left but a handful of arithmetic and trig operations. It's really hard to reconstruct and understand this filter using the C code alone.

The full derivation of this filter is available in chapter 3 ("Time-discretization"). of Zavalishin's book, which as of writing, is available as a PDF from Native Instruments.

Tangled Files

As per usual, a single C and Header file is provided called valp1.c and valp1.h. Defining SK_VALP1_PRIV will expose the core struct used in this algorithm.

<<valp1.c>>=
#include <math.h>
#define SK_VALP1_PRIV
#include "valp1.h"

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

<<funcs>>

<<valp1.h>>=
#ifndef SK_VALP1_H
#define SK_VALP1_H

#ifndef SKFLT
#define SKFLT float
#endif

<<typedefs>>
<<funcdefs>>

#ifdef SK_VALP1_PRIV
<<structs>>
#endif

#endif

Struct

Called sk_valp1.

<<typedefs>>=
typedef struct sk_valp1 sk_valp1;

<<structs>>=
struct sk_valp1 {
    <<sk_valp1>>
};

Cutoff Frequency

The only parameter is the cutoff frequency. It is set with sk_valp1_freq.

<<funcdefs>>=
void sk_valp1_freq(sk_valp1 *lp, SKFLT freq);

<<funcs>>=
void sk_valp1_freq(sk_valp1 *lp, SKFLT freq)
{
    lp->freq = freq;
}

Caching is used so coefficients need not be re-calculated every sample.

<<sk_valp1>>=
SKFLT freq;
SKFLT pfreq;

<<init>>=
sk_valp1_freq(lp, 1000);
lp->pfreq = -1;

Filter Variables

Filter Memory

Filter memory is stored in a value called s, which is the same variable name used in the textbook implementation.

<<sk_valp1>>=
SKFLT s;

<<init>>=
lp->s = 0;

Gain coefficient

The gain coefficent G is a cached value used to compute the filter. It gets updated every time the frequency changes.

<<sk_valp1>>=
SKFLT G;

<<init>>=
lp->G = 0;

Big T (1 / sr)

T, otherwise known as "big T", is the sampling rate converted to seconds, or 1/sr. It is needed in order to compute the gain coefficient G.

<<sk_valp1>>=
SKFLT T;

<<init>>=
lp->T = 1.0 / (SKFLT)sr;

Initialization

Done with sk_valp1_init. Sampling rate is all that is needed.

<<funcdefs>>=
void sk_valp1_init(sk_valp1 *lp, int sr);

<<funcs>>=
void sk_valp1_init(sk_valp1 *lp, int sr)
{
    <<init>>
}

Computation

A single sample is computed with sk_valp1_tick. The computation itself only requires a few short lines of very simple C code. However, the steps required to get it to this point were not as simple a matter. Often this is the case for filter implementations. By the time a filter design reaches C code, all you are left with is a handful of arithmetic and trig operations.

<<funcdefs>>=
SKFLT sk_valp1_tick(sk_valp1 *lp, SKFLT in);

<<funcs>>=
SKFLT sk_valp1_tick(sk_valp1 *lp, SKFLT in)
{
    SKFLT out;
    SKFLT v;
    out = 0;
    <<update_coefficient>>
    <<compute>>
    return out;
}

In the chapter, Zavalishin does a wonderful job showing how take the filter topology of a analog 1-pole lowpass filter and faithfully digitize it in a delay-free way using the bilinear transform. This approach, which Zavalishin calls TPT, differs from the more traditional direct form approach, which involves taking a transfer function for an analogue filter in the s-plane, then plugging-and-chugging in the BLT to convert it to a transfer function in the discrete time (digital) z-plane.

After all the song and dance about things like time discretization methods and zero-delay feedback loops, the final equation looks like this:

$$ y = v + s $$

Where $y$ is the filter output, $v$ can be considered to be the estimated output of $y$, and $s$ is the feedback. This will be returned to in a moment.

Before computing the filter equation, the coefficient G must be updated if the frequency has been updated.

G is computed as g/(1 + g). Little g is the gain amount.


where g is the gain amount $\omega_a T \over 2$, where $\omega_a$ is the prewarped filter cutoff frequency, in units radians/second. To get this value, first the cutoff frequency is multiplied by 2pi to convert it to units of radians per second, which will be called $\omega_c$, or wc in C. This then gets put through a transformation:

$$ \omegac T \over 2) $$

This sort of operation is very common when using the BLT in filter design, and it is known prewarping.

Basically, the BLT is a process for getting analogue filters digitized, but it doesn't come for free. The behavior of the cutoff frequency in the filter gets skewed a bit. This is known as frequency warping. The prewarping controls the warp in such a way that the cutoff frequency has a perfect mapping from the analog space, leaving everything around it to warp.

<<update_coefficient>>=
if (lp->pfreq != lp->freq) {
    SKFLT wc;
    SKFLT wa;
    SKFLT g;

    wc = 2.0 * M_PI * lp->freq;
    wa = (2.0/lp->T) * tan(wc * lp->T * 0.5);
    g = wa * lp->T * 0.5;
    lp->G = g / (1.0 + g);

    lp->pfreq = lp->freq;
}

Next comes computation.

The $v$, or predicted part of the equation is computed and stored in a variable called v as (x - s) * G, where x is the input signal, s is the filter memory state, and G is the computed scaling parameter used in the BLT.

The final filter output y can be computed as v + s.

The filter memory state s is updated to be y + v.

<<compute>>=
v = (in - lp->s) * lp->G;
out = v + lp->s;
lp->s = out + v;