Audio EQ Filters

Adapting the EQ filters from the Audio EQ Cookbook, to be pasted and reused in other projects.

Log in to post a comment.

input.type = 0; // min=0, max=8, step=1 (LPF, HPF, resonance, BPF, notch, APF, peak (use dBgain), lowShelf (use dBgain), highShelf (use dBgain))
input.f0 = 2000; // min=20, max=20000, step=1
input.Q = 3; // min=0.1, max=15, step=0.01
input.dBgain = 6; // min=-12, max=12, step=0.1

// ========== Audio EQ filters ==========

const cos = Math.cos, sin = Math.sin, PI = Math.PI, sqrt = Math.sqrt;
// Second-order section in Transposed Direct Form II
// https://ccrma.stanford.edu/~jos/filters/Transposed_Direct_Forms.html
class TDF2 {
    constructor(a1,a2,b0,b1,b2) {
        this.set_coefs(a1,a2,b0,b1,b2);
        this.s1 = 0; this.s2 = 0;
    }
    set_coefs(a1,a2,b0,b1,b2){
        this.a1 = a1; this.a2 = a2; this.b0 = b0; this.b1 = b1; this.b2 = b2;
    }
    process(xn) {
        var yn  = this.s1                + this.b0 * xn;
        this.s1 = this.s2 - this.a1 * yn + this.b1 * xn;
        this.s2 =         - this.a2 * yn + this.b2 * xn;
        return yn;
    }
}

const biquad_types = ["LPF", "HPF", "resonance", "BPF", "notch", "APF", "peak", "lowShelf", "highShelf"];

// Biquad filter coefficients from the Audio EQ Cookbook
// https://www.w3.org/TR/audio-eq-cookbook/
function calc_biquad_coefs(opt) {
    var w0 = 2*PI*opt.f0*ditty.dt; var cw0 = cos(w0); var alpha = sin(w0)/(2*opt.Q);
    if(opt.type == "LPF") { // Low pass filter
        var b0 =  (1 - cw0)/2, b1 =   1 - cw0, b2 =  (1 - cw0)/2, a0 =   1 + alpha, a1 =  -2*cw0, a2 =   1 - alpha;
    } else if (opt.type == "HPF") { // High pass filter
        var b0 =  (1 + cw0)/2, b1 =  -1 - cw0, b2 =  (1 + cw0)/2, a0 =   1 + alpha, a1 =  -2*cw0, a2 =   1 - alpha;
    } else if(opt.type == "resonance") { // Band pass filter (constant skirt gain, peak gain = Q)
        var b0 =  opt.Q*alpha, b1 =  0, b2 = -opt.Q*alpha, a0 =   1 + alpha, a1 =  -2*cw0, a2 =   1 - alpha;
    } else if(opt.type == "BPF") { // Band pass filter (constant 0 dB peak gain)
        var b0 = alpha, b1 =  0, b2 = -alpha, a0 =   1 + alpha, a1 =  -2*cw0, a2 = 1 - alpha;
    } else if(opt.type == "notch") { // Notch filter (removes frequency f0)
        var b0 = 1, b1 = -2*cw0, b2 = 1, a0 =   1 + alpha, a1 =  -2*cw0, a2 = 1 - alpha;
    } else if(opt.type == "APF") { // All pass filter (constant gain, phase shift around f0)
        var b0 = 1 - alpha, b1 = -2*cw0, b2 = 1 + alpha, a0 =   1 + alpha, a1 =  -2*cw0, a2 = 1 - alpha;
    } else if(opt.type == "peak") { // Peaking EQ (requires dBgain)
        var A = 10**(opt.dBgain/40);
        var b0 = 1 + alpha*A, b1 = -2*cw0, b2 = 1 - alpha*A, a0 =   1 + alpha/A, a1 =  -2*cw0, a2 = 1 - alpha/A;
    } else if(opt.type == "lowShelf") { // Low shelf (requires dBgain)
        var A = 10**(opt.dBgain/40);
        var b0 = A*( (A+1) - (A-1)*cw0 + 2*sqrt(A)*alpha ), b1 =  2*A*( (A-1) - (A+1)*cw0), b2 = A*( (A+1) - (A-1)*cw0 - 2*sqrt(A)*alpha ),
            a0 = (A+1) + (A-1)*cos(w0) + 2*sqrt(A)*alpha,   a1 =   -2*( (A-1) + (A+1)*cw0), a2 = (A+1) + (A-1)*cos(w0) - 2*sqrt(A)*alpha;
    } else if(opt.type == "highShelf") { // High shelf (requires dBgain)
        var A = 10**(opt.dBgain/40);
        var b0 =    A*( (A+1) + (A-1)*cos(w0) + 2*sqrt(A)*alpha ),
            b1 = -2*A*( (A-1) + (A+1)*cos(w0)                   ),
            b2 =    A*( (A+1) + (A-1)*cos(w0) - 2*sqrt(A)*alpha ),
            a0 =        (A+1) - (A-1)*cos(w0) + 2*sqrt(A)*alpha,
            a1 =    2*( (A-1) - (A+1)*cos(w0)                   ),
            a2 =        (A+1) - (A-1)*cos(w0) - 2*sqrt(A)*alpha;
    } else {
        debug.error("Unknown filter type");
    }
    return [a1/a0, a2/a0, b0/a0, b1/a0, b2/a0];
}

class BiquadFilter {
    constructor(opt) {
        var c = calc_biquad_coefs(opt);
        this.ops = [new TDF2(...c), new TDF2(...c)];
    }
    process(x, opt) {
        return [this.ops[0].process(x[0]),this.ops[1].process(x[1])];
    }
}
// Create a Dittytoy-compatible filter.
// Only for filters with constant coefficients!
const biquad = filter.def(BiquadFilter);

class BiquadFilterVar {
    constructor(opt) {
        var c = calc_biquad_coefs(opt);
        this.ops = [new TDF2(...c), new TDF2(...c)];
    }
    process(x, opt) {
        var c = calc_biquad_coefs(opt);
        this.ops[0].set_coefs(...c);
        this.ops[1].set_coefs(...c);
        return [this.ops[0].process(x[0]),this.ops[1].process(x[1])];
    }
}
// Use this only if you want to change filter type, f0, Q, or dBgain frequently (heavier computations!)
const biquadVar = filter.def(BiquadFilterVar);

const noise = synth.def( (phase, env) => (Math.random()*2-1) * env.value * ((phase%1) < 0.5 ? 1 : 0), {env:one, duration:1000, amp:0.1});
loop( (i) => {
    noise.play(hz_to_midi(50));
    sleep(1000);
})
.connect(biquad.create({type:'LPF', f0:8000, Q:0.5}))
//.connect(biquad.create({type:'LPF', f0:2500, Q:10}))
//.connect(biquad.create({type:'lowShelf', f0:1000, Q:1, dBgain:-5}))
.connect(biquadVar.create({type: () => biquad_types[input.type], f0: () => input.f0, Q: () => input.Q, dBgain: () => input.dBgain}));