// Wizards & Warriors Theme (NES, 1987) Commodore-64-style chiptune remix
// by srtuss
// 2023/03/04
// ------------------------------------------------------------------------------------------------------------------
// Here’s a Ditty synth that resembles the audio pipeline in the famous Commodore 64 SID (“sound interface device”)
// sound-chip MOS 6581. Simple waveforms (saw, pulse, triangle, colored noise), analog filter, and parameter updates
// at 50Hz. It is used for all instruments and drums. The real chip's polyphony is limited to 3 voices, so this Ditty
// "cheats" in that regard. On the real hardware the musician would be forced perform clever voice housekeeping and
// rapid toggling of instruments and notes per channel, at exact moments when it's the most inconspicuous, to give
// the impression of there being more than 3 channels. This necessity spawned innovative techniques, such as the
// playing of chords in a singular channel, by rapidly switching through the notes ("arpeggios").
// ------------------------------------------------------------------------------------------------------------------
// I invite you to use the SID class and make your own chiptunes!
//
// The original W&W music was composed by David Wise. Additional score by me (srtuss).
ditty.bpm = 120;
const fract = (x) => x - Math.floor(x);
const triangle01 = x => Math.abs(fract(x + .5) - .5) * 2;
const triangle11 = x => Math.abs(fract(x + .75) - .5) * 4 - 1;
const invlerp = (a, b, x) => clamp01((x-a) / (b-a));
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]; }}const echo = filter.def(class { constructor(options) { this.lastOutput = [0, 0]; var time = 60 / ditty.bpm; 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 * .4; this.lastOutput[1] = inv[1] + this.delay[1].lastOutput * .4; 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]; }});
class SID{
constructor(opt) {
this.f = midi_to_hz(opt.note);
this.p = 0;
this.noisev = [0, 0];
this.pw =.5;
this.tri = 1;
this.preflt = 1;
this.tupdate = 1;
this.mixsaw = 1;
this.mixpulse = 0;
this.mixnoise = 0;
this.mixtri = 0;
this.ptr = 0;
this.filter = [];
this.filtmode = 'lp';
this.kf = 1;
this.kq = 1;
this.flten = 0;
this.updaterate = 50;
for(var i = 0; i < 2; ++i)
this.filter.push({bp:0,lp:0,hp:0});
if(opt.bend===undefined)
opt.bend=0;
}
pblep(t, dt) {
if(t < dt) {
t /= dt;
return t + t - t * t - 1;
}
else if (t > 1 - dt) {
t = (t - 1) / dt;
return t * t + t + t + 1;
}
return 0;
}
setwf(c) {
this.mixsaw = c & 1;
this.mixtri = (c >> 1) & 1;
this.mixpulse = (c >> 2) & 1;
this.mixnoise = (c >> 3) & 1;
}
process(note, env, tick, opt) {
if(this.tupdate > 1) {
this.tupdate -= 1;
// Handle parameter updates, or advance instrument-table if provided:
const it = opt.it;
if(it) {
var l = it.length/4;
if(this.ptr < l) {
const pause = it[this.ptr*4];
const wf = it[this.ptr*4+1];
const pitch = it[this.ptr*4+2];
const pw = it[this.ptr*4+3];
if(pitch < 0)
this.f = -pitch;
else
this.f = midi_to_hz(opt.note + pitch);
this.setwf(wf);
this.pw = pw + .5;
if(pause == 1)
this.ptr++;
if(this.ptr >= l)
this.ptr = 0;
}
else {
if(opt.pulsew) {
this.pw = opt.pulsew;
}
}
}
else {
this.setwf(opt.wf);
if(opt.pulsew) {
this.pw = opt.pulsew;
}
}
if(opt.filtmode)
this.filtmode = opt.filtmode;
if(isFinite(opt.fc)) {
this.kf = clamp01(opt.fc);
this.flten = 1;
}
else
this.flten = 0;
if(isFinite(opt.fq))
this.kq = clamp(1 - opt.fq, .05, 1);
}
this.tupdate += ditty.dt*this.updaterate;
var dp = this.f * ditty.dt * (2**(opt.bend/12));
// Generate waveforms (band-limited):
var blep0 = this.pblep(this.p, dp*this.preflt);
var saw = this.p*2-1 - blep0;
var o = this.p - this.pw;
if(o < 0) o += 1;
var pulse = (this.p < this.pw ? -1 : 1) - blep0 + this.pblep(o, dp*this.preflt);
o = this.p - .5;
if(o < 0) o += 1;
var blep05 = this.pblep(o, dp*this.preflt);
var square = ((this.p < .5 ? -1 : 1) - blep0 + blep05);
this.tri = this.tri * .999 + square * 4 * dp;
// Generate colored noise (band-limited):
var noise = (this.p > .5 ? this.noisev[0] : this.noisev[1]) + blep05 * (this.noisev[0]-this.noisev[1]) * .5;
this.p += dp;
if(this.p >= 1) {
this.p -= 1;
this.noisev[1] = this.noisev[0];
this.noisev[0] = Math.random()*2-1;
}
// Mix waveforms:
// The real SID chip "mixes" waveforms using bitwise-AND, which is (mostly) impractical. This synth mixes through addition.
var oscmix = (pulse*this.mixpulse+saw*this.mixsaw+noise*this.mixnoise+this.tri*this.mixtri) * env.value;
// Run SVR Filter:
// The original SID filter has a 12dB/octave rolloff, this code does 24dB/octave.
if(this.flten) {
var fltHeadroom = .5;
var fltsig = oscmix * fltHeadroom;
var clip = (x) => x > 1 ? 1 : (x < -1 ? -1 : x);
for(var i = 0; i < 2; ++i) {
var f = this.filter[i];
f.hp = clip(fltsig - f.lp - f.bp * this.kq);
f.bp = clip(f.bp + f.hp * this.kf);
f.lp = clip(f.lp + f.bp * this.kf);
fltsig = f[this.filtmode];
f.hp = clip(fltsig - f.lp - f.bp * this.kq);
f.bp = clip(f.bp + f.hp * this.kf);
f.lp = clip(f.lp + f.bp * this.kf);
fltsig = f[this.filtmode];
}
return fltsig / fltHeadroom;
}
return oscmix;
}
}
// The drum kit...
// SID drums work by switching around frequencies and waveforms rapidly. This is done via the
// instrument-table option "it". The colums are
// <pause> <waveform-select-bits> <pitch or -frquency> <pulsewidth>.
const kick = synth.def(SID, { attack: 0.005, release: 0.165, duration: .08, amp: .6, it:[
1, 8, -12000, 0,
1, 4, -123, 0,
1, 4, -86, 0,
-1, 4, -50, 0]});
const snare = synth.def(SID, { attack: 0.005, release: 0.2, duration: .08, amp: .6, it:[
1, 8, -4700, 0,
1, 8, -18000, 0,
1, 2, -247, 0,
1, 4, -209, 0,
1, 8, -18000, 0,
-1, 8, -5300, 0]});
const hat = synth.def(SID, { attack: 0.001, release: 0.05, duration: .02, amp: .4, it:[
1, 8, -15000, 0,
-1, 8, -18000, 0]});
const lead = synth.def(SID, { attack: 0.005, release: 0.165, amp: .4, pan: .2, it:[
1, 4, 12, 0,
-1, 4, 0, .3], pulsew:(t,o)=>triangle01(ditty.tick*.1) * .4+.05, wf: 4});
const lead2 = synth.def(SID, { attack: 0.005, release: 0.165, amp: .3, pan: .2, it:[
1, 5, 0, 0,
1, 5, 0, 0,
-1, 2, 0, .3]});
const lead3 = synth.def(SID, { attack: 0.005, release: 0.165, amp: .6, pan: .2, wf: 2});
const bass = synth.def(SID, { attack: 0.005, release: 0.165, amp: .6, pan: -.2,
pulsew:(t,o)=>triangle01(t*.5+.2) * .4+.1, wf: 4,
fc:(t,o)=>Math.exp(t*-.1),
fq:.2,
});
const arp0 = synth.def(SID, { attack: 0.005, release: 0.165, amp: .2, pan: .2, it:[
1, 2, 0, 0,
1, 2, 7, 0,
1, 2, 12, 0,
1, 2, 15, 0]});
const arp1 = synth.def(SID, { attack: 0.005, release: 0.165, amp: .2, pan: -.2, it:[
1, 2, 0, 0,
1, 2, 12, 0,
1, 2, 16, 0,
1, 2, 19, 0]});
const arp2 = synth.def(SID, { attack: 0.005, release: 0.165, amp: .2, pan: .2, it:[
1, 2, 0, 0,
1, 2, 15, 0,
1, 2, 19, 0,
1, 2, 22, 0]});
const arp3 = synth.def(SID, { attack: 0.005, release: 0.165, amp: .2, pan: -.2, it:[
1, 2, 0, 0,
1, 2, 13, 0,
1, 2, 16, 0,
1, 2, 19, 0]});
loop( () => {
for(var i = 0; i < 4; ++i) {
arp0.play(a4+5, {duration: 1});
sleep(2);
}
arp1.play(f4+5, {duration: 1});
sleep(2);
arp2.play(d4+5, {duration: 1});
sleep(2);
arp3.play(e4+5, {duration: 1});
sleep(2);
arp3.play(e4+5, {duration: 1});
sleep(2);
}, { name: 'arp' });
/*loop( () => {
sleep(1);
hat.play();
sleep(1/4);
hat.play();
sleep(1/4);
hat.play();
sleep(2/4);
hat.play();
sleep(1/4);
hat.play();
sleep(2/4);
}, { name: 'hat' });*/
loop( (lc) => {
const pat1 = {
p:"aaabababababaaabab",
x:[8,2,1.5,.5,0,1,0,1,0,.5,0,.5,.25,.25,0,.25,0,.25],
d:[.417,.417,.417,.167,.417,.167,.417,.167,.417,.167,.417,.167,.167,.167,.167,.167,.167,.167],
};
const pat2 = {
tp:-97,
p:"accabccacacbccc",
x:[.25,.25,.25,.25,.25,.25,.25,.5,.25,0,.5,.25,.25,.25,.25],
};
const pat3 = {
p:"abababbababababab",
x:[.25,.5,.25,.75,0,.5,.25,.25,.25,0,.25,0,.25,0,.25,0,.25]
};
const seq = [, pat1, pat2, pat2, pat2, pat2, pat2, pat2, pat2, pat3][lc < 2 ? lc : (lc-2) % 8 + 2];
if(!seq) {
sleep(16);
return;
}
var kit = [kick, snare, hat];
for (let i=0; i < seq.p.length; i++) {
kit[seq.p.charCodeAt(i)-97].play();
sleep(seq.x[i]);
}
}, { name: 'drums', amp: 1.1 });
var gtp = 0;
loop( (lc) => {
var pat1 = porta({
p:[33,45,33,45,33,45,38,50,38,50,38,50,31,43,31,43,31,43,36,48,36,48,60,48,41,53,41,53,41,53,38,50,38,50,38,50,40,52,40,52,40,52,52,76,50,72,48,47,33,45,33,45,33,45,38,50,38,50,38,50,31,43,31,43,31,43,36,48,36,48,60,48,41,53,41,53,41,53,38,50,38,50,38,50,40,52,40,52,40,52,52,55,59,64],
x:[.5,.25,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.5,0,.5,0,.5,.5,.5,.25,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.5,.5,.5,.5],
d:[.458,.208,.208,.458,.208,.208,.208,.458,.208,.458,.208,.208,.208,.458,.208,.458,.208,.208,.208,.458,.208,.448,.458,.24,.208,.458,.208,.458,.208,.208,.208,.458,.208,.458,.208,.208,.198,.448,.198,.542,.219,.219,.458,.24,.458,.24,.458,.365,.458,.208,.208,.458,.208,.208,.208,.458,.208,.458,.208,.208,.208,.458,.208,.458,.208,.208,.208,.458,.208,.448,.458,.24,.208,.458,.208,.458,.208,.208,.208,.458,.208,.458,.208,.208,.198,.448,.198,.542,.219,.219,.458,.458,.458,.458]
});
var pat2 = porta({
p:[41,53,41,53,41,53,43,55,43,55,43,55,40,52,40,52,40,52,45,57,45,57,60,57,41,53,41,53,41,53,38,50,38,50,38,50,40,52,40,52,40,52,52,76,50,72,48,47,41,53,41,53,41,53,43,55,43,55,43,55,40,52,40,52,40,52,45,57,45,57,60,57,41,53,41,53,41,53,38,50,38,50,38,50,40,52,40,52,40,52,52,55,59,64],
x:[.5,.25,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.5,0,.5,0,.5,.5,.5,.25,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.25,.5,.25,.5,.25,.25,.5,.5,.5,.5],
d:[.458,.208,.208,.458,.208,.208,.208,.458,.208,.458,.208,.208,.208,.458,.208,.458,.208,.208,.208,.458,.208,.448,.458,.24,.208,.458,.208,.458,.208,.208,.208,.458,.208,.458,.208,.208,.198,.448,.198,.542,.219,.219,.458,.24,.458,.24,.458,.365,.458,.208,.208,.458,.208,.208,.208,.458,.208,.458,.208,.208,.208,.458,.208,.458,.208,.208,.208,.458,.208,.448,.458,.24,.208,.458,.208,.458,.208,.208,.208,.458,.208,.458,.208,.208,.198,.448,.198,.542,.219,.219,.458,.458,.458,.458]
});
var pat3 = porta({
p:[45,41,38,40,40,40,40,64,40,40,40,40,40,40,81],
x:[8,2,2,1,1,.25,.25,.25,.25,.125,.125,.125,.125,.094,.406],
d:[7.906,1.906,1.906,.906,.906,.167,.167,.167,.167,.083,.083,.083,.083,.333,.396],
});
var seq = [, pat3, pat1, pat2, pat1][lc < 2 ? lc : (lc-2) % 3 + 2];
if(!seq) {
sleep(16);
return;
}
for(let i=0; i < seq.p.length; i++) {
bass.play(seq.p[i]-7 + gtp, { duration: seq.d[i], release: .01, bend: bendf, prate:.2, bends:seq.bends[i]});
sleep(seq.x[i]);
}
}, { name: 'bass', amp: .9 });
loop( (lc) => {
var pat0 = porta({
tp:-32,
p:"jelemeoefcjcocmclhmhochcfaeamalajflfmfjfjclcmcjcleieleieqeieqiqe",
x:[.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25],
d:[.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.26],
m:[1,,,,,,,,1,,,,,1,,1,,,,,,,,,1,,,,,,,]
});
var pat1 = porta({
tp:-30,
p:"kchckmocodhpodhpmafajkmamcfokfjfkdhdhjkdkadahjkakcgcjcgcocgcogoc",
x:[.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25],
d:[.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.26],
m:[1,,,,,,,,1,,,,,1,,1,,,,,,,,,1,,,,,,,]
});
var pat2 = porta({
tp:-30,
p:"kchckmocodhpodhpmafajkmarcpcochckdhdhjkdkadahjkajoogoc",
x:[.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,1.75,1.25,.25,.25,.25,.25],
d:[.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,.25,2,1.208,.25,.25,.25,.198],
m:[1,,,,,,,,1,,,,,1,,1,,,,,,,,,1,,,,,,,]
});
const pat3 = porta({
tp:-30,
p:"hadhmkjfdckjhhhjkhjgjgogogo",
x:[1.5,.5,.5,.5,.5,.5,1.5,.5,.5,.5,.5,.5,1.5,.5,.5,.5,.5,.5,.5,.5,.5,.5,.5,.5,.25,.25,.5],
d:[1.396,.375,.5,.5,.5,.5,1.188,.5,.5,.5,.5,.5,1.375,.49,.5,.5,.5,.5,.75,.51,.375,.5,.5,.5,.25,.25,.25],
m:[,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,]
});
var seq = [pat0, pat0, pat0, pat0, pat1, pat2, pat3, pat3][lc < 2 ? lc : (lc-2) % 6 + 2];
var modulate = (t,o)=>bendf(t,o, Math.sin(t*Math.PI*2*6)*1*invlerp(.125,.2,t));
var vibrato = (t,o)=>bendf(t,o, Math.sin(t*Math.PI*6) * .5 * clamp01((t-.4) * 4));
for (let i=0; i < seq.p.length; i++) {
var pitch = seq.p[i];
/*(i%16<8?lead:lead2)*/lead.play(pitch + gtp, {
bend: seq.m[i] ? modulate : vibrato,
bends: seq.bends[i],
prate:.1,
duration: seq.d[i],
release: .01});
sleep(seq.x[i]);
}
}, { name: 'lead', amp: .9 }).connect(echo.create());
// The portamento generator.
var porta = (seq) => {
var oseq = {
x: [],
d: [],
p: [],
bends: [],
m: seq.m
};
seq.tp |= 0;
if(seq.p.charCodeAt) {
var p = new Array(seq.p.length);
for(var i = 0; i < seq.p.length; ++i) {
p[i] = seq.p.charCodeAt(i);
}
seq.p = p;
}
for(let i=0; i < seq.p.length; i++) {
var t0 = seq.x[i];
var t1 = seq.d[i];
var n = 0;
var bends = [];
var p = seq.p[i]+seq.tp;
var pstart = p;
for(let j = 1; t0 < t1 && j < seq.p.length && i+j < seq.p.length; ++j) { // if notes overlap
var k = (i+j)%seq.p.length;
t1 = Math.max(t1, t0 + seq.d[k]); // t1 is now the duration of the overlapping cluster
bends.push({t: t0, p: seq.p[k]+seq.tp-p});
t0 += seq.x[k]; // t0 is now the sleep time for the next note after the overlapping cluster
p = seq.p[k]+seq.tp;
++n;
}
i += n;
oseq.p.push(pstart);
oseq.d.push(t1);
oseq.x.push(t0);
oseq.bends.push(bends);
}
return oseq;
};
var bendf = (tick,opt,x) => {
var b = 0;
var bends = opt.bends;
var prate = opt.prate;
if(!bends.length)
return x || 0;
for(var i = 0; i < bends.length; ++i) {
var po = bends[i];
b += clamp01(tick_to_second(tick - po.t)/prate) * po.p;
}
return b;
};