Using a shared filter for compression.
Slightly different with each playback.
Log in to post a comment.
// WIP // Basic elements of a beat/bassline as commonly found in (modern) techno. function processSVF(s, input, a1, a2, a3, k) { var v1, v2, v3; v3 = input - s.ic2eq; v1 = a1 * s.ic1eq + a2 * v3; v2 = s.ic2eq + a2 * s.ic1eq + a3 * v3; s.ic1eq = 2 * v1 - s.ic1eq; s.ic2eq = 2 * v2 - s.ic2eq; s.lp = v2; s.bp = v1; s.hp = input - k * v1 - v2; s.ap = s.lp + s.hp - k * s.bp; } // State Variable Filter based on an article by cytomic Sound Music Software // https://cytomic.com/files/dsp/SvfLinearTrapOptimised2.pdf class SVF { constructor(opt) { this.stages = []; this.mode = opt ? opt.mode || 'lp' : 'lp'; this.fc = 0; this.q = 1; this.num = opt ? opt.num || 1 : 1; // g parameter determines cutoff // k parameter = 1/Q for(var i = 0; i < this.num; ++i) { this.stages.push({lp:0, bp:0, hp:0, ap:0, ic1eq:0, ic2eq:0}); } this.q = opt && isFinite(opt.q) ? opt.q : 1; this.fc = opt && isFinite(opt.fc) ? opt.fc : .25; } process(input) { if(this.fc != this._fc) { this._fc = this.fc; this._q = this.q; var fc = this.fc * .5; if (fc >= 0 && fc < .5) { this.g = Math.tan(Math.PI * fc); this.k = 1 / this.q; this.a1 = 1 / (1 + this.g * (this.g + this.k)); this.a2 = this.g * this.a1; this.a3 = this.g * this.a2; } } if(this.q != this._q) { this._q = this.q; this.k = 1 / this.q; this.a1 = 1 / (1 + this.g * (this.g + this.k)); this.a2 = this.g * this.a1; this.a3 = this.g * this.a2; } for(var i = 0; i < this.num; ++i) { processSVF(this.stages[i], input, this.a1, this.a2, this.a3, this.k); processSVF(this.stages[i], input, this.a1, this.a2, this.a3, this.k); input = this.stages[i][this.mode]; } return input; } } input.compressorThreshold = -15; // min=-30, max=0, step=0.1 input.compressorGain = 4; // min=1, max=4, step=0.1 input.compressorEn = 1; // min=0, max=1, step=1 (off, on) const dB2gain = v => 10 ** (v / 20); const gain2dB = v => Math.log10(v) * 20; const compressor = filter.def(class Compressor { constructor(opt) { this.gain = 1; this.threshold = -40; // PARAM min:-30 max:-1 this.ratio = .9; // PARAM this.peak = 0.01; this.active = 1; // PARAM } process(inv, opt) { this.threshold = input.compressorThreshold; this.gain = input.compressorGain; this.active = input.compressorEn; var inputLevel = Math.max(Math.abs(inv[0]), Math.abs(inv[1])); if(inputLevel > this.peak) this.peak = inputLevel; else this.peak *= .9999; inputLevel = gain2dB(this.peak); var compression = Math.max(0, inputLevel - this.threshold); var dgain = compression ? dB2gain(-compression * this.ratio) : 1; dgain *= this.gain; if(this.active > .5) return [inv[0] * dgain, inv[1] * dgain]; else return inv; } }).createShared(); class Delayline { constructor(n) { this.n = ~~n; this.p = 0; this.lastOut = 0; this.data = new Float32Array(n); } clock(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]; } } function allpass(delayline, x, k) { var delayin = x - delayline.lastOut * k; var y = delayline.lastOut + k * delayin; delayline.clock(delayin); return y; } // Simple allpass reverberator, based on this article by Spin Semiconductor: // http://www.spinsemi.com/knowledge_base/effects.html const reverb = filter.def(class { constructor(opt) { this.lastReturn = 0; this.krt = .5; this.delaylines = []; this.flt = new SVF({fc:.02, q:.5, num:1}); this.pred = new Delayline(60 * .425 / ditty.bpm / ditty.dt); // Create several delay lines with random lengths for(var i = 0; i < 12; ++i) { this.delaylines.push(new Delayline(10 + Math.floor(Math.random() * 2441))); } this.wet = opt.wet || .04; } process(input, opt) { var inv = input[0]; if(opt.dist) { inv = clamp(inv*opt.dist,-1,1) } inv = this.flt.process(this.pred.clock(inv)); var v = this.lastReturn; // Let the signal pass through the loop of delay lines. Inject input signal at multiple locations. v = allpass(this.delaylines[0], v + inv, .5); v = allpass(this.delaylines[1], v, .5); this.delaylines[2].clock(v); v = this.delaylines[2].lastOut * this.krt; v = allpass(this.delaylines[3], v + inv, .5); v = allpass(this.delaylines[4], v, .5); this.delaylines[5].clock(v); v = this.delaylines[5].lastOut * this.krt; v = allpass(this.delaylines[6], v + inv, .5); v = allpass(this.delaylines[7], v, .5); this.delaylines[8].clock(v); v = this.delaylines[8].lastOut * this.krt; v = allpass(this.delaylines[9], v + inv, .5); v = allpass(this.delaylines[10], v, .5); this.delaylines[11].clock(v); v = this.delaylines[11].lastOut * this.krt; this.lastReturn = v; // Tap the delay lines at randomized locations and accumulate the output signal. var ret = [0, 0]; ret[0] += this.delaylines[2].tap(111); ret[1] += this.delaylines[2].tap(2250); ret[0] += this.delaylines[5].tap(311); ret[1] += this.delaylines[5].tap(1150); ret[0] += this.delaylines[8].tap(511); ret[1] += this.delaylines[8].tap(50); ret[0] += this.delaylines[11].tap(4411); ret[1] += this.delaylines[11].tap(540); // Mix wet + dry signal. ret[0] = ret[0] * this.wet + input[0]; ret[1] = ret[1] * this.wet + input[1]; return ret; } }); //input.ws0 = 0.5; input.kickWs1 = 0.58; input.kickWsAsym = 0.52; //const wf1 = x => x > input.ws0 ? (x > input.ws1 ? input.ws0+input.ws1 - x : input.ws0) : x; const fold01 = (x, a) => x > a ? a-(x-a) : x; const fold11 = (x, a) => x > 0 ? fold01(x,a) : -fold01(-x,a); const bd = synth.def(class { constructor(opt) { this.t = 0; this.p = 0; this.svf = new SVF({num:2}); this.svfw = new SVF({num:1, fc:.03, q: 2, mode:'bp'}); } process(note, env, tick, opt) { var v = Math.sin(this.p * Math.PI * 2); this.p += lerp(700, midi_to_hz(note), clamp01(this.t * 50) ** .25) * ditty.dt; this.t += ditty.dt; var nse = (Math.random() - .5) * 2; this.svf.fc = lerp(350, 200, clamp01(this.t * 10)) * ditty.dt; v += this.svf.process(nse); v += this.svfw.process(Math.random()) * Math.exp(this.t * -20) * .5; v += (Math.random()) * Math.exp(this.t * -80) * .4; return fold11((v+input.kickWsAsym-.5) * env.value, input.kickWs1); return v*env.value; } }, {duration: .02, release: .6, attack: .0001}); const perc = synth.def(class { constructor(opt) { this.t = 0; this.p = 0; this.amtBody = opt.amtBody||0; this.amtBurst = .4; this.fDrop = 200; this.fcEnv = opt.fcEnv||500; this.fcBase = opt.fcBase||3000 + (opt.filter||0) * 2000; this.fcEnvSpeed = opt.fcEnvSpeed||100; this.svfl = new SVF({num:2, q: opt.q||1, mode:opt.mode||'bp'}); this.svfr = new SVF({num:2, q: opt.q||1, mode:opt.mode||'bp'}); } process(note, env, tick, opt) { var v = Math.sin(this.p * Math.PI * 2) * this.amtBody; this.p += (midi_to_hz(note) + this.fDrop * clamp01(1 - this.t * 50)) * ditty.dt; this.t += ditty.dt; this.svfr.fc = this.svfl.fc = lerp(this.fcBase+this.fcEnv, this.fcBase, clamp01(this.t * this.fcEnvSpeed)) * ditty.dt; var vl = v + this.svfl.process(Math.random() - .5); var vr = v + this.svfr.process(Math.random() - .5); vl += (Math.random()-.5) * Math.exp(this.t * -20) * this.amtBurst; vr += (Math.random()-.5) * Math.exp(this.t * -20) * this.amtBurst; var sidec = ditty.tick%1; vl *= sidec; vr *= sidec; return [vl * env.value, vr * env.value]; } }, {duration: .01, release: .1, attack: .0001}); const triangle = x => 1-Math.abs(1-x%2); const signed = (x, l) => x > 0 ? l(x) : -l(-x); const nonlin = x => Math.min(x - (clamp(x, .4, .6)-.4)*2, .6); ditty.bpm = 138; const distructor = filter.def(class { constructor(opt) { this.stages = []; for(var i = 0; i < 1; ++i) { this.stages.push({flt: new SVF({num: 2, mode: 'bp', fc: .008, q:2}), flt2:new SVF({num: 2, mode: 'bp', fc: .02, q:2})}); } } process(inn, opt) { var stage = this.stages[0]; var v = signed((inn[0]*input.ws0+input.ws1)*10, x=>nonlin(x)**.5); v = v+stage.flt.process(v)*.4+stage.flt2.process(v)*.8; v = signed((v*input.ws0+input.ws1), x=>nonlin(x)**.5); return [v, v]; } }); const post = filter.def(class { constructor(opt) { this.flt = [new SVF({mode:'hp', num:2}), new SVF({mode:'hp', num:2})]; } process(inn, opt) { var x = ditty.tick%64; this.flt[0].fc = this.flt[1].fc = clamp01((16-x) * 2) * .006 * (1 + Math.max(0,x-8)*.1) + .0005; inn[0] = this.flt[0].process(inn[0]); inn[1] = this.flt[1].process(inn[1]); return inn; } }); loop( () => { var r = [.3, .6].choose(); var note = a1; for(var i = 0; i < 16; ++i) { bd.play(note, {release:r, amp: .4}); sleep(.5); bd.play(note, {release:r, amp: .08, pan: -.2}); sleep(.25); bd.play(note, {release:r, amp: .09, pan: .2}); sleep(.25); } }, { name: 'bd', amp: .6 }).connect(reverb.create({dist:5})).connect(post.create()).connect(compressor); const series = (n, lambda) => { var r = [] for(var i = 0; i < n; ++i) r.push(lambda(i)); return r; } loop( () => { var series0 = series(16, x=>Math.random()**2); var series1 = series(16, x=>Math.random()**2); var series2 = series(16, x=>Math.random()**2); for(var k = 0; k < 16; ++k) { var swing = .04; for(var i = 0; i < 16; ++i) { var nr = i == 6 ? 2 : 1; var p = i; for(var j = 0; j < nr; ++j) { perc.play(g4, {filter: series0[p], duration:series1[p]*.2, amp:series2[p]*.5+.2, pan:(series2[p]*2-1)*.5, amtBody:.1}); sleep((i&1?.25-swing:.25+swing)/nr); } } } }, { name: 'perc', amp: .8 }).connect(reverb.create({wet:.2})).connect(post.create()).connect(compressor); loop( () => { sleep(.5); perc.play(gs4, {ampBody:0, attack: .01, release:.15, mode:'hp', fcBase:4000, amp:.6}); sleep(.5); }, { name: 'hat' }).connect(reverb.create({wet:.2})).connect(post.create()).connect(compressor); loop( () => { sleep(.5); perc.play(gs4, {duration: 1, ampBody:0, attack: .1, release:14, mode:'hp', fcBase:1, fcEnv:8000, fcEnvSpeed:.2 }); sleep(16+15); }, { name: 'noise', amp: .1 }).connect(reverb.create({wet:.2})).connect(post.create()).connect(compressor);