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());