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