Oxygene4.js

The song that inspired every electronic musician.

Log in to post a comment.

// Oxygene Pt. 4 by Jean-Michel Jarre, 1976
// performed by 19KB of JS code
//
// by srtuss, 2022/12/25
// Drums are based around the circuits of the Korg Minipops 7, the 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) {
    let x = p%1;
    return (x - 0.5) * softclip(formant*x*(1-x));
}

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];
    }
});

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});
        }
    }
    __allpass(s, input, a) {
        var z = input - a * s.z;
        s.ap = s.z + a * z;
        s.z = z;
        return s.ap;
    }
    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 = this.__allpass(this.stages[0][i], vl, lfo);
            vr = this.__allpass(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 v = Math.sin(p * Math.PI * 2 * 2700) * .6 ** Math.max(p * 2750 - 2, 0) * Math.exp(-this.t * 10); // ringing at 2.7kHz
        v -= (Math.sin(p2 * Math.PI * 2 * 2700) * .6 ** Math.max(p2 * 2750 - 2, 0)) * .25 * Math.exp(-this.t * 20); // ringing at 2.7kHz
        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: 1860, amp: .3});
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 * Math.exp(-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 = 0;
        this.detune = opt.detune;
        this.cutoff = opt.cutoff;
        this.fa = opt.fa;
        this.fd = opt.fd;
    }
    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;
            }
        }
        this.tshimmer += ditty.dt * 10;
        var cutoff = 4 + Math.min(1, tick / this.fa) * Math.exp(-Math.max(0, tick - this.fa) * this.fd) * this.cutoff * 100;
        for(var i = 0; i < this.ops.length; ++i) {
            var op = this.ops[i];
            
            var fbase = midi_to_hz(note + op.po * this.detune) * ditty.dt;
            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.detune = opt.detune;
        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 = midi_to_hz(note + op.pitch * this.detune) * ditty.dt;
            // Math.min(1, tick) * (2 + 400 * Math.exp(-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:.005});
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});
const strings = synth.def(Analog, {nuni:4, attack: 1, release: 3, cutoff: .3, fa: .01, fd: 0, detune: .5, amp: .1});
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+Math.min(1, tick * 2) * .7 - tick * .4);
        return this.flt.process(Math.random() - .5) * env.value;
    }
}, {attack: .01, release: 8, cutoff: .3, amp: .12});

/*loop(() => {
    strings.play(c4, {duration:1});
    sleep(20);
}, {name: "instrument test"});*/


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(0,{duration:1, pan: -.8});
            sleep(1);
            noise.play(1,{duration:1, pan: .8});
            sleep(3);
        }
        else
            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:[c5,c5,c4,c5,c4,c5,c4,c5,c5,c4,c5,c4], a:[0,0,1,0,1,0,1,0,0,1,0,1]};
    var pat1 = {p:[d5,d5,d4,d5,d4,d5,d4,d5,d5,d4,d5,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());