Oxygene Pt 4 - optimized

The song that inspired every electronic musician - now faster.

Log in to post a comment.

// Forked from "Oxygene Pt 4" by srtuss
// https://dittytoy.net/ditty/24373308b4

// The aim of this fork is to improve the performance of this amazing ditty,
// so that it runs smoothly on low-end hardware.

// My profiling on Chrome and Firefox led to the following observations:
// - frequent calls to Math.min and Math.max could be made much faster by rewriting the function
// - in the Analog and Analog2 synths, the costly frequency calculation could be performed 
//   only 10 times per second, rather than once per sample.
// - the float part operator (x%1) can be made 10 times faster by using x - (~~x)
// - the most costly synth is the string pad, due to the use of 4 notes per chord x 4 oscillators x 2 bars per note
//   The computational cost can be reduced by using only 2 oscillators per note, with no difference to my ear.
//   There is a significant 
// - The echo has negligible complexity, so there is no problem in having several.
// - The phaser is quite costly, but that's normal
//
// Some of the cost corresponds to limitations in the Dittytoy API:
// - using the Options object is significantly more costly than using simple variables or object fields.
//   When some of the options are used every sample and are known to be constant, it is preferable to save them
//   as fields of the object (see the cutoff calculation in Analog).
// - having many synths is costly, partly because of the per-synth cost in the Synth.process() function.
//   The following lines are especially costly, due to the use of properties with getters and setters in the Options object:
//   
//			const note = this.options.note;                                             
//			this.options.tick = this.tick += ditty.dtick;                               
//          ...
//          return filterPanAmp(a ? v : [v, v], this.options.pan, this.options.amp);
//
//   For some reason, running 8 synths in the same loop takes more than 6x more time than running 4 synths.
//   Is there something going on with the algorithm here, or is it a cache issue?


// =================================================================================

// Oxygene Pt. 4 by Jean-Michel Jarre, 1976
// performed by 19KB of JS code
//
// by srtuss, 2022/12/25
// Drums are a fairly accurate emulation of the Korg Minipops7 drummachine that JMJ used
// for this track. Other synths are put together by ear (and aiming for minimal code).
// The "whoosh" sfx in the beginning of the song is missing.

// filtered sawtooth function form athibaul's ditties:
function softclip(x) {
    return x < -1 ? -1 : x > 1 ? 1 : 1.5*(1 - x*x/3)*x;
}
function varsaw(p, formant) {
    var x = p - (~~p); // Faster version of x = p%1;
    return (x - 0.5) * softclip(formant*x*(1-x));
}
function fexp(x) {
    // Fast low-accuracy approximation of exp()
    // exp(x) is approximately (1 + x/n)^n for large n
    x = 1.0 + x / 32.0;
    if(x < 0) return 0.0;
    x *= x; x *= x; x *= x; x *= x; x *= x;
    return x;
}
function fmin(a,b) { return a < b ? a : b; }
function fmax(a,b) { return a > b ? a : b; }

ditty.bpm = 123;

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];
    }
}
input.echo = .8;
const echo = 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 = .5;
        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);
        var m = (this.lastOutput[0] + this.lastOutput[1])*.5;
        var s = (this.lastOutput[0] - this.lastOutput[1])*.5;
        s *= 2;
        return [m+s, m-s];
    }
});

function processAllpass(s, input, a) {
    var z = input - a * s.z;
    s.ap = s.z + a * z;
    s.z = z;
    return s.ap;
}

const fract = (x) => x - Math.floor(x);
const triangle = x => Math.abs(fract(x + .5) - .5) * 2;

var phaser = filter.def(class {
    constructor(opt) {
        this.stages = [[], []];
        this.n = 4;
        this.lastOut = [0, 0];
        this.p = 0;
        this.feedback = .5;
        this.speed = .1;
        this.mix = .5;
        this.fc = .0;
        for(var i = 0; i < this.n; ++i) {
            this.stages[0].push({z: 0, ap: 0});
            this.stages[1].push({z: 0, ap: 0});
        }
    }
    process(inv, opt) {
        //inv[0] = inv[1] = Math.random() - .5;
        var vl = inv[0] + clamp(this.stages[0][this.n-1].ap * this.feedback, -1, 1);
        var vr = inv[1] + clamp(this.stages[1][this.n-1].ap * this.feedback, -1, 1);
        var lfo = (2**triangle(this.p))*.5-1.4;
        this.p += ditty.dt * this.speed;
        for(var i = 0; i < this.n; ++i) {
            vl = processAllpass(this.stages[0][i], vl, lfo);
            vr = processAllpass(this.stages[1][i], vr, lfo);
        }
        vl = lerp(inv[0], vl, this.mix);
        vr = lerp(inv[1], vr, this.mix);
        return [vl, vr];
    }
});

