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