Fauré with reverb

Combining my reverb filter with my flute synth

Log in to post a comment.

const ISQRT2 = Math.sqrt(0.5);
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
let meandelay = fibodelays[3] * ditty.dt;

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 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;
        }
    }
    process(input, options) {
        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] += input[0]*SQRT8;
        y[2] += input[1]*SQRT8;
        let a0 = clamp01(2*Math.PI*options.cutoff*ditty.dt);
        for(let i=0; i<8; i++) {
            let hipass = y[i] - this.s0[i];
            this.dls[i].clock(this.s0[i] * loopgain + hipass * higain);
            this.s0[i] += a0 * hipass;
        }
        return [lerp(input[0], v[0], options.mix),
                lerp(input[1], v[1], options.mix)];
    }
}, {mix:0.2, rt60:1, cutoff:5000, rtHi:0.8});

input.reverbMix = 0.8; // min=0, max=1, step=0.01
input.reverbTime = 3.0; // min=0.1, max=10, step=0.1
input.reverbTimeHi = 1.3; // min=0.1, max=10, step=0.1
input.reverbCutoff = 4000; // min=200, max=10000, step=10

const hallverb = reverb.createShared({
    mix:() => input.reverbMix,
    rt60:() => input.reverbTime, 
    cutoff:() => input.reverbCutoff, 
    rtHi:() => input.reverbTimeHi
});



// A very simplified "physical modeling" flute using a single delay line, a nonlinearity, a wide bandpass filter, and noise.

// Basically:
//
// [ noise ] ---> (+) ---> [ nonlinearity ] ---> [ bandpass filter ] ---> output
//                 ^                                                  |
//                 |                                                  |
//                 |------------------ [ delay line ] <----------------
//
// This model can do flute-like attacks quite nicely.
// However it can be quite out of tune due to the filters.
// One way to compensate for it is to let the filters follow the note as well (but that's cheating!)
// It is unable to perform transitions between notes, and
// does not account for the jump to higher harmonics when increasing blowing pressure.

// Make sure to play with the sliders!

// Flute

input.fluteGain = 1.23; // min=1, max=1.5, step=0.01
input.fluteOffset = 0.16; // min=0, max=0.5, step=0.01
input.fluteNoiseAmount = 0.05; // min=0.005, max=0.5, step=0.005


const saturate = (x) => x / Math.sqrt(1 + x*x);

// Adapted from https://forums.codeguru.com/showthread.php?473996-How-to-do-cubic-interpolation-with-an-audio-sample
const cubic_interpolate = (y0, y1, y2, y3, mu ) => {
   let mu2 = mu*mu;
   let a0 = y3 - y2 - y0 + y1; //p
   let a1 = y0 - y1 - a0;
   let a2 = y2 - y0;
   let a3 = y1;
   return ( a0*mu*mu2 + a1*mu2 + a2*mu + a3 );
};



const flute = synth.def(class {
    constructor(options) {
        let freq = midi_to_hz(options.note);
        let delay_samples = 1 / (freq * ditty.dt); // Duration of one period in samples
        this.len = Math.floor(delay_samples) + 2; // buffer size
        this.fd = delay_samples % 1; // fractional delay to interpolate between samples
        this.buf = new Float32Array(this.len); // buffer used to create a delay
        this.pos = 0; // current position of the reading/writing head
        let lpcutoff = 5*freq;
        this.a0 = clamp01(2 * Math.PI * lpcutoff * ditty.dt); // Lowpass filter the reinjection
        let hpcutoff = 0.1*freq;
        this.a1 = clamp01(2 * Math.PI * hpcutoff * ditty.dt); // hipass filter the reinjection
        this.s0 = 0; // Signal value history for the lowpass filter
        this.s1 = 0; // Signal value history for the hipass filter
        this.windEnv = adsr.create({attack: 0.05, release: 0.1, duration:options.duration});
    }
    process(note, env, tick, options) {
        let pos = this.pos;
        /*
        let value = lerp(this.buf[pos],
                         this.buf[(pos+1)%this.len],
                         this.fd); // linear interpolation for the fractional delay
        */
        let value = cubic_interpolate(this.buf[pos],
                                      this.buf[(pos+1)%this.len],
                                      this.buf[(pos+2)%this.len],
                                      this.buf[(pos+3)%this.len],
                                      this.fd);
        
        /*
        if(tick < options.duration) {
            // Add noise
            value += input.fluteNoiseAmount * (Math.random() - 0.5);
            // Nonlinearity to gain amplitude
            value = input.fluteGain * saturate(value + input.fluteOffset);
        } else {
            value *= 0.95;
        }*/
        
        // Add noise
        value += this.windEnv.value * input.fluteNoiseAmount * (Math.random() - 0.5);
        // Nonlinearity to gain amplitude
        let offset = (options.offset >= 0) ? options.offset : input.fluteOffset;
        value = saturate(value + offset);
        // Lower amplitude when we stop blowing
        value *= lerp(0.95, input.fluteGain, this.windEnv.value);
        
        this.s0 += this.a0 * (value - this.s0); // lowpass filter
        this.s1 += this.a1 * (this.s0 - this.s1); // hipass filter
        this.buf[pos] = this.s0 - this.s1;
        this.pos = (pos+1)%this.len;
        return this.buf[pos] * env.value; // Apply envelope to avoid clicks
    }
}, {env:adsr, attack:0.05, offset:-1, duration:1, release:1});
// lpcutoff:2000, hpcutoff:50