var quijada = synth.def(class {
    constructor(opt) {
        this.t = 0;
    }
    process(note, env, tick, opt) {
        this.t += ditty.dt;
        var p = ((this.t * 25 + .75) % 1) / 25; // pulses at 25Hz
        var p2 = ((this.t * 25) % 1) / 25; // pulses at 25Hz
        
        var sig = Math.sin(p * Math.PI * 2 * 2700); // ringing at 2.7kHz
        var env2 = .6 ** fmax(p * 2750 - 2, 0) * fexp(-this.t * 10);
        env2 -= .6 ** fmax(p2 * 2750 - 2, 0) * .25 * fexp(-this.t * 20);
        var v = sig * env2;
        
        //var v2 = Math.sin(p * Math.PI * 2 * 2700) * .6 ** fmax(p * 2750 - 2, 0) * fexp(-this.t * 10); // ringing at 2.7kHz
        //v2 -= (Math.sin(p2 * Math.PI * 2 * 2700) * .6 ** fmax(p2 * 2750 - 2, 0)) * .25 * fexp(-this.t * 20); // ringing at 2.7kHz
        //v -= v2; // null test
        
        return v * env.value;
    }
}, { attack: .055, duration: 2.0 });

class Tank {
    constructor(opt) {
        this.t = 0;
    }
    process(note, env, tick, opt) {
        this.t += ditty.dt;
        return Math.sin(this.t * Math.PI * 2 * opt.freq) * env.value;
    }
};

var bassdrum = synth.def(Tank, {attack: .001, release: .165, duration: 0, freq: 65});
var conga = synth.def(Tank, {attack: .001, release: .165, duration: 0, freq: 195, amp: .5});
var smallbongo = synth.def(Tank, {attack: .001, release: .05, duration: 0, freq: 600, amp: .5});
var largebongo = synth.def(Tank, {attack: .001, release: .08, duration: 0, freq: 400, amp: .5});
var claves = synth.def(Tank, {attack: .001, release: .05, duration: 0, freq: 2200});
var rimshot = synth.def(Tank, {attack: .0005, release: .01, duration: 0, freq: 860});
var hihat = synth.def((p, e, t, o) => (Math.random() - .5) * e.value, {release: .04, duration: 0, amp: .4});
var cymbal = synth.def((p, e, t, o) => (Math.random() - .5) * e.value, {release: .2, duration: 0, amp: .4});

const beat = [
    ['x....xx..x.x', bassdrum],
    ['..x..x..x..x', smallbongo],
    ['x.....x..x..', largebongo],
    ['.........x.x', conga],
    ['...x.....x..', rimshot],
    ['xxxxxxxxxxxx', hihat],
    ['..x.........', cymbal],
    ['.. .. .. x. ', quijada]
];

loop( () => {
    for(var i = 0; i < 12; ++i) {
        for(var j = 0; j < beat.length; ++j) {
            if(beat[j][0][i] == 'x') {
                beat[j][1].play();
            }
        }
        sleep(1/3);
    }
}, { name: 'minipops7' });


const bass = synth.def(class {
    constructor() {
        this.p = Math.random();
        this.c = 100 + 100 * Math.random();
    }
    process(note, env, tick, options) {
        this.p += midi_to_hz(note) * ditty.dt;
        return varsaw(this.p, 4 + (500 * fexp(-tick * 5) + 10 * env.value) * .1) * env.value;
    }
}, {attack:.01});

