240 Bits per Mile

240 bits per mile, with struss' SID class, from Wizards & Warriors C64 Remix

Log in to post a comment.

// 240 Bits per Mile (Leon Riskin), from the Five Nights at Freddy's 6 soundtrack
// The SID class is from struss' Wizards & Warriors C64 remix: https://dittytoy.net/ditty/c35f3133c0

// === SID class (by struss) === 

// ------------------------------------------------------------------------------------------------------------------
// 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!

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
]});

// TODO: probably make these match the original closer
const lead = synth.def(SID, { attack: 0.005, release: 0.16, amp: 0.2, pan: -0.2, pulsew: (t,o)=>0.5, wf:4 });
const bass = synth.def(SID, { attack: 0.005, release: 0.165, amp: 0.4, pan: .2, pulsew:(t,o)=>0.9, wf: 1});
const pad = synth.def(SID, { attack: 0.005, release: 0.165, amp: 0.3, pan: -.2,
    pulsew:(t,o)=>triangle01(t*.5+.2) * .4+.1, wf: 1,
    fc:(t,o)=>Math.exp(t*-.1),
    fq:.2,
});

// === Song ===

// 240 bits per mile is indeed 240 bpm
ditty.bpm = 240;

// play the drums
function playDrums(kick, snare, duration, notes) {
    for (const note of notes) {
        // string encoding of the note
        // k = kick
        // s = snare
        // b = both
        // rest is ignored
        if (note == "k" || note == "b") kick.play(20);
        if (note == "s" || note == "b") snare.play(20);
        sleep(duration);
    }
}

// Alternate 3 notes
function alt(instr, count, a, b, low) {
    for (let i = 0; i < 4 * count; i++) { 
        instr.play([a, low, b, low].ring(i), { duration: 0.5 });
        sleep(0.5);
    }
}

// Alternate with a pattern
function altpat(instr, low, pat) {
    for (const note of pat) {
        instr.play(note, { duration: 0.5 });
        sleep(0.5);
        instr.play(low, { duration: 0.5 });
        sleep(0.5);
    }
}

// play a pattern
function pat(instr, time, pat) {
    for (const note of pat) {
        if (note > 0) instr.play(note, { duration: time });
        sleep(time);
    }
}

// play a pattern, merge notes
function patmerge(instr, time, pat) {
    const notes = [];
    for (const note of pat) {
        if (notes.length && notes[notes.length - 2] == note) notes[notes.length - 1] += time;
        else notes.push(note, time);
    }
    
    for (let i = 0; i < notes.length; i += 2) {
        if (notes[i] > 0) instr.play(notes[i], { duration: notes[i + 1] });
        sleep(notes[i + 1]);
    }
}

// intro base pattern
// 64 beats
function bass1(instr) {
    alt(instr, 4, ds4, c4, g3);
    alt(instr, 4, ds4, c4, gs3);
    alt(instr, 4, ds4, c4, f3);
    alt(instr, 2, ds4, c4, gs3);
    alt(instr, 2, f4, d4, as3);
    
    alt(instr, 4, g4, ds4, c4);
    alt(instr, 4, gs4, ds4, c4);
    alt(instr, 4, g4, ds4, as3);
    alt(instr, 4, g4, d4, b3);
}

// 64 beats
function bass2(instr) {
    alt(instr, 4, gs4, ds4, c4);
    alt(instr, 4, gs4, f4, c4);
    alt(instr, 4, g4, ds4, c4);
    alt(instr, 4, g4, ds4, as3);
    
    alt(instr, 4, gs4, ds4, c4);
    alt(instr, 4, gs4, f4, c4);
    
    // join the crazy bit
    patmerge(instr, 1/3, [a4, fs4, d4, a4, fs4, d4, a4, fs4, d4, a4, fs4, d4]);
    patmerge(instr, 1/3, [a4, fs4, d4, a4, fs4, d4, a4, fs4, d4, a4, fs4, d4]);
    
    // even crazier
    patmerge(instr, 0.25, [b4, g4, d4, b3, g3, d3, c3, d3, g3, b3, d4, g4, b4, d5]);
    patmerge(instr, 0.25, [b4, g4, d4, b3, g3, d3, c3, d3, g3, b3, d4, g4, b4, d5]);
    patmerge(instr, 0.25, [b4, g4, d4, b3]);
}

