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