const LOWPASS = 'lp';
const BANDPASS = 'bp';
const HIGHPASS = 'hp';
const ALLPASS = 'ap';
class SVF {
    constructor(opt) {
        this.mode = opt ? opt.mode || LOWPASS : LOWPASS;
        this.stages = opt ? opt.stages || 2 : 2;
        this.states = [];
        for(var i = 0; i < this.stages; ++i) {
            this.states.push({lp:0, hp:0, bp:0});
        }
        this.kf = opt && opt.kf ? opt.kf : 0.1;
        this.kq = opt && opt.kq ? opt.kq : 1.5;
        this.run = (state, input, kf, kq) => {
            var lp, hp, bp;
            lp = state.lp + kf * state.bp;
            hp = input - lp - kq * state.bp;
            bp = state.bp + kf * hp;
            state.lp = lp;
            state.hp = hp;
            state.bp = bp;
        };
    }
    process(input) {
        for(var i = 0, ni = this.states.length; i < ni; ++i) {
            const state = this.states[i];
            this.run(state, input, this.kf, this.kq);
            this.run(state, input, this.kf, this.kq);
            input = state[this.mode];
        }
        return input;
    }
}

class Analog {
    constructor(opt) {
        this.ops = [];
        for(var i = 0; i < opt.nuni; ++i) {
            var t = i / (opt.nuni-1);
            this.ops.push({p:Math.random(), p2: 0, po: Math.random()-.5, fl: t, fr:1-t});
        }
        this.c = 100 + 100 * Math.random();
        this.tshimmer = 1;
        this.fa = opt.fa;
        this.fd = opt.fd;
        this.cutoff = opt.cutoff;
    }
    process(note, env, tick, opt) {
        var vl=0, vr=0;
        if(this.tshimmer >= 1) {
            this.tshimmer -= 1;
            for(var i = 0; i < this.ops.length; ++i) {
                var op = this.ops[i];
                op.po = Math.random()-.5;
                op.fbase = midi_to_hz(opt.note + op.po * opt.detune) * ditty.dt;
            }
        }
        this.tshimmer += ditty.dt * 10;
        // Using this.field is much more performant than using opt.field
        //var cutoff = 4 + fmin(1, tick / opt.fa) * fexp(-fmax(0, tick - opt.fa) * opt.fd) * opt.cutoff * 100;
        var cutoff = 4 + fmin(1, tick / this.fa) * fexp(-fmax(0, tick - this.fa) * this.fd) * opt.cutoff * 100;
        for(var i = 0; i < this.ops.length; ++i) {
            var op = this.ops[i];
            // fmin(1, tick) * (2 + 400 * fexp(-tick * 5))
            var fbase = op.fbase;
            var v = varsaw(op.p, cutoff * .008 / fbase);
            vl += v * op.fl;
            vr += v * op.fr;
            op.p += fbase;
            op.p2 += fbase * .5;
        }
        return [vl*env.value, vr*env.value];
    }
}

class Analog2 {
    constructor(opt) {
        if(!isFinite(opt.spread))
            opt.spread = .8;
        this.ops = [];
        for(var i = 0; i < opt.nuni; ++i) {
            var t = opt.nuni > 1 ? i / (opt.nuni-1) : .5;
            var t2 = opt.nuni > 1 ? i / (opt.nuni-1) : 0;
            this.ops.push({
                pha1:Math.random(),
                pha2: .5,
                pitch: t2*2-1,
                fl: lerp(.5,t,opt.spread),
                fr: lerp(.5,1-t, opt.spread)
            });
            this.ops[i].fbase = midi_to_hz(opt.note + this.ops[i].pitch * opt.detune) * ditty.dt;
        }
        this.c = 100 + 100 * Math.random();
        this.tshimmer = 0;
    }
    process(note, env, tick, opt) {
        var vl=0, vr=0;
        for(var i = 0; i < this.ops.length; ++i) {
            var op = this.ops[i];
            var fbase = op.fbase;
            // fmin(1, tick) * (2 + 400 * fexp(-tick * 5))
            var osc1 = varsaw(op.pha1, .3 / fbase);
            var osc2 = varsaw(op.pha2, .5 / fbase);
            vl += osc1 * op.fl;
            vr += osc1 * op.fr;
            op.pha1 += fbase * (1.001+osc2*20);
            op.pha2 += fbase * .995;
        }
        return [vl*env.value, vr*env.value];
    }
};

