Shelf

Shelf

Overview

shelf is an implementation of the two-pole shelf filters described in the Audio EQ Cookbook by Robert Bristow-Johnson.

The implementations include both high and low shelf, and have the following parametric controls: transition frequency, boost amount (in dB units), and slope (a value between 0 and 1).

S-plane Transfer Functions

Below are the S-plane transfer functions for the high shelf and low shelf. A full explanation of transfer functions beyond the scope of this document. Think of these as a sort of blueprint. Filters that are designed in the S-plane are kind of like analogue filters. In order for them to be implemented in code, they must first be "digitized", which means transferring the filter from the S-plane to the z-plane. The most common way this is done is using something called the Bilinear Transform (or BLT), which is essentially a mathematical subsitution done on the S-plane transfer function. In the Audio EQ cookbook, this process has already been done and the coefficients have been pre-derived.

This is the lowshelf transfer function in the S-plane:

H(s) = A \Bigl({s^2 + ({\sqrt{A} / Q})s + A \over
As^2 + ({\sqrt{A}/Q})s + 1}\Bigr)

The highshelf S-plane transfer function is the inverse of the lowshelf transfer function:

H(s) =A \Bigl({A s^2 + (\sqrt{A}/Q)s + 1
\over
s^2 + (\sqrt{A}/Q)s + A}\Bigr)

These are then put through the bilinear transform to derive digital filter coefficients.

Tangled Files

shelf.c and shelf.h are the files created. If SK_SHELF_PRIV is defined, the private struct is exposed.

<<shelf.h>>=
#ifndef SK_SHELF_H
#define SK_SHELF_H

#ifndef SKFLT
#define SKFLT float
#endif

#ifdef SK_SHELF_PRIV
<<structs>>
#endif

<<typedefs>>
<<funcdefs>>
#endif

<<shelf.c>>=
#include <math.h>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

#define SK_SHELF_PRIV
#include "shelf.h"
<<static_funcdefs>>
<<funcs>>

Struct

sk_shelf is the struct that can handle both the high shelf and the low shelf. It can be initialized with sk_shelf_init. The sampling rate must be specified.

<<typedefs>>=
typedef struct sk_shelf sk_shelf;

<<funcdefs>>=
void sk_shelf_init(sk_shelf *shf, int sr);

A shelf filter is a kind of biquad, which a generalized form for a 2-pole filter. A biquad has filter memory for previous inputs and outputs, as well as coefficients for these as well. The values of the coefficients determine whether or not the filter will behave as a high or low shelf filter.

<<structs>>=
struct sk_shelf {
    int sr;
    SKFLT a[2];
    SKFLT b[3];
    SKFLT x[2];
    SKFLT y[2];
    SKFLT gain;
    SKFLT freq;
    SKFLT slope;
    int changed;
    SKFLT T;
};

<<funcs>>=
void sk_shelf_init(sk_shelf *shf, int sr)
{
    int i;

    shf->T = 1.0 / sr;

    for (i = 0; i < 2; i++) {
        shf->x[i] = 0;
        shf->y[i] = 0;
        shf->a[i] = 0;
        shf->b[i] = 0;
    }

    shf->b[2] = 0;

    sk_shelf_frequency(shf, 1000);
    sk_shelf_gain(shf, 6);
    sk_shelf_slope(shf, 1.0);
}

Parameters

Parameters include cutoff frequency, gain, and slope. Setting any of these will cause the changed flag to be set.

<<funcdefs>>=
void sk_shelf_frequency(sk_shelf *shf, SKFLT freq);
void sk_shelf_gain(sk_shelf *shf, SKFLT gain);
void sk_shelf_slope(sk_shelf *shf, SKFLT slope);

<<funcs>>=
void sk_shelf_frequency(sk_shelf *shf, SKFLT freq)
{

    if (freq != shf->freq) {
        shf->freq = freq;
        shf->changed = 1;
    }
}