// 64 beats
function bass3(instr) {
    alt(instr, 4, ds4, c4, g3);
    alt(instr, 4, ds4, c4, gs3);
    alt(instr, 4, ds4, c4, a3);
    alt(instr, 2, ds4, c4, gs3);
    sleep(0.5);
    pat(instr, 0.5, [c4, ds4, f4, fs4, f4, ds4, f4]);
    
    
    alt(instr, 4, ds4, c4, g3);
    alt(instr, 4, ds4, c4, gs3);
    alt(instr, 4, ds4, c4, f3);
    alt(instr, 2, ds4, c4, gs3);
    alt(instr, 2, f4, d4, as3);
}

// 64 beats
function bass4(instr) {
    alt(instr, 4, ds4, c4, g3);
    alt(instr, 2, ds4, c4, gs3);
    alt(instr, 2, d4, b3, g3);
    alt(instr, 4, ds4, c4, g3);
    alt(instr, 2, ds4, c4, gs3);
    alt(instr, 2, d4, b3, g3);
    
    alt(instr, 4, ds4, c4, g3);
    alt(instr, 2, ds4, c4, gs3);
    alt(instr, 2, d4, b3, g3);
    alt(instr, 4, ds4, c4, g3);
    alt(instr, 2, ds4, c4, gs3);
    alt(instr, 2, d4, b3, g3);
}

// 64 beats
function lead1(instr) {
    altpat(instr, g4, [c5, c5, d5, ds5, c5, c5, d5, ds5]);
    altpat(instr, gs4, [c5, c5, d5, ds5, c5, c5, d5, ds5]);
    altpat(instr, a4, [c5, c5, d5, ds5, c5, c5, d5, ds5]);
    altpat(instr, gs4, [c5, c5, d5, ds5]);
    altpat(instr, as4, [d5, d5, ds5, f5]);
    
    patmerge(instr, 0.5, [c5, 0, c6, 0, as5, 0, g5, 0, f5, f5, ds5, 0, d5, ds5, 0, d5, d5, d5]);
    patmerge(instr, 0.5, [c5, 0, d5, 0, ds5, gs5, gs5, gs5, c5, c5, d5, 0, ds5, 0]);
    patmerge(instr, 1, [g5, as4, ds5, g5, gs5]);
    patmerge(instr, 0.5, [g5, f5, f5, ds5, ds5, d5, d5, d5]);
    patmerge(instr, 0.5, [b4, b4, d5, b4, f5, b4, g5, b4, f5, b4, ds5, b4, d5, b4]);
}

// 64 beats
function lead2(instr) {
    patmerge(instr, 0.5, [c5, 0, c5, c5, d5, ds5, ds5, c5, 0, c5, 0, c5, d5, d5, ds5, ds5]);
    patmerge(instr, 0.5, [c5, 0, c5, c5, d5, ds5, ds5, c5, 0, c5, 0, c5, ds5, ds5, d5, d5]);
    
    patmerge(instr, 0.5, [c5, 0, c5, c5, d5, ds5, ds5, c5, 0, c5, 0, c5, d5, d5, ds5, ds5]);
    patmerge(instr, 0.5, [g5, g5, as4, as4, ds5, g5, g5, gs5, gs5, g5, g5, f5, f5, ds5, ds5, 0]);
    
    patmerge(instr, 0.5, [gs5, gs5, c5, 0, gs5, gs5, c5, 0, as5, as5, gs5, g5, 0, f5, f5, gs5]);
    patmerge(instr, 0.5, [0, gs5, c5, 0, gs5, gs5, c5, 0, as5, as5, gs5, g5, 0, f5, f5, f5]);
    
    // the wild one
    // 24 * 1/3 = 8 beats 
    patmerge(instr, 1/3, [fs5, fs5, d5, a5, fs5, d5, as5, fs5, d5, c6, fs5, d5, as5, fs5, d5, a5, fs5, d5, g5, fs5, d5, fs5, fs5, d5]);
    
    // the even wilder bit
    patmerge(instr, 0.25, [g5, d5, b4, g4, d4, b3, g3, b3, d4, g4, b4, d5, g5, b5]);
    patmerge(instr, 0.25, [g5, d5, b4, g4, d4, b3, g3, b3, d4, g4, b4, d5, g5, b5]);
    patmerge(instr, 0.25, [g5, d5, b4, g4]);
}

