Artificial reverberation 2

using generalized Fibonacci numbers + Hadamard rotation

Log in to post a comment.

// Artificial reverberation based on the technique shown here:
// http://msp.ucsd.edu/techniques/latest/book-html/node111.html

// TODO Add non-recirculating delays for the early reflections
// TODO Add filters to increase high-frequency damping

input.reverbMix = 0.7; // min=0, max=1, step=0.01
input.reverbDuration = 8; // min=0.1, max=10, step=0.1


class Delayline {
    constructor(n) {
        this.n = n;
        this.p = 0;
        this.data = new Float32Array(n);
    }
    current() {
        return this.data[this.p]; // using lastOut results in 1 sample excess delay
    }
    clock(input) {
        this.data[this.p] = input;
        if(++this.p >= this.n) {
            this.p = 0;
        }
    }
}

const ISQRT2 = Math.sqrt(0.5);
const SQRT2 = Math.sqrt(2);
const SQRT8 = Math.sqrt(8);
const ISQRT8 = 1/SQRT8;
// const fibodelays = [1410,1662,1872,1993,2049,2114,2280,2610]; // X^8 = X+1
const fibodelays = [1467,1691,1932,2138,2286,2567,3141,3897]; // X^8 = X+2
const fiboshort = [465, 537, 617, 698, 742, 802, 963, 1215]; // X^8 = X+2
let meandelay = fibodelays[3] * ditty.dt;

const reverb = filter.def(class {
    constructor(options) {
        this.outgain = 0.3;
        
        this.dls = [];
        this.s0 = new Float32Array(8); // Lowpass filter memory
        for(let i=0; i<8; i++) {
            this.dls.push(new Delayline(fibodelays[i]));
            this.s0[i] = 0;
        }
        this.a0 = clamp01(2*Math.PI*options.cutoff*ditty.dt);
        
        this.dls_nr = []; // Non-recirculating delay lines
        for(let i=0; i<4; i++) {
            this.dls_nr.push(new Delayline(fiboshort[i*2]));
        }
    }
    process(input, options) {
        // First stage: non-recirculating delay lines
        let s0 = input[0], s1 = input[1];
        for(let i=0; i<4; i++) {
            let u0 = 0.6*s0 + 0.8*s1, u1 = 0.8*s0 - 0.6*s1;
            let u1p = this.dls_nr[i].current();
            this.dls_nr[i].clock(u1);
            s0 = u0;
            s1 = u1p;
        }
        let v0 = (s0 + s1);
        let v1 = (s0 - s1);
        //let s0 = -0.6*input[0] + 0.8*input[1];
        //let s1 = 0.8*input[0] + 0.6*input[1];
        
        // Second stage: recirculating delay lines
        let rt60 = options.rt60;
        let loopgain = 10 ** (-3*meandelay / rt60) * ISQRT8;
        let higain = 10 ** (-3*meandelay / options.rtHi) * ISQRT8;
        let v = this.dls.map( (dl) => dl.current());
        // Fast Walsh-Hadamard transform
        // https://formulasearchengine.com/wiki/Fast_Walsh%E2%80%93Hadamard_transform
        let w = [v[0]+v[4], v[1]+v[5], v[2]+v[6], v[3]+v[7],
                 v[0]-v[4], v[1]-v[5], v[2]-v[6], v[3]-v[7]];
        let x = [w[0]+w[2], w[1]+w[3], w[0]-w[2], w[1]-w[3],
                 w[4]+w[6], w[5]+w[7], w[4]-w[6], w[5]-w[7]];
        let y = [x[0]+x[1], x[0]-x[1], x[2]+x[3], x[2]-x[3],
                 x[4]+x[5], x[4]-x[5], x[6]+x[7], x[6]-x[7]];
        y[0] += v0;
        y[2] += v1;
        for(let i=0; i<8; i++) {
            let hipass = y[i] - this.s0[i];
            this.dls[i].clock(this.s0[i] * loopgain + hipass * higain); // Mix lowpass and hipass in different amounts
            this.s0[i] += this.a0 * hipass;
        }
        return [lerp(input[0], v[0], options.mix),
                lerp(input[1], v[2], options.mix)];
    }
}, {mix:0.2, rt60:1, cutoff:5000, rtHi:0.8});

const impulse = synth.def( (phase, env, tick, options) => (tick <= 1.5 * ditty.dt) ? 1.0 : 0.0);
const noise = synth.def( (phase, env) => (Math.random() - 0.5)*env.value, {env:adsr, attack:0.01, release:0.1, amp:0.1});

ditty.bpm = 60;

// Impulse response

/*
loop( () => {
    impulse.play();
    sleep(1);
}).connect(reverb.filter({mix:() => input.reverbMix, rt60:() => input.reverbDuration})); 
*/


loop( () => {
    noise.play(c, {pan:Math.random()*2-1});
    sleep(1);
}, {name:"noise"}).connect(reverb.create({mix:() => input.reverbMix, rt60:() => input.reverbDuration})); 

// Not quite a sine wave
const osc = synth.def((phase, env) => {
    let x = (phase % 1);
    return 20*x*(1-x)*(x-0.5) * env.value;
});

const mynotes = scale(e4, scales["aeolian"]);
loop( () => {
    let nn = mynotes.choose() + (12 * Math.floor(Math.random()*2 - 0.2));
    let drop = 2*Math.random();
    osc.play((tick) => nn + drop / (50*tick+1), {atk:0.01, rel:0.1, pan:Math.random()*2-1, amp:0.3});
    sleep(Math.random() * 0.5);
}, {name:"drops"}).connect(reverb.create({mix:() => input.reverbMix, rt60:() => input.reverbDuration}));