// ========== Audio EQ filters ========== see https://dittytoy.net/ditty/b0737467eb const cos = Math.cos, sin = Math.sin, PI = Math.PI, sqrt = Math.sqrt; // Second-order section in Transposed Direct Form II // https://ccrma.stanford.edu/~jos/filters/Transposed_Direct_Forms.html class TDF2 { constructor(a1,a2,b0,b1,b2) { this.set_coefs(a1,a2,b0,b1,b2); this.s1 = 0; this.s2 = 0; } set_coefs(a1,a2,b0,b1,b2){ this.a1 = a1; this.a2 = a2; this.b0 = b0; this.b1 = b1; this.b2 = b2; } process(xn) { var yn = this.s1 + this.b0 * xn; this.s1 = this.s2 - this.a1 * yn + this.b1 * xn; this.s2 = - this.a2 * yn + this.b2 * xn; return yn; } } // Biquad filter coefficients from the Audio EQ Cookbook // https://www.w3.org/TR/audio-eq-cookbook/ function calc_biquad_coefs(opt) { var w0 = 2*PI*opt.f0*ditty.dt; var cw0 = cos(w0); var alpha = sin(w0)/(2*opt.Q); if(opt.type == "LPF") { // Low pass filter var b0 = (1 - cw0)/2, b1 = 1 - cw0, b2 = (1 - cw0)/2, a0 = 1 + alpha, a1 = -2*cw0, a2 = 1 - alpha; } else if (opt.type == "HPF") { // High pass filter var b0 = (1 + cw0)/2, b1 = -1 - cw0, b2 = (1 + cw0)/2, a0 = 1 + alpha, a1 = -2*cw0, a2 = 1 - alpha; } else if(opt.type == "resonance") { // Band pass filter (constant skirt gain, peak gain = Q) var b0 = opt.Q*alpha, b1 = 0, b2 = -opt.Q*alpha, a0 = 1 + alpha, a1 = -2*cw0, a2 = 1 - alpha; } else if(opt.type == "BPF") { // Band pass filter (constant 0 dB peak gain) var b0 = alpha, b1 = 0, b2 = -alpha, a0 = 1 + alpha, a1 = -2*cw0, a2 = 1 - alpha; } else if(opt.type == "notch") { // Notch filter (removes frequency f0) var b0 = 1, b1 = -2*cw0, b2 = 1, a0 = 1 + alpha, a1 = -2*cw0, a2 = 1 - alpha; } else if(opt.type == "APF") { // All pass filter (constant gain, phase shift around f0) var b0 = 1 - alpha, b1 = -2*cw0, b2 = 1 + alpha, a0 = 1 + alpha, a1 = -2*cw0, a2 = 1 - alpha; } else if(opt.type == "peak") { // Peaking EQ (requires dBgain) var A = 10**(opt.dBgain/40); var b0 = 1 + alpha*A, b1 = -2*cw0, b2 = 1 - alpha*A, a0 = 1 + alpha/A, a1 = -2*cw0, a2 = 1 - alpha/A; } else if(opt.type == "lowShelf") { // Low shelf (requires dBgain) var A = 10**(opt.dBgain/40); var b0 = A*( (A+1) - (A-1)*cw0 + 2*sqrt(A)*alpha ), b1 = 2*A*( (A-1) - (A+1)*cw0), b2 = A*( (A+1) - (A-1)*cw0 - 2*sqrt(A)*alpha ), a0 = (A+1) + (A-1)*cos(w0) + 2*sqrt(A)*alpha, a1 = -2*( (A-1) + (A+1)*cw0), a2 = (A+1) + (A-1)*cos(w0) - 2*sqrt(A)*alpha; } else if(opt.type == "highShelf") { // High shelf (requires dBgain) var A = 10**(opt.dBgain/40); var b0 = A*( (A+1) + (A-1)*cos(w0) + 2*sqrt(A)*alpha ), b1 = -2*A*( (A-1) + (A+1)*cos(w0) ), b2 = A*( (A+1) + (A-1)*cos(w0) - 2*sqrt(A)*alpha ), a0 = (A+1) - (A-1)*cos(w0) + 2*sqrt(A)*alpha, a1 = 2*( (A-1) - (A+1)*cos(w0) ), a2 = (A+1) - (A-1)*cos(w0) - 2*sqrt(A)*alpha; } else { debug.error("Unknown filter type"); } return [a1/a0, a2/a0, b0/a0, b1/a0, b2/a0]; } // =================== echo filter by srtuss =================== // https://dittytoy.net/ditty/cef878f9e1 class Delayline { constructor(n) { this.n = ~~n; this.p = 0; this.lastOut = 0; this.data = new Float32Array(n); } process(input) { this.lastOut = this.data[this.p]; this.data[this.p] = input; if(++this.p >= this.n) this.p = 0; return this.lastOut; } tap(offset) { var x = this.p - offset - 1; x %= this.n; if(x < 0) x += this.n; return this.data[x]; } } const echo = filter.def(class { constructor(opt) { this.lastOut = [0, 0]; var division = opt.division || 3/4; var pan = clamp01((opt.pan || 0)*.5+.5); var sidetime = (opt.sidetime || 0) / ditty.dt; var time = 60 * division / ditty.bpm; this.fb = clamp(opt.feedback || 0, -1, 1); this.kl = 1-pan; this.kr = pan; this.wet = opt.wet || .5; this.stereo = isFinite(opt.stereo) ? opt.stereo : 1; var n = ~~(time / ditty.dt); this.delay = [new Delayline(n), new Delayline(n)]; this.dside = new Delayline(~~sidetime); } process(inv, opt) { this.dside.process(inv[0]); var l = this.dside.lastOut * this.kl; var r = inv[1] * this.kr; var nextl = l + this.delay[1].lastOut * this.fb; var nextr = r + this.delay[0].lastOut * this.fb; this.lastOut[0] = inv[0] + this.delay[0].lastOut * this.wet; this.lastOut[1] = inv[1] + this.delay[1].lastOut * this.wet; this.delay[0].process(nextl); this.delay[1].process(nextr); if(this.stereo != 1) { var m = (this.lastOut[0] + this.lastOut[1])*.5; var s = (this.lastOut[0] - this.lastOut[1])*.5; s *= this.stereo; this.lastOut[0] = m+s; this.lastOut[1] = m-s; } return this.lastOut; } }, {sidetime: .01, division: 1/2, pan: .5, wet: .5, feedback: .6, stereo: 2}); // ================================================= // ================== RAINY NIGHT ================== // ================================================= // // Synth code by athibaul // https://dittytoy.net/ditty/ea22c668ca const rand = (a,b) => a + (b-a) * Math.random(); const xrand = (a,b) => a * (b/a)**Math.random(); const aaSaw = (p, dp) => (2 * p - 1) * clamp01(p * (1-p)/dp); // Basic anti-aliasing //const aaSaw = (p, dp) => (2 * p - 1); // without anti-aliasing const bpfSaw = synth.def(class { constructor(opt) { this.freq = midi_to_hz(opt.note); var coefs = calc_biquad_coefs({type:"resonance", f0:opt.cf, Q:opt.Q}); this.filt = new TDF2(...coefs); this.p = opt.p0 || 0; this.dp = ditty.dt * this.freq; this.shimt = 0; } process(note, env, tick, opt) { this.shimt -= ditty.dt * opt.shimFreq; if(this.shimt <= 0) { var freq = this.freq * (1 + 0.06*rand(-1,1)*opt.shimAmp); this.dp = ditty.dt * freq; this.shimt = 1; } this.p += this.dp; this.p -= ~~this.p; return this.filt.process(aaSaw(this.p, this.dp)) * env.value * 0.05; } }, {attack:0.01, release:0.3, cf:2500, Q:3, shimFreq:10, shimAmp:0}); /* loop((i) => { // Rhythm sleep(1); bpfSaw.play(hz_to_midi(2), {cf:60, Q:5, env:one}); // bass drum bpfSaw.play(hz_to_midi(2), {cf:8000, Q:1, env:one}); // BD click bpfSaw.play(hz_to_midi(8), {cf:1000, Q:15, env:one, amp:0.2}); bpfSaw.play(hz_to_midi(2), {cf:2500, Q:30, env:one, amp:0.2, p0:-1/2}); bpfSaw.play(hz_to_midi(1), {cf:1800, Q:10, env:one, amp:0.15, p0:-3/8}); bpfSaw.play(hz_to_midi(4), {cf:4623, Q:15, env:one, amp:0.15}); sleep(1); bpfSaw.play(hz_to_midi(1), {cf:210, Q:5, env:one}); // snare sleep(1000); }, {amp:3}); */ loop((i) => { sleep(4); bpfSaw.play(hz_to_midi(1/16), {cf:38.3, Q:200, env:one, amp:xrand(1,2)}); bpfSaw.play(hz_to_midi(1/16), {cf:55.2, Q:40, env:one, amp:xrand(1,2)}); sleep(1000); }, {name:"bass drum"}) .connect(echo.create()); loop( (i) => { var nn = 60 + ~~((12/5) * ~~(15*Math.random())); var freq = xrand(0.5,6); bpfSaw.play(hz_to_midi(freq), {cf:midi_to_hz(nn), attack:5, release:5, Q:xrand(10,700), amp:0.5, pan:rand(-1,1), shimFreq:xrand(0.01,1)*freq, shimAmp:6 }); sleep(1.5); }, {name:"drops"}) .connect(echo.create()); loop( (i) => { var nn = 48 + ~~((12/5) * ~~(15*Math.random())); bpfSaw.play(nn, {attack:0.2, release:xrand(0.5,5), cf:xrand(500,3000), Q:xrand(2,10), pan:rand(-0.5,0.5)}); sleep(xrand(0.5,15)); }, {name:"voice", amp:0.5}) .connect(echo.create()); const bnn = [36,29,36,38,31,33]; const chordsnn = [[48,55,62,69,76],[53,57,60,65,67,69,72], [48,55,60,64,66,67,71,74], [50,53,62,64,65,67,69], [53,57,60,64,65,68,72], [53,55,57,60,61,62]]; loop( (i) => { for(nn of bnn) { bpfSaw.play(nn, {attack:5, duration:32, release:5, cf:60, Q:0.1, amp:xrand(3,6), shimAmp:0.05}); bpfSaw.play(nn+12.05, {attack:5, duration:32, release:5, cf:100, Q:0.1, amp:xrand(3,6), shimAmp:0.05}); sleep(32); } }, {name:"bass", amp:0.8}) .connect(echo.create()); loop( (i) => { for(chord of chordsnn) { for(nn of chord) { for(j of [1,2,3]) { bpfSaw.play(nn, {attack:rand(2,8), duration:32, release:rand(2,8), cf:xrand(1500,3000), Q:xrand(5,10), amp:xrand(0.2,0.5), shimFreq:xrand(2,20), shimAmp:0.1, pan:rand(-1,1) }); } } sleep(32); } }, {name:"pad", amp:0.3}) .connect(echo.create());