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:
The highshelf S-plane transfer function is the inverse of the lowshelf transfer function:
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.
#ifndef SK_SHELF_H
#define SK_SHELF_H
#ifndef SKFLT
#define SKFLT float
#endif
#ifdef SK_SHELF_PRIV
<<structs>>
#endif
<<typedefs>>
<<funcdefs>>
#endif
#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.
typedef struct sk_shelf sk_shelf;
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.
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;
};
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.
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);
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.
The implementation below looks a little different than the equation described above. To save on divides, the coefficients have already been pre-divided by .
static SKFLT compute_filter(sk_shelf *shf, SKFLT in);
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
.
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:
Where is defined in terms of the filter's gain in dB units:
The variable is the frequency converted to radians. The variable is a constant typically used to scale things things relative the sampling rate .
The variable is derived from the slope value.
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 cos
is 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.
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
.
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:
More detail can be found in the high shelf section.
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;
}