// 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; };