Flute + Fauré's Pavane

A simplified flute simulation, manually retuned, playing the beginning of Gabriel Fauré's Pavane.

Log in to post a comment.

// 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.4});
        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.3, offset:0.23});
        flute.play(retune(notes2[i]), {duration:durs[i], pan:-0.2, amp:0.3, offset:0.06});
        sleep(durs[i]);
    }
    sleep(1);
}, {name:"Flute, Oboe, Clarinet"});



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"});




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"});


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.3});
        }
        sleep(durs[i]);
    }
}, {name: "Violin I"});