ditty.bpm = 60;


// Manually adjusted retuning, starting from C4
// Only valid for the default parameters!
const out_of_tune = [
    -31,-30,
    -29,-26,-26,-26,-28,
    -24,-27,-25,-25,-18,
    -22, -31, -15, -16-7-9, -14-2,
    -26+2, -29, -19-7+16, -28+8+16, -27+15+5,
    -13+5-20+1, -24-8+22-4, -9, -11-22, -15,
    0, -10, -25, 6, 22,
    -36, 29];

// Try to correct the pitch of the note
const retune = (nn) => {
    let i = nn - 48;
    if(i < 0) {
        return nn - 0.01*out_of_tune[0];
    }
    if(i < out_of_tune.length) {
        return nn - 0.01*out_of_tune[i];
    }
    return nn;
};

//let i = 48;
//flute.play(retune(i));

// Single note
// Try to turn the gain all the way down, and then to some value, to simulate the attack of the flute.
//flute.play(fs4, {duration: 120});



// First few bars of Fauré's Pavane


loop( () => {
    // Flute part
    let notes = [fs4, gs4, a4, b4, a4, gs4, a4,fs4,gs4,a4,gs4,fs4,gs4,e4,fs4,f4,cs4];
    let durs =  [1.75, 0.25, 1.75, 0.25, 0.5, 0.5, 0.5, 0.5, 1.75, 0.25, 0.5, 0.5, 0.5, 0.5, 1.75, 0.25, 4];
    for(let i=0; i<notes.length; i++){
        flute.play(retune(notes[i]), {duration:durs[i], pan:-0.2, amp:0.3});
        sleep(durs[i]);
    }
    // Oboe + clarinet
    let notes1 = [cs5, d5, e5, fs5, e5, d5, e5, cs5, d5, e5, d5, cs5, d5, b4, cs5, c5, cs5];
    let notes2 = [a4, b4, cs5, d5, cs5, b4, cs5, a4, b4, cs5, b4, a4, b4, g4, a4, gs4, gs4];
    durs =  [1.75, 0.25, 1.75, 0.25, 0.5, 0.5, 0.5, 0.5, 1.75, 0.25, 0.5, 0.5, 0.5, 0.5, 1.5, 0.5, 3];
    for(let i=0; i<notes1.length; i++){
        flute.play(retune(notes1[i]), {duration:durs[i], pan:0.2, amp:0.25, offset:0.23});
        flute.play(retune(notes2[i]), {duration:durs[i], pan:-0.2, amp:0.25, offset:0.06});
        sleep(durs[i]);
    }
    sleep(1);
}, {name:"Flute, Oboe, Clarinet"}).connect(hallverb);



loop( () => {
    // Cello part
    let notes = [fs3, d3, e3, cs3, d3, b2, cs3, b2, a2, fs2, b2, e2, a2, d3, cs3, 0];
    for(let i=0; i<notes.length; i++) {
        flute.play(retune(notes[i]), {duration:0.75, pan:0.4, amp:0.6});
        sleep(2);
    }
}, {name:"Cello", amp:0.45}).connect(hallverb);




loop( () => {
    // Viola part
    let notes = [0, a3, cs4, a3, 0, cs4, fs4, cs4, 0, b3, e4, b3,  0, b3, e4, b3,
                 0, a3, d4, a3,  0, a3, d4, a3, 0, gs3, f4, gs3, 0, gs3, f4, gs3,
                 0, fs4, cs4, fs4, 0, cs4, a3, cs4, 0, fs4, a3, fs4, 0, b3, g3, b3,
                 0, e4, g3, e4, 0, fs4, gs3, fs4, 0, gs3, cs4, d4, b3, cs4, a3, gs3];
    for(let i=0; i<notes.length; i++){
        if(notes[i]) {
            flute.play(retune(notes[i]), {duration:0.3, pan:-0.3, amp:0.3});
        }
        sleep(0.5);
    }
}, {name: "Viola"}).connect(hallverb);


loop( () => {
    let notes = [0, a4, g4, fs4, f4, 0];
    let durs = [4*4+2, 4, 4, 2, 3, 1];
    for(let i=0; i<notes.length; i++) {
        if(notes[i]) {
            flute.play(retune(notes[i]), {duration: durs[i], pan:-0.6, amp:0.25});
        }
        sleep(durs[i]);
    }
}, {name: "Violin I"}).connect(hallverb);