const pluck = synth.def(Analog, {nuni:2, cutoff: 0, fa: .01, fd: 30, detune: .3, amp: .3, release: .1, attack:.01});
const synth1 = synth.def(Analog, {nuni:4, cutoff: .3, fa: .15, fd: 4, detune: .3, amp: .3});
const synth2 = synth.def(Analog2, {nuni:2, attack: .001, cutoff: .3, fa: .15, fd: 4, detune: .1, amp: .25});

// Original
//const strings = synth.def(Analog, {nuni:4, attack: 1, release: 3, cutoff: .3, fa: .01, fd: 0, detune: .5, amp: .1});
// A little bit faster
//const strings = synth.def(Analog, {nuni:3, attack: 1, release: 3, cutoff: .3, fa: .01, fd: 0, detune: .5, amp: .115});
 // Even faster
const strings = synth.def(Analog, {nuni:2, attack: 1, release: 3, cutoff: .3, fa: .01, fd: 0, detune: .5, amp: .14});

const noise = synth.def(class {
    constructor(opt) {
        this.flt = new SVF({kq: .7, mode: 'bp'});
    }
    process(note, env, tick, opt) {
        this.flt.kf = 2 ** (-1+fmin(1, tick * 2) * .7 - tick * .4);
        return this.flt.process(Math.random() - .5) * env.value;
    }
}, {attack: .01, release: 8, cutoff: .3, amp: .15});

loop(() => {
    var pat = [
        0,0,
        0,0,0,
        0,0,0,0,
        1,1,1,1,
        1,1,1,1,
        0,0,0,
        0,0,0,
        1,0,0,
        0,0,0,0,
        1,1,1,1,
        0,0,0,
        0,0,0,
        1,0,0,
        1,0,0,
        1,0,0,
        1,0,0,
        1,0,0];
    for(var i = 0; i < pat.length; ++i) {
        sleep(4);
        if(pat[i]) {
            noise.play({duration:1});
        }
        sleep(4);
    }
}, {name: 'noise'});


loop( (lc) => {
    var pat0 = {p:[c2,as1,c2,g1,as1,g1,c2,as1,c2,c2,as1,g1],d:[3,2,3,1,2,1,3,2,3,1,2,1]};
    var pat1 = {p:[d2,c2,d2,d2,c2,a1,d2,c2,d2,d2,c2,a1],    d:[3,2,3,1,2,1,3,2,3,1,2,1]};
    var pat2 = {p:[f2,ds2,c2,f2,ds2,c2,f2,ds2,c2,c2,ds2,c2],d:[3,2,3,1,2,3,1,2,3,1,2,1]};
    var pat3 = {p:[f2,ds2,c2,f2,ds2,c2,f2,ds2,c2,ds2,c2,as1,g1,as1,g1,as1],d:[3,2,3,1,2,3,1,1,1,1,1,1,1,1,1,1]};
    var seq = [
        pat0, pat0,
        pat0, pat0, pat1,
        pat0, pat0, pat1, pat2,
        pat0, pat0, pat1, pat3,
        pat0, pat0, pat1, pat2,
        pat0, pat1, pat2,
        pat0, pat1, pat3,
        pat0, pat1, pat2,
        pat0, pat0, pat1, pat2,
        pat0, pat0, pat1, pat3,
        pat0, pat1, pat2,
        pat0, pat1, pat3,
        pat0, pat1, pat2,
        pat0, pat1, pat3,
        pat0, pat1, pat2,
        pat0, pat1, pat3,
        pat0, pat1, pat3,
    ];
    var pat = seq[lc % seq.length];
    for(var i = 0; i < pat.p.length; ++i) {
        var r = Math.random() * .05;
        sleep(r);
        bass.play(pat.p[i], {duration: pat.d[i]/3 - .1 * Math.random(), release: .1});
        sleep(pat.d[i]/3 - r);
    }
}, { name: 'bass' });

