Wizards & Warriors C64 Remix

👾

Log in to post a comment.

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