minitracker

Tiny tracker with a cello instrument.
Melody is not too great.

Log in to post a comment.

//
// You can find the Dittytoy API Reference here: https://Dittytoy.net/syntax
// Example ditties can be found here: https://dittytoy.net/user/Dittytoy
//
// Most of your ditty will run 44100 times per second using javascript in the browser.
// Make sure you optimize your ditty to work well on as many devices as possible. To do that, try to limit
// the number of simultaneously active synths: make sure they don't last longer than necessary, or you can
// hear them, and spread them over different loops (each loop runs in a separate worker).
//

ditty.bpm = 120;

const start = 0;
const notelen = 0.15;
const melody = [
    // open
     0, ds4, 12, 1,
    15, fs4, 4, 1,
    20, cs4, 4, 1,
    25, gs3, 13, 1,
    
    50, ds4, 14, 1,
    65, fs4, 4, 1,
    70, cs4, 4, 1,
    75, gs3, 13, 1,
    
    100, ds4, 13, 1,
    115, fs4, 4, 1,
    120, cs4, 5, 1,
    125, gs3, 13, 1,
    
    150, ds4, 12, 1,
    165, fs4, 5, 1,
    170, cs4, 4, 1,
    175, gs3, 16, 1,
    
    // main
    200, as4, 5, 1,
    205, ds5, 5, 1,
    210, as5, 14, 1,
    225, gs5, 5, 1,
    230, f5, 15, 1,
    
    250, cs5, 5, 1,
    255, ds5, 4, 1,
    260, fs5, 18, 1,
    
    280, f5, 5, 1,
    285, as4, 10, 1,
    
    300, as4, 1, 1,
    305, ds5, 1, 1,
    310, as5, 1, 1,
    325, gs5, 1, 1,
    330, f5, 1, 1,
    
    350, cs5, 1, 1,
    355, ds5, 1, 1,
    360, fs5, 1, 1,
    
    380, f5, 1, 1,
    385, as4, 1, 1
];

input.tuning = 0.875; //min=0.125, max=2, step=0.125

input.tuning = 0.75; //min=0.125, max=2, step=0.125

input.vibrato_amount = 1.5; //min=0, max=5, step=0.01
input.vibrato_freq = 15; //min=0, max=60, step=0.01

input.loss_freq = 900; //min=100, max=8000, step=1
input.loss_q = 1.3;//min=0.1, max=10, step=0.1
input.loss_wet = 1; //min=0, max=1, step=0.01

input.body_length = 0.06; //min=0, max=1, step=0.01
input.body_diffuse = 0.02; //min=0, max=1, step=0.01
input.body_feedback = 0.6; //min=0, max=1, step=0.01
input.body_damping = 5200; //min=10, max=10000, step=1
input.body_wet = 1.0; //min=0, max=1, step=0.01

function softclip(x) {
    return x < -1 ? -1 : x > 1 ? 1 : 1.5*(1-x*x/3)*x;
}

function varsaw(p, formant) {
    let x = p-~~p;
    return (x - 0.5) * softclip(formant*x*(1-x)) * 2;
}

const osc = synth.def(
    class {
        constructor(options) {
            // The value of the note argument of the play call is retrievable via options.note.
            this.phase = 0.0;
            this.vibrato = 0.0;
            
            // filter state
            this.ic1eq = 0.0;
            this.ic2eq = 0.0;
        }
        process(note, env, tick, options) {
            // saw wave
            this.vibrato += ditty.dt * input.vibrato_freq;
            this.phase += (midi_to_hz(note) + Math.sin(this.vibrato) * input.vibrato_amount)
                * ditty.dt * input.tuning;
                
            const saw = varsaw(this.phase, 50);
            const noise = Math.random();
            const v0 = (saw + 0.2 * noise) * env.value;
            
            // SVF filter
            // https://cytomic.com/files/dsp/SvfLinearTrapOptimised2.pdf
            const g = Math.tan(Math.PI * input.loss_freq * ditty.dt);
            const k = 1 / input.loss_q;
            const a1 = 1 / (1 + g * (g + k));
            const a2 = g * a1;
            const a3 = g * a2;
            
            // tick
            const v3 = v0 - this.ic2eq;
            const v1 = a1 * this.ic1eq + a2 * v3;
            const v2 = this.ic2eq + a2 * this.ic1eq + a3 * v3;
            
            // update
            this.ic1eq = 2 * v1 - this.ic1eq;
            this.ic2eq = 2 * v2 - this.ic2eq;
            
            // bandpass out
            return v1 * input.loss_wet + (1 - input.loss_wet) * v0;
        }
    }, {
        // attack parameters
        attack: 0.02,
        decay: 0.0,
        sustain: 1.0,
        release: 0.1,
        env: adsr,
    }
);

// from struss
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;
    }
    tap(offset) {
        let x = this.p - (offset|0) - 1;
        x %= this.n;
        if (x < 0) x += this.n;
        return this.data[x];
    }
}

function newdelay(count) {
    return new Array(count)
        .fill(null)
        .map(_ => new Delayline(ditty.sampleRate * 0.2))
}

