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