Fork: DT303 pxf

Another fork by paulofalcao

Log in to post a comment.

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