function newlens(count, max) {
    return new Array(count)
        .fill(1)
        .map(_ => Math.random() * max * 0.2);
}

function hada8([c0, c1, c2, c3, c4, c5, c6, c7]) {
    // shuffle
    [c0, c1, c2, c3, c4, c5, c6, c7] = [c3, -c7, c1, -c4, -c5, c6, c0, c2];
    
    // 8x8 hadamard matrix
    return [
        (c0 + c1 + c2 + c3 + c4 + c5 + c6 + c7) * Math.sqrt(1 / 8),
        (c0 - c1 + c2 - c3 + c4 - c5 + c6 - c7) * Math.sqrt(1 / 8),
        (c0 + c1 - c2 - c3 + c4 + c5 - c6 - c7) * Math.sqrt(1 / 8),
        (c0 - c1 - c2 + c3 + c4 - c5 - c6 + c7) * Math.sqrt(1 / 8),
        (c0 + c1 + c2 + c3 - c4 - c5 - c6 - c7) * Math.sqrt(1 / 8),
        (c0 - c1 + c2 - c3 - c4 + c5 - c6 + c7) * Math.sqrt(1 / 8),
        (c0 + c1 - c2 - c3 - c4 - c5 + c6 + c7) * Math.sqrt(1 / 8),
        (c0 - c1 - c2 + c3 - c4 + c5 + c6 - c7) * Math.sqrt(1 / 8),
    ];
}

function house8([c0, c1, c2, c3, c4, c5, c6, c7]) {
    const sum = c0 + c1 + c2 + c3 + c4 + c5 +  c6 + c7;
    return [c0, c1, c2, c3, c4, c5, c6, c7].map(x => x - sum * 0.25);
}

// geraint luff reverberator
// https://signalsmith-audio.co.uk/writing/2021/lets-write-a-reverb/
class Reverb {
    constructor(n) {
        // diffusors
        this.diff1 = newdelay(8);
        this.diff2 = newdelay(8);
        this.diff3 = newdelay(8);
        this.diff4 = newdelay(8);
        
        // length of diffusors
        this.lens1 = newlens(8, ditty.sampleRate);
        this.lens2 = newlens(8, ditty.sampleRate);
        this.lens3 = newlens(8, ditty.sampleRate);
        this.lens4 = newlens(8, ditty.sampleRate);
        
        // reverb loop
        this.reverb = newdelay(8);
        this.rvlens = newlens(8, ditty.sampleRate);
        this.rvlp = new Array(8).fill(0);
    }
    tick(inp) {
        // split into channels
        const channels = new Array(8).fill(inp);
        
        // diffuser 1
        const diff1 = hada8(this.diff1.map((d, i) => {
            d.clock(channels[i]);
            return d.tap(this.lens1[i] * input.body_diffuse);
        }));
        
        // diffuser 2
        const diff2 = hada8(this.diff2.map((d, i) => {
            d.clock(diff1[i]);
            return d.tap(this.lens2[i] * input.body_diffuse);
        }));
        
        // diffuser 3
        const diff3 = hada8(this.diff3.map((d, i) => {
            d.clock(diff2[i]);
            return d.tap(this.lens3[i] * input.body_diffuse);
        }));
        
        // diffuser 4
        const diff4 = hada8(this.diff4.map((d, i) => {
            d.clock(diff3[i]);
            return d.tap(this.lens4[i] * input.body_diffuse);
        }));
        
        // reverb loop
        const fb = house8(this.reverb.map((d, i) => {
            const tap = d.tap(this.rvlens[i] * input.body_length);
            return tap * input.body_feedback + diff4[i];
        }));
        
        // filter
        const damped = fb.map((x, i) => {
            // one pole
            this.rvlp[i] 
                += (x - this.rvlp[i]) 
                * (1 - Math.exp(-input.body_damping * ditty.dt * Math.PI * 2));
            
            return this.rvlp[i];
        });
        
        // feedback
        this.reverb.forEach((d, i) => d.clock(damped[i]));
        
        // and out again
        return fb.reduce((a, d) => a + d, 0) * Math.sqrt(1 / channels.length);
    }
}

// Simple allpass reverberator, based on this article:
// http://www.spinsemi.com/knowledge_base/effects.html
const reverb = filter.def(class {
    constructor(options) {
        this.l = new Reverb(1);
        this.r = new Reverb(1);
    }
    process(inp, options) {
        const [l, r] = inp;
        return [
            this.l.tick(l) * input.body_wet
            + l * (1 - input.body_wet),
            this.r.tick(r) * input.body_wet
            + r * (1 - input.body_wet)
        ];
    }
});

loop( () => {
    let time = 0;
    for (let i = 0; i < melody.length; i += 4) {
        const sta = melody[i + 0];
        const not = melody[i + 1];
        const len = melody[i + 2];
        const amp = melody[i + 3];
        
        if (sta >= start) {
            sleep((sta - time) * notelen);
            osc.play(not, { duration: len * notelen });
        }
        
        time = sta;
    }
    
    sleep(5);

}, { name: 'Cello' }).connect(reverb.create());