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