//Added the drum from "techno beat 1" by struss https://dittytoy.net/ditty/b3585fb3a3 // Another fork by paulofalcao // https://dittytoy.net/ditty/568826700e // Forked from "DT303" by srtuss // https://dittytoy.net/ditty/0029103012 // TB303-like synth. // 2022/12/4, srtuss // patterns are encoded as 4 values per step: note,rest,slide,accent (rests are not implemented) var pattern = [ c3,1,0,0,g5,1,1,0,c3,1,0,0,c3,1,1,1,c3,1,0,0,g5,1,0,0,c3,1,1,1,c5,1,0,0, c3,1,0,0,g5,1,1,0,c3,1,0,0,c3,1,1,1,c3,1,0,0,g5,1,0,0,c3,1,1,1,c5,1,0,0, c3,1,0,0,g5,1,1,0,c3,1,0,0,c3,1,1,1,c3,1,0,0,g5,1,0,0,c3,1,1,1,c5,1,0,0, c3,1,0,0,a5,1,1,0,c3,1,0,0,c3,1,1,1,c3,1,0,0,f5,1,0,0,c3,1,1,1,c5,1,0,0, c3,1,0,0,f5,1,1,0,c3,1,0,0,c3,1,1,1,c3,1,0,0,f5,1,0,0,c3,1,1,1,c5,1,0,0, c3,1,0,0,f5,1,1,0,c3,1,0,0,c3,1,1,1,c3,1,0,0,f5,1,0,0,c3,1,1,1,c5,1,0,0, c3,1,0,0,f5,1,1,0,c3,1,0,0,c3,1,1,1,c3,1,0,0,f5,1,0,0,c3,1,1,1,c5,1,0,0, c3,1,0,0,c5,1,1,0,c3,1,0,0,c3,1,1,1,c3,1,0,0,g5,1,0,0,c3,1,1,1,c5,1,0,0, ]; const pblep = (t, dt) => { if(t < dt) { t /= dt; return t + t - t * t - 1; } else if (t > 1 - dt) { t = (t - 1) / dt; return t * t + t + t + 1; } return 0; } input.gate=.58; // min=.1,max=1,step=.02 input.tune=-18; // min=-24,max=24,step=.01 input.waveform=0.43; input.cutoff=0.40; input.resonance=0.69; input.envmod=0.01; input.decay=0.05; input.overdrive=0.45; input.echo = .8; // value=.8 ditty.bpm = 138; class SVFilter { constructor() { this.lastLp = 0; this.lastHp = 0; this.lastBp = 0; this.kf = 0.1; this.kq = .1; } process(input) { var lp = this.lastLp + this.kf * this.lastBp; var hp = input - lp - this.kq * this.lastBp; var bp = this.lastBp + this.kf * hp; this.lastLp = lp; this.lastHp = hp; this.lastBp = bp; return lp; } } class SVFilter2 { constructor() { this.f = [new SVFilter(), new SVFilter()]; this.lastLp = 0; this.kf = 0.1; this.kq = .3; } process(input) { this.f[0].kf = this.kf; this.f[0].kq = this.kq; this.f[1].kf = this.kf; this.f[1].kq = this.kq; this.f[0].process(input); this.f[1].process(this.f[0].lastLp); this.lastLp = this.f[1].lastLp; return this.lastLp; } } class Convol { constructor() { this.kernel = [0.013475, 0.097598, 0.235621, 0.306612, 0.235621, 0.097598, 0.013475]; this.taps = new Float32Array(this.kernel.length); } process(v) { var n = this.kernel.length; var t = this.taps; for(var i = n-1; i > 0; --i) t[i] = t[i-1]; t[0] = v; var o = 0; for(var i = 0; i < n; ++i) o += this.kernel[i] * t[i]; return o; } } class TbVoice { constructor(opt) { this.ph = 0; this.dtph = .001; this.flt = new SVFilter2(); this.tclk = 0; this.dtclk = ditty.bpm / 60 * 4 * ditty.dt; this.clkidx = 0; this.sqidx = 0; this.globalidx = 0; this.lastgate = 0; this.venv = 0; this.fenv = 0; this.slide = 0; this.note = 0; this.notecap = 0; this.att = 0; this.accent = 0; this.aenv = 0; this.cut = 0; this.us = new Convol(); this.ds = new Convol(); } seqstep() { var patdata = pattern; var pl = patdata.length/4; this.sqidx %= pl; this.slide = patdata[this.sqidx*4+2]; this.xslide = patdata[((this.sqidx+pl-1)%pl)*4+2]; this.note = patdata[this.sqidx*4] + input.tune; this.accent = patdata[this.sqidx*4+3]; this.cut = Math.sin(this.globalidx*0.05)*0.2 + Math.cos(this.sqidx*3.1415*0.125)*0.1 + input.cutoff; this.sqidx++; this.globalidx++; this.sqidx %= pl; } process(note, env, tick, opt) { while(this.tclk >= 1) { this.tclk -= 1; this.seqstep(); } var gate = this.tclk < input.gate || this.slide; this.tclk += this.dtclk; if(gate) { if(!this.lastgate) { this.fenv = 1; this.att = 0; this.aenv = this.accent; } this.venv = 1; this.att = Math.min(this.att+ditty.dt/.0005, 1); } this.lastgate = gate; if(this.xslide) { this.notecap += (this.note - this.notecap) * .0005; } else { this.notecap = this.note; } this.dtph = midi_to_hz(this.notecap) * ditty.dt; this.venv *= .995; this.fenv *= Math.exp(-(1-input.decay) * .004); var blep0 = pblep(this.ph, this.dtph); var saw = this.ph * 2 - 1 - blep0; var pw = Math.max(.5, input.waveform)*.95; var square = (this.ph > pw ? 1 : -1) + pblep((this.ph-pw+1)%1, this.dtph) - blep0; var v = lerp(saw, square, clamp01(input.waveform * 2)); this.flt.kq = 1 - input.resonance * .9 - this.aenv * .1; this.flt.kf = 2 ** Math.min(0, this.cut * 7 - 7 + this.fenv * input.envmod * 5 + this.aenv * .4); this.flt.process(v); v = this.flt.process(v); this.ph += this.dtph; while(this.ph >= 1) { this.ph -= 1; } v = v * this.venv * this.att * (this.aenv + 1); this.aenv *= .9995; v *= .5; if(input.overdrive > 0) { var e = [0, 0, 0, 0]; e[0] = this.us.process(v*4); e[1] = this.us.process(0); e[2] = this.us.process(0); e[3] = this.us.process(0); for(var i = 0; i < 4; ++i) { e[i] = clamp(e[i] * (1+input.overdrive*8), -1, 1); } this.ds.process(e[0]); this.ds.process(e[1]); this.ds.process(e[2]); v = this.ds.process(e[3]); } return v; } } class Delayline { constructor(n) { this.n = ~~n; this.p = 0; this.lastOutput = 0; this.data = new Float32Array(n); } clock(input) { this.lastOutput = this.data[this.p]; this.data[this.p] = input; if(++this.p >= this.n) { this.p = 0; } } tap(offset) { var x = this.p - offset - 1; x %= this.n; if(x < 0) { x += this.n; } return this.data[x]; } } const stereoEcho = filter.def(class { constructor(options) { this.lastOutput = [0, 0]; var time = 60 / ditty.bpm; //time *= 3 / 4; time /= 2; var n = Math.floor(time / ditty.dt); this.delay = [new Delayline(n), new Delayline(n)]; this.dside = new Delayline(500); this.kfbl = .4; this.kfbr = .7; } process(inv, options) { this.dside.clock(inv[0]); var new0 = (this.dside.lastOutput + this.delay[1].lastOutput) * this.kfbl; var new1 = (inv[1] + this.delay[0].lastOutput) * this.kfbr; this.lastOutput[0] = inv[0] + this.delay[0].lastOutput * input.echo; this.lastOutput[1] = inv[1] + this.delay[1].lastOutput * input.echo; this.delay[0].clock(new0); this.delay[1].clock(new1); return this.lastOutput; } }); const tbvoice = synth.def(TbVoice); loop( () => { tbvoice.play(c0, {duration:600, release:.1, amp: .25}); sleep(600); }, { name: 'acid' }).connect(stereoEcho.create()); // Drum from "techno beat 1" by struss https://dittytoy.net/ditty/b3585fb3a3 // 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(); //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 wf1 = (x, a) => x > a ? a-(x-a) : x; const wf0 = (x, a) => x > 0 ? wf1(x,a) : -wf1(-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(200, midi_to_hz(note), clamp01(this.t * 20)) * 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) * .2; v += (Math.random()) * Math.exp(this.t * -80) * .1; return wf0((v+input.kickWsAsym-.5) * env.value, input.kickWs1); return v*env.value; } }, {duration: .02, release: .6, 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 = .0005; inn[0] = this.flt[0].process(inn[0]); inn[1] = this.flt[1].process(inn[1]); return inn; } }); loop( () => { var r = .6;// [.3, .6].choose(); for(var i = 0; i < 16; ++i) { bd.play(g1, {release:r, amp: .4}); sleep(1.0); } }, { name: 'bd' }).connect(post.create()).connect(compressor);