Back to Supersonic RC Revive
SOURCE / PINNED RELEASE

Made of little things.

Supersonic RC Revive

Release
1ba42f1ca1d6…
Author-recorded commit
baecad10b1cd…
License
LICENSE
Author’s source reference
nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.napplet.soy%2F/n-143146b0d6f

Archive hash verified: 90d22b206672eba4…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/game/synth.ts
// Procedural stand-ins for every sound the game plays, rendered sample by
// sample into AudioBuffers at load time. Used when the bundle has no sound
// files (the custom bundle). Names match the original cast members, and the
// engine samples keep their roles: CarStart revs up into CarLoop's pitch,
// CarLoop loops seamlessly (whole cycles only), CarEnd winds down.

const RATE = 22050;

/** Deterministic noise (mulberry32). */
function noise(seed: number): () => number {
  let a = seed >>> 0;
  return () => {
    a = (a + 0x6d2b79f5) >>> 0;
    let t = a;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return (((t ^ (t >>> 14)) >>> 0) / 4294967296) * 2 - 1;
  };
}

type Render = (out: Float32Array) => void;

const TAU = Math.PI * 2;
const tri = (p: number) => 1 - 4 * Math.abs((p % 1) - 0.5);
const square = (p: number, duty = 0.5) => ((p % 1) < duty ? 1 : -1);
const saw = (p: number) => 2 * (p % 1) - 1;
const midi = (n: number) => 440 * Math.pow(2, (n - 69) / 12);

/** One-pole low-pass, in place. */
function lowpass(x: Float32Array, cutoff: number): void {
  const k = 1 - Math.exp((-TAU * cutoff) / RATE);
  let y = 0;
  for (let i = 0; i < x.length; i++) x[i] = y += k * (x[i] - y);
}

// --- engine ----------------------------------------------------------------------------

const IDLE_HZ = 70;
const LOOP_HZ = 110;

/** An electric RC motor: a buzzy whine with a gear-mesh overtone. Phase is integrated. */
function motor(freq: (t: number) => number, gain: (t: number) => number): Render {
  return (out) => {
    let phase = 0;
    for (let i = 0; i < out.length; i++) {
      const t = i / RATE;
      phase += freq(t) / RATE;
      const v = 0.5 * saw(phase) + 0.3 * square(phase * 2, 0.3) + 0.25 * Math.sin(TAU * phase * 6.5);
      out[i] = v * gain(t) * 0.5;
    }
    lowpass(out, 2600);
  };
}

const carStart: [number, Render] = [2.0, motor((t) => IDLE_HZ + (LOOP_HZ - IDLE_HZ) * Math.min(1, t / 1.8) ** 0.7, (t) => Math.min(1, t * 8))];
// 1.0 s × 110 Hz: whole cycles, so the loop point is seamless (the overtone at
// 6.5× completes 715 cycles too). The low-pass settles before the end.
const carLoop: [number, Render] = [
  1.0,
  (out) => {
    const one = new Float32Array(out.length * 2);
    motor(() => LOOP_HZ, () => 1)(one);
    out.set(one.subarray(out.length));
  },
];
const carEnd: [number, Render] = [1.0, motor((t) => LOOP_HZ - (LOOP_HZ - 40) * Math.min(1, t) ** 0.6, (t) => Math.max(0, 1 - t) ** 1.5)];

// --- one-shots ---------------------------------------------------------------------------

/** Servo/tyre chirp when the steering flicks. */
function turn(seed: number, up: boolean): [number, Render] {
  const n = noise(seed);
  return [
    0.12,
    (out) => {
      let phase = 0;
      for (let i = 0; i < out.length; i++) {
        const t = i / out.length;
        phase += (up ? 900 + 500 * t : 1400 - 500 * t) / RATE;
        out[i] = (0.5 * tri(phase) + 0.15 * n()) * Math.sin(Math.PI * t) * 0.6;
      }
    },
  ];
}