// 64 beats
function lead3(instr) {
    pat(instr, 0.5, [ds5, g4, c5, g4, ds5, g4, c5, g4, ds5, g4, c5, ds5, g4, f5, g4, ds5]);
    pat(instr, 0.5, [gs4, ds5, c5, gs4, ds5, gs4, c5, gs4, ds5, gs4, c5, ds5, gs4, f5, gs4, ds5]);
    pat(instr, 0.5, [a4, ds5, c5, a4, ds5, a4, c5, a4, ds5, a4, c5, ds5, a4, f5, a4, ds5]);
    pat(instr, 0.5, [gs4, ds5, c5, gs4, ds5, gs4, c5, ds5, 0, c5, ds5, f5, fs5, f5, ds5, f5]);
 
    pat(instr, 0.5, [c5, g4, c5, g4, d5, ds5, g4, g5, g4, c5, g4, c5, d5, g4, ds5, g4]);
    pat(instr, 0.5, [gs5, gs4, c5, gs4, d5, ds5, gs4, gs5, gs4, c5, gs4, c5, d5, gs4, ds5, gs4]);
    pat(instr, 0.5, [a5, a4, c5, a4, d5, ds5, a4, a5, a4, c5, a4, c5, d5, a4, ds5, a4]);
    pat(instr, 0.5, [gs5, gs4, ds5, gs4, c5, gs5, gs4, g5, as4, f5, as4, f5, ds5, as4, d5, as4]);
}

// 64 beats
function lead4(instr) {
    pat(instr, 0.5, [0, g4]); // 1
    pat(instr, 1/3, [d5, ds5, d5, c5, g4, ds4, g4, c5, ds5]); // 3 -- 4
    pat(instr, 0.25, [g5, f5, ds5, d5, c5, d5]); // 1.5 -- 5.5
    pat(instr, 0.5, [c5]); // 0.5 -- 6
    pat(instr, 0.25, [g5, ds5, c5, g4]); // 1 -- 7
    pat(instr, 0.5, [0, g4]); // 1 -- 8
    
    pat(instr, 1/3, [gs5, ds5, c5, gs5, ds5, c5, gs5, ds5, c5, as5, ds5, c5]); // 12
    pat(instr, 1/3, [g5, d5, b4, f5, d5, b4, ds5, d5, b4, bs4, b4, g4]); // + 12 * 1/3 = 8
    
    pat(instr, 1/3, [c5, g5, ds5, g5, ds5, c5, ds5, c5, g4, c5, g4, ds4]); // 12
    pat(instr, 1/3, [c4, ds4, g4, c5, ds5, g5, c6, ds6, g6, ds6, c6, ds6]); // + 12 * 1/3 = 8
    
    pat(instr, 0.25, [g5, 0]); // 0.5
    patmerge(instr, 0.5, [g5, f5, 0, ds5, f5, 0, ds5, ds5, ds5]); // 4.5 -- 5
    patmerge(instr, 0.25, [d5, 0, d5, d5, c5, 0, c5, c5, b4, 0, b4, b4]); // 3 -- 8
    
    patmerge(instr, 0.25, [c5, c5, c5, c5, g5, 0, g5, g5, f5, 0, f5, f5, ds5, ds5]); // 3.5 -- 3.5
    patmerge(instr, 0.25, [c5, c5, c5, 0, c5, c5, g5, 0, g5, g5, f5, 0, f5, f5, ds5, 0, ds5, ds5]); // 4.5 -- 8 
    
    pat(instr, 1, [c5]); // 1
    patmerge(instr, 0.25, [g5, 0, g5, g5, f5, 0, f5, f5, ds5, ds5, c5, c5, c5, 0, c5, c5]); // 4 -- 5
    patmerge(instr, 0.25, [g5, 0, g5, g5, f5, 0, f5, f5, ds5, 0, ds5, ds5]); // 3 -- 8
    
    pat(instr, 1, [c5]); // 1
    patmerge(instr, 0.25, [g5, 0, g5, g5, f5, 0, f5, f5, ds5, ds5, c5, c5, c5, 0, c5, c5]); // 4 -- 5
    patmerge(instr, 0.25, [g5, 0, g5, g5, f5, 0, f5, f5, ds5, 0, ds5, ds5]); // 3 -- 8
    
    pat(instr, 1, [c5]); // 1
    patmerge(instr, 0.25, [g5, 0, g5, g5, f5, 0, f5, f5, ds5, ds5]); // 2.5 -- 3.5
    pat(instr, 4.5, [c5]); // 4.5 -- 8
}