void sk_shelf_gain(sk_shelf *shf, SKFLT gain)
{
    if (gain != shf->gain) {
        shf->gain = gain;
        shf->changed = 1;
    }
}

void sk_shelf_slope(sk_shelf *shf, SKFLT slope)
{
    if (slope != shf->slope && slope > 0) {
        shf->slope = slope;
        shf->changed = 1;
    }
}

Computing The Filter

Both shelving filters are biquads, which means they can be computed the same way.

Computation of the filter is derived from the difference equation, and is known as Direct Form 1.

\eqalign{y[n] = (b_0/a_0)x[n]& + (b_1/a_0)x[n-1] + (b_2/a_0)x[n - 2] \cr
\hfil & - (a_1/a_0)y[n - 1] - (a_2/a_0)y[n - 2]\cr
}

The implementation below looks a little different than the equation described above. To save on divides, the coefficients have already been pre-divided by a0 .

<<static_funcdefs>>=
static SKFLT compute_filter(sk_shelf *shf, SKFLT in);

<<funcs>>=
static SKFLT compute_filter(sk_shelf *shf, SKFLT in)
{
    SKFLT out;
    SKFLT *b, *a, *x, *y;

    out = 0;

    b = shf->b;
    a = shf->a;
    x = shf->x;
    y = shf->y;

    out =
        b[0]*in + b[1]*x[0] + b[2]*x[1]
        - a[0]*y[0] - a[1]*y[1];

    y[1] = y[0];
    y[0] = out;

    x[1] = x[0];
    x[0] = in;

    return out;
}

High Shelf

Filter with the high shelf filter with sk_shelf_high_tick.

<<funcdefs>>=
SKFLT sk_shelf_high_tick(sk_shelf *shf, SKFLT in);

Before computing a sample, the frequency/gain values are checked to see if they have been changed, and if so, are updated.

The coefficients are the following:

\eqalign{
b_0 &= A \Bigl((A + 1) + (A-1)\cos(\omega_0) + 2\sqrt(A)\alpha\Bigr)\cr
b_1 &= -2 A ((A-1) + (A+1)\cos(\omega_0))\cr
b_2 &= A ((A+1) + (A-1)\cos(\omega_0) - 2\sqrt(A)\alpha)\cr
a_0 &= (A + 1) - (A-1)\cos(\omega_0) + 2\sqrt(A)\alpha\cr
a_1 &= 2((A-1) - (A+1)\cos(\omega_0))\cr
a_2 &= (A+1) - (A-1)\cos(\omega_0) - 2\sqrt(A)\alpha\cr
}

Where A is defined in terms of the filter's gain g in dB units:

\eqalign{
A &= \sqrt{10^{g/20}} \cr
&= 10^{g/40}
}

The variable \omega_0 is the frequency f converted to radians. The variable T is a constant typically used to scale things things relative the sampling rate F_s .

\eqalign{
\omega_0 &= 2 \pi f \cr
\omega_0 T &= {{2 \pi f} \over F_s } \cr
}

The variable \alpha is derived from the slope value.

\alpha = {\sin(\omega_0) \over 2}
\sqrt{\Bigl(A + {1 \over A}\Bigr)\Bigl({1 \over S} - 1\Bigr) + 2}

Some additional operations are done in the name of optimization. To save a on a few division operations in the difference equation, the coefficients are divided by a0 ahead of time. The trig function cosis quite costly, so the constant co is used to store cos(omegaT). sqrt is also reasonably expensive, so the expression sqrt(A)*alpha*2.0 is saved to an arbitrarily named variable k.

After coefficients are updated (if they needed to be), the filter can then be computed.