/** Plastic-on-wood knock: a pitched thump and a burst of filtered noise. */
function crash(seed: number, glass: boolean): [number, Render] {
  const n = noise(seed);
  const pitch = 80 + ((seed * 37) % 60);
  const length = glass ? 0.45 : 0.3 + ((seed * 13) % 10) / 50;
  return [
    length,
    (out) => {
      const body = new Float32Array(out.length);
      for (let i = 0; i < out.length; i++) body[i] = n();
      lowpass(body, glass ? 5000 : 1400);
      let phase = 0;
      for (let i = 0; i < out.length; i++) {
        const t = i / RATE;
        phase += pitch * (1 - t) / RATE;
        let v = Math.sin(TAU * phase) * Math.exp(-t * 18) + body[i] * Math.exp(-t * (glass ? 9 : 14)) * 1.4;
        // Plastic rattles: a few bright partials ringing out.
        if (glass) v += 0.25 * (Math.sin(TAU * 2310 * t) + Math.sin(TAU * 3170 * t)) * Math.exp(-t * 12);
        out[i] = v * 0.7;
      }
    },
  ];
}

interface Note {
  /** MIDI note, or null for a rest. */
  n: number | null;
  /** Length in beats. */
  len: number;
}

/** A short melodic jingle on a square lead with a triangle under it. */
function jingle(notes: Note[], bpm: number, tail = 0.25): [number, Render] {
  const beat = 60 / bpm;
  const total = notes.reduce((s, x) => s + x.len, 0) * beat + tail;
  return [
    total,
    (out) => {
      let at = 0;
      for (const { n, len } of notes) {
        const start = Math.floor(at * RATE);
        const dur = len * beat;
        at += dur;
        if (n === null) continue;
        const f = midi(n);
        const end = Math.min(out.length, start + Math.floor((dur + tail) * RATE));
        for (let i = start; i < end; i++) {
          const t = (i - start) / RATE;
          const env = Math.min(1, t * 200) * Math.exp(-t * 3.5) * (t < dur ? 1 : Math.max(0, 1 - (t - dur) / tail));
          out[i] += (0.32 * square(f * t, 0.25) + 0.3 * tri(f * 0.5 * t)) * env;
        }
      }
    },
  ];
}

const N = (n: number | null, len = 0.5): Note => ({ n, len });

function alarm(): [number, Render] {
  return [
    0.6,
    (out) => {
      for (let i = 0; i < out.length; i++) {
        const t = i / RATE;
        const on = t % 0.2 < 0.12 ? 1 : 0;
        out[i] = 0.35 * square(t * (t % 0.4 < 0.2 ? 1320 : 990)) * on;
      }
      lowpass(out, 4000);
    },
  ];
}

function click(): [number, Render] {
  return [
    0.04,
    (out) => {
      for (let i = 0; i < out.length; i++) {
        const t = i / RATE;
        out[i] = 0.6 * Math.sin(TAU * 1800 * t) * Math.exp(-t * 160);
      }
    },
  ];
}

// --- music -------------------------------------------------------------------------------

const BPM = 132;
const STEP = 60 / BPM / 4; // sixteenth notes

/** Chord roots for each bar, all diatonic to C major. */
const PROGRESSIONS = [
  [57, 53, 48, 55], // Am F C G
  [48, 55, 57, 53], // C G Am F
  [53, 55, 48, 48], // F G C C
  [57, 55, 53, 52], // Am G F E
  [50, 53, 48, 55], // Dm F C G
  [48, 53, 55, 55], // C F G G (splash)
];
const SCALE = [0, 2, 4, 5, 7, 9, 11];

/**
 * Four bars of chiptune: kick/snare/hat, a pumping bass on the chord root and
 * a seeded lead that sticks to chord tones on strong beats. Every loop shares
 * tempo and key, so back-to-back loops in any order stay in time.
 */