// 64 beats
function pad1(instr) {
    pat(instr, 8, [c3, gs2, f2]);
    pat(instr, 4, [gs2, as2]);
    pat(instr, 8, [c3, gs3, ds3, g2]);
}

// 64 beats
function pad2(instr) {
    pat(instr, 2, [gs2, gs3, ds3, c3]);
    pat(instr, 2, [f3, c3, gs3, f3]);
    pat(instr, 2, [c3, c4, g3, ds3]);
    pat(instr, 2, [ds3, ds4, as3, g3]);
    
    pat(instr, 2, [gs2, gs3, gs2, gs3]);
    pat(instr, 2, [f2, f3, f2, f3]);
    pat(instr, 2, [fs2, fs3]);
    pat(instr, 1, [fs2, a2, d3, fs3]);
    pat(instr, 8, [g3]);
}

// 64 beats
function pad3(instr) {
    pat(instr, 8, [c3, gs2, f2]);
    pat(instr, 4, [gs2]);
    pat(instr, 0.5, [as2, c3, ds3, f3, fs3, f3, ds3, f3]);
    pat(instr, 8, [c3, gs2, f2]);
    pat(instr, 4, [gs2, as2]);
}

// 64 beats
function pad4(instr) {
    pat(instr, 8, [c3]);
    pat(instr, 4, [gs2, g2]);
    pat(instr, 8, [c3]);
    pat(instr, 4, [gs2, g2]);
    
    pat(instr, 8, [c3]);
    pat(instr, 4, [gs2, g2]);
    pat(instr, 8, [c3]);
    pat(instr, 4, [gs2, g2]);
}

// This *seems* to be the same drumbeat throughout the entire song
// 32 beats
function drums1(kick, snare) {
    playDrums(kick, snare, 0.5, "k-s-k-s-k-sk-ks-");
    playDrums(kick, snare, 0.5, "k-s-k-s-k-sk-sss");
    playDrums(kick, snare, 0.5, "k-s-k-s-k-sk-kss");
    playDrums(kick, snare, 0.5, "k-s-k-s-k-sk-ks-");
}

// === Loops ===

loop(() => {
    sleep(64);
    lead1(lead);
    lead2(lead);
    lead3(lead);
    lead4(lead);
}, { name: "Lead" });

loop(() => {
    bass1(bass);
    bass1(bass);
    bass2(bass);
    bass3(bass);
    bass4(bass);
}, { name: "Bass" });

loop(() => {
    pad1(pad);
    pad1(pad);
    pad2(pad);
    pad3(pad);
    pad4(pad);
}, { name: "Pad" });

loop(() => {
    drums1(kick, snare);
}, { name: "Drums" });

// Animate the car
const road = "⬛⬛⬛⬛⬛⬛⬛⬛⬛";
const top = "⬛⬛⬛🟪⬛🟪⬛⬛⬛";
const bottom = "⬛⬛⬛🟪⬛🟪⬛⬛⬛";
const frames = [
    "⬜⬜⬛🟪🟪🟪🟪⬛⬛",
    "⬜⬛⬛🟪🟪🟪🟪⬛⬜",
    "⬛⬛⬛🟪🟪🟪🟪⬜⬜",
    "⬛⬛⬛🟪🟪🟪🟪⬜⬛",
    "⬛⬛⬛🟪🟪🟪🟪⬛⬛",
    "⬛⬛⬛🟪🟪🟪🟪⬛⬛",
    "⬛⬛⬛🟪🟪🟪🟪⬛⬛",
    "⬛⬛⬜🟪🟪🟪🟪⬛⬛",
    "⬛⬜⬜🟪🟪🟪🟪⬛⬛",
];
    
// play it back with an instrument
// bit jank but it works
const anim = synth.def(_ => {
    debug.log(1, road);
    debug.log(2, top);
    debug.log(3, frames.ring(ditty.tick * 4));
    debug.log(4, bottom);
    debug.log(5, road);
    return 0;
});

loop(() => {
    // Bit jank, not sure how to make this work without playing an instrument
    for (let i = 0; i < 5; i++) {
        anim.play(0, { duration: 1 });
        sleep(1);
    }
}, { name: "Car" });