<<funcs>>=
SKFLT sk_shelf_high_tick(sk_shelf *shf, SKFLT in)
{
    SKFLT out;

    out = 0;

    if (shf->changed) {
        SKFLT ia0;
        SKFLT alpha;
        SKFLT A;
        SKFLT k; /* sqrt(A)*alpha*2.0 */
        SKFLT omegaT;
        SKFLT *a, *b;
        SKFLT co; /* cos(omegaT) */

        A = pow(10.0, shf->gain / 40.0);
        omegaT = 2.0 * M_PI * shf->freq * shf->T;
        alpha = sin(omegaT) * 0.5 *
        sqrt((A + (1.0/A))*((1.0/shf->slope) - 1.0) + 2.0);
        co = cos(omegaT);
        k = sqrt(A)*alpha*2.0;

        a = shf->a;
        b = shf->b;

        ia0 = (A+1.0) - (A-1.0)*co + k;

        if (ia0 != 0) ia0 = 1.0 / ia0;
        else ia0 = 0;


        b[0] = A * ((A+1.0) + (A-1.0)*co + k);
        b[0] *= ia0;
        b[1] = -2.0*A*((A-1.0) + (A+1.0)*co);
        b[1] *= ia0;
        b[2] = A*((A+1.0) + (A-1.0)*co - k);
        b[2] *= ia0;

        a[0] = 2.0*((A-1.0) - (A+1.0)*co);
        a[0] *= ia0;
        a[1] = (A+1.0) - (A-1.0)*co - k;
        a[1] *= ia0;

        shf->changed = 0;
    }


    out = compute_filter(shf, in);
    return out;
}

Low Shelf

Filter with the low shelf filter with sk_shelf_low_tick.

<<funcdefs>>=
SKFLT sk_shelf_low_tick(sk_shelf *shf, SKFLT in);

Similar to high shelf. Updates the coefficients if needed, then computes the filter sample.

The coefficients are the following:

\eqalign{
b_0 &= A \Bigl((A + 1) - (A-1)\cos(\omega_0) + 2\sqrt{A}\alpha\Bigr)\cr
b_1 &= 2A \Bigl((A - 1) - (A + 1)\cos(\omega_0)\Bigr)\cr
b_2 &= A \Bigl((A + 1) - (A - 1)\cos(\omega_0) - 2\sqrt{A}\alpha\Bigr)\cr
a_0 &= (A + 1) + (A-1)\cos(\omega_0) + 2\sqrt{A}\alpha \cr
a_1 &= -2\Bigl((A - 1) + (A + 1)\cos(\omega_0)\Bigr)\cr
a_2 &= (A + 1) + (A - 1)\cos(\omega_0) - 2\sqrt{A}\alpha\cr
}

More detail can be found in the high shelf section.

<<funcs>>=
SKFLT sk_shelf_low_tick(sk_shelf *shf, SKFLT in)
{
    SKFLT out;

    out = 0;

    if (shf->changed) {
        SKFLT ia0;
        SKFLT alpha;
        SKFLT A;
        SKFLT k; /* sqrt(A)*alpha*2.0 */
        SKFLT omegaT;
        SKFLT *a, *b;
        SKFLT co; /* cos(omegaT) */

        A = pow(10.0, shf->gain / 40.0);
        omegaT = 2.0 * M_PI * shf->freq * shf->T;
        alpha = sin(omegaT) * 0.5 *
        sqrt((A + (1.0/A))*((1.0/shf->slope) - 1.0) + 2.0);
        co = cos(omegaT);
        k = sqrt(A)*alpha*2.0;


        a = shf->a;
        b = shf->b;

        ia0 = (A+1.0) + (A-1.0)*co + k;

        if (ia0 != 0) ia0 = 1.0 / ia0;
        else ia0 = 0;

        b[0] = A * ((A+1.0) - (A-1.0)*co + k);
        b[0] *= ia0;
        b[1] = 2.0*A*((A-1.0) - (A+1.0)*co);
        b[1] *= ia0;
        b[2] = A*((A+1.0) - (A-1.0)*co - k);
        b[2] *= ia0;

        a[0] = -2.0*((A-1.0) + (A+1.0)*co);
        a[0] *= ia0;
        a[1] = (A+1.0) + (A-1.0)*co - k;
        a[1] *= ia0;

        shf->changed = 0;
    }

    out = compute_filter(shf, in);
    return out;
}