Rameau | Allemande

A transcription of a score for harpsichord by Rameau, with a custom notation system.

Log in to post a comment.

// Custom temperament
//const temperament_shift = [0,0,0,0,0,0,0,0,0,0,0,0]; // equal temperament
const temperament_shift = [0,-24,-7,-31,-14,3,-21,-3,-27,-10,7,-17]; // 1/4 comma meantone
const my_midi_to_hz = (nn) => midi_to_hz(nn + temperament_shift[nn%12]/100) * (415/440);


const ks = synth.def(class {
    constructor(options) {
        let freq = my_midi_to_hz(options.note);
        let dly = 1 / (freq*ditty.dt); // duration of a period in samples
        let cutoff = options.cutoff;
        this.a0 = clamp01(2*Math.PI*cutoff*ditty.dt);
        let dly_lp = (1 - this.a0) / this.a0; // Delay induced by the lowpass filter
        let dly_ap = (dly - dly_lp - 0.1) % 1 + 0.1; // Delay of the allpass filter
        let dly_M  = Math.round(dly - dly_lp - dly_ap); // Samples in the delay line
        this.dl_s = new Float32Array(dly_M);
        this.dl_pos = 0;
        this.dl_M = dly_M;
        this.eta = (1 - dly_ap) / (1 + dly_ap); // Coefficient of the allpass filter
        this.ap_ynm1 = 0;
        this.ap_xnm1 = 0;
        this.lp_znm1 = 0;
        
        this.buf = this.dl_s;
        this.len = this.buf.length;
    
        /*
        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) + 1; // buffer size
        this.fd = 1 - (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
        this.a1 = clamp01(2 * Math.PI * options.cutoff * ditty.dt); // Lowpass filter the reinjection
        this.s0 = 0; // Signal value history for the lowpass filter
        */
        
        this.isStarted = false; // Wait a bit before starting the note
    }
    process(note, env, tick, options) {
        if(!this.isStarted) {
            if(tick < options.dly) {
                return 0;
            } else {
                debug.log("freq", midi_to_hz(options.note));
                let offs = Math.floor(this.len * 0.2 * (1 + 0.2*Math.random())); // the offset determines the plucking position
                debug.log("offset", offs);
                // Initialize part of the buffer with noise
                for(let i=0; i < offs && i < this.len; i++){
                    let v = (1 + Math.random()) / Math.sqrt(offs);
                    //let v = Math.sin(Math.sqrt(i));
                    this.buf[i] += v;
                    // The following line introduces "comb filtering" in the filter input, for more interesting results
                    this.buf[(i+offs)%this.len] -= v;
                }
                this.isStarted = true;
            }
        }
        /*
        let pos = this.pos;
        let value = lerp(this.buf[pos],
                         this.buf[(pos+1)%this.len],
                         this.fd); // linear interpolation for the fractional delay
        // Nonlinearity (optional)
        // value /= 1 + Math.max(value,0);
        this.s0 += this.a1 * (value - this.s0); // lowpass filter
        //this.buf[pos] = value * 0.998;
        this.buf[pos] = lerp(value, this.s0, options.lowpass_amt) * 0.998;
        this.pos = (pos+1)%this.len;
        return this.s0 * env.value; // The natural decay is a bit slow, so we still apply an envelope
        */
        
        let xn = this.dl_s[this.dl_pos];
        // Apply allpass filter (fractional delay)
        // https://ccrma.stanford.edu/~jos/pasp/First_Order_Allpass_Interpolation.html#sec:apinterp
        let yn = this.eta * (xn - this.ap_ynm1) + this.ap_xnm1;
        this.ap_xnm1 = xn; this.ap_ynm1 = yn;
        let noise = 0.001 * (Math.random() - 0.5) * env.value;
        // Apply lowpass filter
        let zn = this.lp_znm1 + this.a0 * (yn - this.lp_znm1 + noise);
        let hp = yn - zn; // Also gather hipass signal
        //let zn = this.lp_znm1 + this.a0 * (yn - this.lp_znm1); // DEBUG bypassing allpass
        this.lp_znm1 = zn;
        
        // Slight nonlinearity : faster decay during the attack
        //zn /= 1 + 0.1*zn**4;
        
        // Feed back into delay line
        this.dl_s[this.dl_pos] = zn * 0.9985;
        this.dl_pos++;
        if(this.dl_pos >= this.dl_M) {
            this.dl_pos -= this.dl_M;
        }
        
        
        // Apply nonlinearity with offset
        let v1 = this.dl_s[this.dl_pos];
        let offs = Math.ceil(this.len * 0.03);
        let v2 = this.dl_s[(this.dl_pos + offs)%this.len];
        let sum = v1 + v2, diff = v1 - v2;
        diff += sum>0 ? 0.03*sum**2 : 0; // Nonlinearity ("zing")
        this.dl_s[this.dl_pos] = v1 = (sum + diff)/2;
        this.dl_s[(this.dl_pos + offs)%this.len] = v2 = (sum - diff)/2;
        
        return zn * env.value;
    }
}, {env:adsr, duration:1, release:0.1, cutoff:6750, lowpass_amt:0.1, dly: 0, amp:1});

