// Fork of "Rameau | Allemande" by athibaul
// https://dittytoy.net/ditty/bd3257fcfd
// 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);
function softclip(x) {
return x<-1?-1:x>1?1:1.5*(1-x*x/3)*x;
}
function varsaw(p, formant) {
let x = p-~~p;
return (x - 0.5) * softclip(formant*x*(1-x));
}
const saw = synth.def(class {
constructor(options) {
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 {
this.freq = my_midi_to_hz(note);
this.freq2 = my_midi_to_hz(note) * 2**(options.vibGap/12);
this.vibHz = options.vibHz;
this.form = options.cutoff / this.freq;
this.p = 0; this.t = 0; this.vibT = 0; this.p2 = 0;
this.isStarted = true;
}
}
this.t += ditty.dt;
if(this.t >= options.vibDly) {
this.vibT += ditty.dt;
}
this.p += lerp(this.freq, this.freq2, 0.5 - 0.5*softclip(1.5*Math.cos(2*Math.PI*this.vibHz*this.vibT))) * ditty.dt * 0.998;
this.p2 += lerp(this.freq, this.freq2, 0.5 - 0.5*softclip(1.5*Math.cos(2*Math.PI*this.vibHz*this.vibT))) * ditty.dt * 1.002;
//this.p += ((this.vibHz*this.vibT)%1 < 0.5 ? this.freq : this.freq2) * ditty.dt;
this.form *= this.form > 5 ? 0.99995 : 1;
var sig = varsaw(this.p, this.form) + varsaw(this.p2, this.form);
return sig * env.value * clamp01(100*this.t) * 0.5;
}
}, {env:adsr, duration:1, release:0.1, cutoff:20000, lowpass_amt:0.1, dly: 0, amp:1, vibGap:0, vibHz:5, vibDly:0.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) / 60, -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;
saw.play(nn, {dly:t, duration: duration, pan:nn_to_pan(nn), vibHz:6+2*Math.random(), vibGap:gap});
//saw.play(nn, {dly:t, duration: t+=2*dt, pan:nn_to_pan(nn)});
/*
saw.play(nn+gap, {dly:t, duration: t+=dt, pan:nn_to_pan(nn)});
while(duration - t > 4*dt && ++count < 3) {
saw.play(nn, {dly:t, duration: t+=dt, pan:nn_to_pan(nn)});
saw.play(nn+gap, {dly:t, duration: t+=dt, pan:nn_to_pan(nn)});
}
saw.play(nn, {dly:t, duration: duration, pan:nn_to_pan(nn)});
*/
} else {
// Simply play the note
saw.play(nn, {duration: duration, dly:dly + 0.01*Math.random(), pan:nn_to_pan(nn),
vibHz:4+2*Math.random(), vibGap:0.1*Math.random()-0.05});
}
}
////////////////////////////////// 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 = [14100,16620,18720,19930,20490,21140,22800,26100]; // 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<0; 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<0; 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.5, rt60:10, cutoff:5000, rtHi:3});
///////////////////////////////////////////////////////////////////////////////////////////////
loop( () => {
playScore(rh);
sleep(1000);
}, {name: "Right hand"}).connect(reverb.create());
loop( () => {
playScore(lh);
sleep(1000);
}, {name: "Left hand"}).connect(reverb.create());