loop( (lc) => {
    var pat0 = {p:[c4,ds4,g4,c5]};
    var pat1 = {p:[d4,g4,as4,d5]};
    var pat2 = {p:[c4,f4,a4,c4]};
    var seq = [
        0, 0,
        pat0, pat0, pat1,
        pat0, pat0, pat1, pat2,
        pat0, pat0, pat1, pat2,
        pat0, pat0, pat1, pat2,
        pat0, pat1, pat2,
        pat0, pat1, pat2,
        pat0, pat1, pat2,
        pat0, pat0, pat1, pat2,
        pat0, pat0, pat1, pat2,
        pat0, pat1, pat2,
        pat0, pat1, pat2,
        pat0, pat1, pat2,
        pat0, pat1, pat2,
        pat0, pat1, pat2,
        pat0, pat1, pat2,
        pat0, pat1, pat2
    ];
    var pat = seq[lc % seq.length];
    if(pat) {
        for(var i = 0; i < pat.p.length; ++i) {
            strings.play(pat.p[i]-12, {duration: 8});
            sleep(.05);
        }
        sleep(8-pat.p.length*.05);
    }
    else
        sleep(8);
}, { name: 'strings' }).connect(phaser.create()).connect(echo.create());

loop( (lc) => {
    var pat0 = {p:[c4,c4,c4,c4,c4,c4,c4,c4,c4,c4,c4,c4], a:[0,0,1,0,1,0,1,0,0,1,0,1]};
    var pat1 = {p:[d4,d4,d4,d4,d4,d4,d4,d4,d4,d4,d4,d4], a:[0,0,1,0,1,0,1,0,0,1,0,1]};
    var seq = [
        pat0, pat0,
        pat0, pat0, pat1,
        pat0, pat0, pat1, pat0,
        pat0, pat0, pat1, pat0,
        pat0, pat0, pat1, pat0,
        pat0, pat1, pat0,
        pat0, pat1, pat0,
        pat0, pat1, pat0,
        pat0, pat0, pat1, pat0,
        pat0, pat0, pat1, pat0,
        pat0, pat1, pat0,
        pat0, pat1, pat0,
        pat0, pat1, pat0,
        pat0, pat1, pat0,
        pat0, pat1, pat0,
        pat0, pat1, pat0,
        pat0, pat1, pat0
    ];
    var pat = seq[(lc>>1) % seq.length];
    for(var i = 0; i < pat.p.length; ++i) {
        pluck.play(pat.p[i], {duration: .1, cutoff: .2 + pat.a[i] * .4});
        sleep(1/3);
    }
}, { name: 'pluck' }).connect(echo.create());

loop( (lc) => {
    var pat0 = {p:[c6,g5,ds5,g5,c5],d:[5,1,2,3,13]};
    var pat1 = {p:[as5,a5,g5,a5,d5],d:[5,1,2,3,13]};
    var pat2 = {p:[a5,g5,f5,c5],d:[2,1,2,7]};
    var patb0 = {p:[c6,g6,c6,g6,c6,g6],d:[1,1,1,1,1,7]};
    var patb1 = {p:[d6,as6,d6,as6,d6,as6],d:[1,1,1,1,1,7]};
    var patb2 = {p:[f6,c7,f6,c7,f6,c7],d:[1,1,1,1,1,7]};
    var seq = [
        0, 0,
        pat0, pat0, pat1,
        pat0, pat0, pat1, pat2, pat2,
        pat0, pat0, pat1, pat2, pat2,
        pat0, pat0, pat1, pat2, pat2,
        patb0, patb0, patb1, patb1, patb2, patb2,
        patb0, patb0, patb1, patb1, patb2, patb2,
        patb0, patb0, patb1, patb1, patb2, patb2,
        pat0, pat0, pat1, pat2, pat2,
        pat0, pat0, pat1, pat2, pat2,
        patb0, patb0, patb1, patb1, patb2, patb2,
        patb0, patb0, patb1, patb1, patb2, patb2,
        patb0, patb0, patb1, patb1, patb2, patb2,
        patb0, patb0, patb1, patb1, patb2, patb2,
        patb0, patb0, patb1, patb1, patb2, patb2,
        patb0, patb0, patb1, patb1, patb2, patb2,
        0, 0, 0,
    ];
    var pat = seq[lc % seq.length];
    if(pat) {
        for(var i = 0; i < pat.p.length; ++i) {
            synth1.play(pat.p[i]-12, {duration: pat.d[i]/3 - .1, release: .1});
            sleep(pat.d[i]/3);
        }
    }
    else
        sleep(8);
}, { name: 'synth1' }).connect(echo.create());

