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