function musicLoop(index: number): [number, Render] {
  const bars = 4;
  const steps = bars * 16;
  const length = steps * STEP;
  return [
    length,
    (out) => {
      const rnd = noise(1000 + index * 77);
      const hat = noise(5 + index);
      const roots = PROGRESSIONS[index];
      // Lead line: one note per eighth, some rests.
      const lead: Array<number | null> = [];
      let degree = 4;
      for (let s = 0; s < steps / 2; s++) {
        const strong = s % 4 === 0;
        if (!strong && rnd() < 0.25) {
          lead.push(null);
          continue;
        }
        degree += Math.round((rnd() * 2 - 1) * 2);
        degree = Math.max(0, Math.min(9, degree));
        if (strong) degree -= degree % 2; // land on chord-ish tones
        // Diatonic to C major from C5: fits every chord in the progressions.
        lead.push(72 + SCALE[degree % 7] + Math.floor(degree / 7) * 12);
      }
      for (let i = 0; i < out.length; i++) {
        const t = i / RATE;
        const step = Math.floor(t / STEP);
        const st = t - step * STEP;
        const inBar = step % 16;
        const root = roots[Math.floor(step / 16) % bars];
        let v = 0;
        // Kick on the beat.
        if (inBar % 4 === 0) {
          const ft = 50 + 120 * Math.exp(-st * 40);
          v += 0.55 * Math.sin(TAU * ft * st) * Math.exp(-st * 14);
        }
        // Snare on 2 and 4.
        if (inBar === 4 || inBar === 12) v += 0.28 * hat() * Math.exp(-st * 22);
        // Hats on off-eighths.
        if (inBar % 2 === 1) v += 0.07 * hat() * Math.exp(-st * 70);
        // Bass: eighth-note pulse on the root, octave jump on the "and".
        const bassNote = root - 12 + (inBar % 4 === 2 ? 12 : 0);
        const bt = t % (STEP * 2);
        v += 0.22 * tri(midi(bassNote) * t) * Math.exp(-bt * 6);
        // Lead.
        const ln = lead[Math.floor(step / 2)];
        if (ln !== null && ln !== undefined) {
          const lt = t % (STEP * 2);
          v += 0.12 * square(midi(ln) * t, 0.25) * Math.min(1, lt * 300) * Math.exp(-lt * 5);
        }
        out[i] = v * 0.8;
      }
    },
  ];
}

const RECIPES: Record<string, () => [number, Render]> = {
  carstart: () => carStart,
  carloop: () => carLoop,
  carend: () => carEnd,
  carturnleft01: () => turn(1, true),
  carturnleft02: () => turn(2, true),
  carturnleft03: () => turn(3, true),
  carturnright01: () => turn(4, false),
  carturnright02: () => turn(5, false),
  carturnright03: () => turn(6, false),
  ...Object.fromEntries([1, 2, 3, 4, 5, 6, 7, 8].map((i) => [`crash${i}`, () => crash(i, false)])),
  crash1_glass: () => crash(11, true),
  crash2_glass: () => crash(12, true),
  crash3_glass: () => crash(13, true),
  alarm,
  buttonclick: click,
  stingcheckpoint: () => jingle([N(72, 0.25), N(76, 0.25), N(79, 0.25), N(84, 0.75)], 160),
  stinghiddentreasure: () => jingle([N(84, 0.25), N(88, 0.25), N(91, 0.25), N(96, 0.25), N(91, 0.25), N(96, 0.75)], 200),
  stingwin: () => jingle([N(67), N(72), N(76), N(79, 1), N(76), N(79, 2)], 140, 0.5),
  stingloseloop: () => jingle([N(72), N(71), N(70), N(69, 2)], 110, 0.5),
  ssrmusicsplashscreen: () => musicLoop(5),
  musicloop1: () => musicLoop(0),
  musicloop2: () => musicLoop(1),
  musicloop3: () => musicLoop(2),
  musicloop4: () => musicLoop(3),
  musicloop5: () => musicLoop(4),
};

/** Render the stand-in for a sound name (case-insensitive), or undefined if there is none. */
export function synthesize(context: BaseAudioContext, name: string): AudioBuffer | undefined {
  const recipe = RECIPES[name.toLowerCase()];
  if (!recipe) return undefined;
  const [seconds, render] = recipe();
  const buffer = context.createBuffer(1, Math.max(1, Math.round(seconds * RATE)), RATE);
  const data = new Float32Array(buffer.length);
  render(data);
  for (let i = 0; i < data.length; i++) data[i] = Math.max(-1, Math.min(1, data[i]));
  buffer.copyToChannel(data, 0);
  return buffer;
}