// Adapted from "Allemande" by Jean-Philippe Rameau
// in Pièces de clavecin avec une méthode, RCT 2-4 (Rameau, Jean-Philippe)
// https://imslp.org/wiki/Pi%C3%A8ces_de_clavecin_avec_une_m%C3%A9thode,_RCT_2-4_(Rameau,_Jean-Philippe)
const rh = ("'4 g ,5b>'3+d5>e>>g 3 g 3.V>a3 >b 4.>e 3+f ^4.+>>f 3e +^4.>d 3+c ,>b a >@g +f >4.g3 b '4e3 ,g >5+f3 b >4a3 '+d 4>.+d4 e,>b>>g "
+ "3 b 'v>c d 5>c4 ,>e >5+f3 'c, >^b a >2a 3Vb2 3>a >g a b 'c d ,b >>4.'e 2+f g >>4.@c 3,b >5+f@>a "
+ "3 'd >,a 'd ,b g b g >Va 'd ,4d3 'd ,>b >g >b >g >>Va >'d >,4d3 >'d >>,4.^b 3a 4.>+f>@a 3g >5.g>d>>,b");
const lh = ("5 ,e4 5b4 'e d @c ,b 5a'4>a g 5+f,4b '+c +d ,b '5e4 ,g Va b 5.e4. 3b '5e4 ,e "
+ "5a3 ,a b 'c ,5d4 'd ,5g '3g a b g '4c3 5g3 ,4b a3 '4+f3 ,4g3 'g 5d4 3,d e +f g a +f 5,g4 'g ^+f d ,5g4 'g ^+f d v5g4 c 5d4 ,d 5.g'>>>>>g");



ditty.bpm = 40;
const dly_ticks = 1/20;
const dur_to_duration = (dur, dot) => 2**(dur-5) * (1 + 0.5*dot); // Convert Musescore numbers to duration
const nn_to_pan = (nn) => clamp((nn - 64) / 40, -1, 1);

function playScore(score) {
    var alt = 0; // Accidentals: +1 for sharp, -1 for flat (entering a note resets the accidentals)
    var dur = 5; // Duration in Musescore numbers (5 is quarter note, lower is shorter)
    var dot = 0; // Does it have a dot? (changing duration resets the dot)
    var orn = 0; // Ornamentation: 'v' is "pincé" (semitone), 'V' is pincé (whole tone), '^' is trill (semitone), '@' is trill (whole tone)
    // (Sleeping resets the ornamentation)
    var dly = 0; // Delay the onset of the note : ">" is delayed, ">>" is more delayed, etc.
    // (Sleeping resets the delay)
    var octave = 4; // The score starts in octave 4. All notes entered are set in the current octave.
    // An octave shift is performed with "'" and ",".
    // Letters a-g play the notes, but do NOT sleep for the designated duration.
    // Space " " sleeps for the duration of the note (so "g " is one note followed by its duration, but "g  " is one note followed by a rest).
    // This allows a "hack" to make notes sustain longer than their rythmic value.
    // e.g. "5g4 b " plays two eighth notes, a G followed by a B, but sustains the G for the full duration of the beat.
    
    for(let char of score) {
        switch(char) {
            case "+":
                alt++; break;
            case "-":
                alt--; break;
            case ".":
                dot = 1 - dot; break;
            case "v": 
            case "V":
            case "^":
            case "@":
                orn = char; break;
            case ">":
                dly+=dly_ticks; break;
            case "'":
                octave++; break;
            case ",":
                octave--; break;
            case "2":
                dur = 2; dot = 0; break;
            case "3":
                dur = 3; dot = 0; break;
            case "4":
                dur = 4; dot = 0; break;
            case "5":
                dur = 5; dot = 0; break;
            case "a":
            case "b":
            case "c":
            case "d":
            case "e":
            case "f":
            case "g":
                // Play the note
                let nn = notes[char+octave] + alt;
                playNote(nn, dur_to_duration(dur, dot), orn, dly);
                alt = 0;
                break;
            case " ":
                sleep(dur_to_duration(dur, dot));
                orn = 0;
                dly = 0;
                break;
            default:
                break;
        }
    }
}

function playNote(nn, duration, orn, dly) {
    var gap = 0;
    if(orn) {
        switch(orn) {
            case "V":
                gap = -2; break;
            case "v":
                gap = -1; break;
            case "^":
                gap = 1; break;
            case "@":
                gap = 2; break;
        }
        // Play note with trill/pincé
        var t = dly;
        var dt = 1/24 * (1 + 0.1*Math.random());
        var count = 0;
        ks.play(nn,     {dly:t, duration: t+=2*dt, pan:nn_to_pan(nn)});
        ks.play(nn+gap, {dly:t, duration: t+=dt, pan:nn_to_pan(nn)});
        while(duration - t > 4*dt && ++count < 3) {
            ks.play(nn,     {dly:t, duration: t+=dt, pan:nn_to_pan(nn)});
            ks.play(nn+gap, {dly:t, duration: t+=dt, pan:nn_to_pan(nn)});
        }
        ks.play(nn,     {dly:t, duration: duration, pan:nn_to_pan(nn)});
    } else {
        // Simply play the note
        ks.play(nn, {duration: duration, dly:dly + 0.01*Math.random(), pan:nn_to_pan(nn)});
    }
}




////////////////////////////////// REVERB ////////////////////////////////////
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.dls.push(new Delayline(fiboshort[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.7, rt60:1, cutoff:5000, rtHi:0.8});
///////////////////////////////////////////////////////////////////////////////////////////////





loop( () => {
    playScore(rh);
    sleep(1000);
}, {name: "Right hand"}).connect(reverb.create());

loop( () => {
    playScore(lh);
    sleep(1000);
}, {name: "Left hand"}).connect(reverb.create());