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