loop( (lc) => {
    var pat0 = {p:[ds5,d5,ds5,c5,g4],d:[1,1,1,2,7]};
    var pat1 = {p:[as4,a4,as4,g4,d4],d:[1,1,1,2,7]};
    var pat2 = {p:[a4,g4,f4,f4,c5],d:[1,1,1,2,7]};
    var pat3 = {p:[a4,g4,f4,a4,g4,f4,c5],d:[1,1,1,1,1,1,6]};
    var pat4 = {p:[ds5,d5,ds5,c5,g4,ds5,g4],d:[1,1,1,2,3,3,1]};
    var pat5 = {p:[ds5,d5,ds5,c5,g4,ds5,d5,c5],d:[1,1,1,2,3,1,2,1]};
    var pat6 = {p:[as4,a4,g4,as4,a4,g4,d5,g4,g4],d:[1,1,1,1,1,1,2,3,1]};
    var pat7 = {p:[as4,a4,g4,as4,d4,as4,a4,g4],d:[1,1,1,2,3,1,2,1]};
    var pat8 = {p:[a4,f4,a4,c5,a4,c5,f5,c5,f5,a5,f5],d:[2,2,2,2,2,2,2,2,2,2,4]};
    
    var pat9 = {p:[ds6,d6,ds6,c6,g5,ds6,g5],d:[1,1,1,2,3,3,1]};
    var pat10 = {p:[ds6,g5,d6,c6,g5],d:[2,1,2,3,4]};
    var pat11 = {p:[as5,a5,as5,g5,d5,as5,d5],d:[1,1,1,2,3,3,1]};
    var pat12 = {p:[as5,a5,g5,as5,a5,g5,d6,g5],d:[1,1,1,1,1,1,2,4]};
    var pat13 = {p:[a5,g5,f5,f5,c6,f5,f5],d:[1,1,1,2,3,3,1]};
    var pat14 = {p:[a5,g5,f5,c6,f5],d:[2,1,2,3,4]};
    var pat15 = {p:[d6,c6,as5,d6,c6,as5,d6,g5,g5],d:[1,1,1,1,1,1,2,3,1]};
    var pat16 = {p:[d6,c6,d6,as5,g5,d6,c6,as5],d:[1,1,1,2,3,1,2,1]};
    var pat17 = {p:[a5,g5,f5,c6,f5,f5],d:[2,1,2,3,3,1]};
    var pat18 = {p:[a5,g5,a5,f5,c6,f5],d:[1,1,1,2,3,4]};
    
    var pat19 = {p:[ds5,d5,c5,ds5,d5,c5,ds5,g4,g4],d:[1,1,1,1,1,1,2,3,1]};
    var pat20 = {p:[as4,a4,as4,g4,d4,as4,d4],d:[1,1,1,2,3,3,1]};
    var pat21 = {p:[d5,c5,as4,d5,c5,as4,d5,g4],d:[1,1,1,1,1,1,2,4]};
    var pat22 = {p:[a4,g4,f4,f4,c5,f4,f4],d:[1,1,1,2,3,3,1]};
    var pat23 = {p:[a4,g4,f4,a4,g4,f4,c5,f4],d:[1,1,1,1,1,1,2,4]};
    
    var pat24 = {p:[a4,g4,f4,c5],d:[2,2.5,1.56,6]};
    
    var seq = [
        0, 0,
        0, 0, 0,
        0, 0, 0, 0,
        0, 0, 0, 0,
        0, 0, 0, 0,
        0, 0, 0,
        pat0, pat0, pat1, pat1, pat2, pat3,
        pat0, pat0, pat1, pat1, pat2, pat24,
        0, 0, 0, 0,
        0, 0, 0, 0,
        0, 0, 0,
        pat0, pat0, pat1, pat1, pat2, pat3,
        pat4, pat5, pat6, pat7, pat8,
        pat9, pat10, pat11, pat12, pat13, pat14,
        pat9, pat10, pat15, pat16, pat17, pat18,
        pat4, pat19, pat20, pat21, pat22, pat23,
        pat9, pat10, pat15, pat16, pat17, pat18];
    var pat = seq[lc % seq.length];
    if(pat) {
        for(var i = 0; i < pat.p.length; ++i) {
            var r = Math.random() * .05;
            sleep(r);
            synth2.play(pat.p[i]-12, {duration: pat.d[i]/3 * .9, release: .1});
            sleep(pat.d[i]/3 - r);
        }
    }
    else
        sleep(8);
}, { name: 'synth2' }).connect(echo.create());