SOURCE / PINNED RELEASE
Made of little things.
Big Rig Rush
- Release
- ec9b6549f752…
- Author-recorded commit
- 74b729258b1a…
- License
- LICENSE
- Author’s source reference
- nostr://npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt/wss%3A%2F%2Fgit.napplet.soy%2F/n-0a99f0cae40
Archive hash verified: 900e22810340ccb1…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Sample-by-sample crash synthesis. Every crash is rendered fresh into a stereo
// buffer, so no two sound alike, and the amount of chaos follows the impact:
//
// tap a dull thump, a crack of plastic, a little crunch
// solid hit crumpling metal with ringing body panels, glass, a short scrape
// high speed several overlapping impacts as the car rolls, long metal-on-asphalt
// scrapes, tire squeal, a storm of glass and bouncing debris, a hubcap
//
// The metal sound is modal: a dense stream of noise grains (the crumple) excites a
// bank of narrow resonators tuned to random panel frequencies, which is what makes
// it ring like a car body instead of hiss like static.
export type ImpactKind = 'car' | 'wall' | 'pileup';
export interface CrashRender {
left: Float32Array<ArrayBuffer>;
right: Float32Array<ArrayBuffer>;
/** 0 (tap) to ~1.25 (carnage), derived from the game's impact intensity. */
severity: number;
}
const TAU = Math.PI * 2;
function clamp(v: number, lo: number, hi: number): number {
return v < lo ? lo : v > hi ? hi : v;
}
function rnd(lo: number, hi: number): number {
return lo + Math.random() * (hi - lo);
}
/** Log-uniform: even spread across octaves, useful for frequencies and amplitudes. */
function logRnd(lo: number, hi: number): number {
return lo * Math.pow(hi / lo, Math.random());
}
// A shared table of white noise is much cheaper than Math.random() per sample.
// Each read walks on from a random point, so repeats are never audible.
const NOISE_SIZE = 1 << 16;
const NOISE_TABLE = new Float32Array(NOISE_SIZE).map(() => Math.random() * 2 - 1);
let noiseAt = 0;
function noise(): number {
noiseAt = (noiseAt + 1) & (NOISE_SIZE - 1);
return NOISE_TABLE[noiseAt];
}
function reseedNoise(): void {
noiseAt = Math.floor(Math.random() * NOISE_SIZE);
}
/** Exponentially distributed delay: most events early, a long tail of stragglers. */
function expDelay(mean: number): number {
return -mean * Math.log(1 - Math.random() * 0.999);
}
class Canvas {
readonly left: Float32Array<ArrayBuffer>;
readonly right: Float32Array<ArrayBuffer>;
/** Mono excitation for the metal resonator bank. */
readonly metal: Float32Array<ArrayBuffer>;
readonly n: number;
metalEnd = 0;
constructor(
readonly sr: number,
seconds: number,
readonly width: number,
) {
this.n = Math.ceil(seconds * sr);
this.left = new Float32Array(new ArrayBuffer(this.n * 4));
this.right = new Float32Array(new ArrayBuffer(this.n * 4));
this.metal = new Float32Array(new ArrayBuffer(this.n * 4));
}
idx(t: number): number {
return Math.floor(t * this.sr);
}
/** A random stereo position, wider for bigger crashes. */
spread(): number {
return noise() * this.width;
}
/** Damped sinusoid with an optional inharmonic second partial: glass, debris, hubcap ticks. */
ping(t: number, freq: number, decay: number, amp: number, pan: number, partial = 0): void {
const sr = this.sr;
const start = this.idx(t);
// Stop at -40 dB.
const len = Math.min(this.n - start, Math.ceil(decay * 4.6 * sr));
if (start < 0 || len <= 0) return;
const gl = Math.sqrt((1 - pan) / 2) * amp;
const gr = Math.sqrt((1 + pan) / 2) * amp;
// Recursive oscillators: y[n] = 2r·cos(w)·y[n-1] - r²·y[n-2] rings and decays with no sin() per sample.
const r = Math.exp(-1 / (decay * sr));
const w1 = (TAU * Math.min(freq, sr * 0.45)) / sr;
const c1 = 2 * r * Math.cos(w1);
const ph = Math.random() * TAU;
let a1 = Math.sin(ph);
let a0 = r * Math.sin(ph + w1);
let p1 = 0;
let p0 = 0;
let c2 = 0;
const mix = partial ? 0.7 : 1;
if (partial) {
const w2 = (TAU * Math.min(freq * partial, sr * 0.45)) / sr;
c2 = 2 * r * Math.cos(w2);
p0 = r * Math.sin(w2);
}
const rr = r * r;
for (let i = 0; i < len; i++) {
const v = a1 * mix + p1 * 0.3;
const a = c1 * a0 - rr * a1;
a1 = a0;
a0 = a;
if (partial) {
const p = c2 * p0 - rr * p1;
p1 = p0;
p0 = p;
}
this.left[start + i] += v * gl;
this.right[start + i] += v * gr;
}
}
/** Short noise burst straight to the stereo mix. tone 0 = dark, 1 = bright. */
grit(t: number, length: number, amp: number, pan: number, tone: number): void {
const start = this.idx(t);
const len = Math.min(this.n - start, Math.ceil(length * this.sr));
if (start < 0 || len <= 0) return;
const gl = Math.sqrt((1 - pan) / 2) * amp;
const gr = Math.sqrt((1 + pan) / 2) * amp;
const a = 1 - tone * 0.95;
let lp = 0;
let prev = 0;
for (let i = 0; i < len; i++) {
const x = noise();
lp += (x - lp) * (1 - a);
// Blend lowpassed and differentiated noise for the requested brightness.
const v = (tone > 0.5 ? x - prev : lp) * Math.exp((-6 * i) / len);
prev = x;
this.left[start + i] += v * gl;
this.right[start + i] += v * gr;
}
}
/** Noise grain into the metal excitation (rings through the panel resonators). */
crumpleGrain(t: number, length: number, amp: number): void {
const start = this.idx(t);
const len = Math.min(this.n - start, Math.ceil(length * this.sr));
if (start < 0 || len <= 0) return;
const k = Math.exp(-5 / len);
let env = amp;
for (let i = 0; i < len; i++) {
this.metal[start + i] += noise() * env;
env *= k;
}
this.metalEnd = Math.max(this.metalEnd, start + len);
}
/** Low-frequency body blow: a falling sine plus a darkened noise thud. */
thump(t: number, freq: number, length: number, amp: number): void {
const sr = this.sr;
const start = this.idx(t);
const len = Math.min(this.n - start, Math.ceil(length * sr));
if (start < 0 || len <= 0) return;
const lpA = Math.exp((-TAU * 160) / sr);
let lp1 = 0;
let lp2 = 0;
let phase = 0;
for (let i = 0; i < len; i++) {
const x = i / len;
const f = freq * (1 - 0.45 * x);
phase += (TAU * f) / sr;
lp1 = lp1 * lpA + noise() * (1 - lpA);
lp2 = lp2 * lpA + lp1 * (1 - lpA);
const env = Math.exp(-5 * x) * Math.min(1, i / (0.002 * sr));
const v = (Math.sin(phase) * 0.8 + lp2 * 9) * env * amp;
this.left[start + i] += v;
this.right[start + i] += v;
}
}
/**
* Metal sliding on asphalt: modulated noise through a bandpass that falls in
* pitch as the wreck slows, with stick-slip chatter feeding the panel resonators.
*/
scrape(t: number, length: number, amp: number, pan: number, f0: number): void {
const sr = this.sr;
const start = this.idx(t);
const len = Math.min(this.n - start, Math.ceil(length * sr));
if (start < 0 || len <= 0) return;
const gl = Math.sqrt((1 - pan) / 2) * amp;
const gr = Math.sqrt((1 + pan) / 2) * amp;
let y1 = 0;
let y2 = 0;
let a1 = 0;
let a2 = 0;
let b0 = 0;
let mod = 0.5;
let modTarget = 0.5;
let chatter = 0;
for (let i = 0; i < len; i++) {
const x = i / len;
if (i % 128 === 0) {
// Retune the bandpass and pick a new roughness target every few ms.
const f = f0 * (1 - 0.6 * x);
const r = Math.exp((-Math.PI * f * 0.5) / sr);
a1 = 2 * r * Math.cos((TAU * f) / sr);
a2 = r * r;
b0 = 1 - r;
modTarget = 0.25 + Math.random() * 0.75;
}
mod += (modTarget - mod) * 0.002;
const env = Math.min(1, i / (0.01 * sr)) * Math.pow(1 - x, 1.5);
const y = b0 * noise() + a1 * y1 - a2 * y2;
y2 = y1;
y1 = y;
const v = y * 3 * mod * env;
this.left[start + i] += v * gl;
this.right[start + i] += v * gr;
// Stick-slip: the body catches and releases, slower as it loses speed.
chatter -= (30 + 70 * (1 - x)) / sr;
if (chatter <= 0) {
chatter = rnd(0.6, 1.4);
this.metal[start + i] += noise() * amp * 0.6 * env;
}
}
this.metalEnd = Math.max(this.metalEnd, start + len);
}
/** Tires sliding sideways: a jittery, raspy squeal. */
squeal(t: number, length: number, amp: number, pan: number): void {
const sr = this.sr;
const start = this.idx(t);
const len = Math.min(this.n - start, Math.ceil(length * sr));
if (start < 0 || len <= 0) return;
const gl = Math.sqrt((1 - pan) / 2) * amp;
const gr = Math.sqrt((1 + pan) / 2) * amp;
const base = rnd(620, 980);
const lpA = Math.exp((-TAU * 2800) / sr);
let phase = 0;
let lp = 0;
let wander = 0;
for (let i = 0; i < len; i++) {
const x = i / len;
wander += (noise() * 0.5 - wander) * 0.0008;
const f = base * (1 + 0.025 * Math.sin((TAU * 7.3 * i) / sr) + wander * 0.08 - 0.15 * x);
phase = (phase + f / sr) % 1;
const saw = 2 * phase - 1;
const raw = saw * (0.65 + 0.35 * noise());
lp = lp * lpA + raw * (1 - lpA);
const env = Math.min(1, i / (0.02 * sr)) * (1 - x) * (1 - x);
const v = lp * env;
this.left[start + i] += v * gl;
this.right[start + i] += v * gr;
}
}
/** A hubcap spinning down: a ringing wobble that speeds up, then clatters flat. */
hubcap(t: number, length: number, amp: number, pan: number): void {
const sr = this.sr;
const start = this.idx(t);
const len = Math.min(this.n - start, Math.ceil(length * sr));
if (start < 0 || len <= 0) return;
const gl = Math.sqrt((1 - pan) / 2) * amp;
const gr = Math.sqrt((1 + pan) / 2) * amp;
const f = rnd(1050, 1500);
const w1 = (TAU * f) / sr;
const w2 = (TAU * f * 2.71) / sr;
let wob = 0;
for (let i = 0; i < len; i++) {
const x = i / len;
wob += (TAU * 4 * Math.pow(9, x)) / sr;
const am = Math.pow(Math.abs(Math.sin(wob)), 3);
const env = Math.min(1, i / (0.01 * sr)) * (x < 0.8 ? 1 : (1 - x) * 5);
const v = (Math.sin(w1 * i) * 0.75 + Math.sin(w2 * i) * 0.25) * am * env;
this.left[start + i] += v * gl;
this.right[start + i] += v * gr;
}
for (let i = 0; i < 4; i++) this.ping(t + length + i * 0.035, f * rnd(0.8, 1.2), 0.02, amp * 0.9, pan, 2.3);
}
/** Run the metal excitation through the panel resonator bank into the mix. */
ringPanels(modes: { f: number; bw: number; g: number }[], amp: number, dry: number): void {
const sr = this.sr;
const end = Math.min(this.n, this.metalEnd + Math.ceil(0.6 * sr));
const src = this.metal;
let first = 0;
while (first < end && src[first] === 0) first++;
modes.forEach((m, k) => {
const r = Math.exp((-Math.PI * m.bw) / sr);
const w = (TAU * m.f) / sr;
const a1 = 2 * r * Math.cos(w);
const a2 = r * r;
const g = (1 - r) * 2 * Math.sin(w) * m.g * amp * 40;
// Alternate modes left and right with some bleed, for a wide, solid body.
const gl = k % 2 ? 0.35 : 1;
const gr = k % 2 ? 1 : 0.35;
let y1 = 0;
let y2 = 0;
for (let i = first; i < end; i++) {
const y = g * src[i] + a1 * y1 - a2 * y2;
y2 = y1;
y1 = y;
this.left[i] += y * gl;
this.right[i] += y * gr;
}
});
// Some unfiltered crunch on top keeps the grit of tearing metal.
let prev = 0;
for (let i = first; i < end; i++) {
const v = (src[i] - prev) * dry;
prev = src[i];
this.left[i] += v;
this.right[i] += v;
}
}
lowpass(cutoff: number): void {
const a = Math.exp((-TAU * cutoff) / this.sr);
for (const ch of [this.left, this.right]) {
let y = 0;
for (let i = 0; i < this.n; i++) ch[i] = y = y * a + ch[i] * (1 - a);
}
}
normalize(peak: number): void {
let max = 0;
for (let i = 0; i < this.n; i++) max = Math.max(max, Math.abs(this.left[i]), Math.abs(this.right[i]));
if (max === 0) return;
const g = peak / max;
for (let i = 0; i < this.n; i++) {
this.left[i] *= g;
this.right[i] *= g;
}
}
}
interface Hit {
t: number;
e: number;
}
/**
* Render one crash. intensity is the game's impact intensity, closing speed / 100:
* about 0.1 for a bump, 0.5 to 1 for a solid hit, 1.1 to 1.7 at highway speed. kind picks the body: a car's thin
* panels ring higher; the truck hitting a wall is heavier and lower.
*/
export function renderCrash(sampleRate: number, intensity: number, kind: ImpactKind): CrashRender {
reseedNoise();
const sev = clamp(intensity / 1.4, 0.08, 1.25);
const chaos = sev * sev;
const heavy = kind === 'wall';
const rolls = !heavy && sev > 0.62;
const seconds = 0.55 + sev * 2.6 + (rolls ? 0.9 : 0);
const c = new Canvas(sampleRate, seconds, 0.15 + 0.8 * Math.min(1, sev));
// The impact sequence: the main hit, then the car tumbling (or the trailer
// slamming in behind the truck). Each bounce is a little later and weaker.
const hits: Hit[] = [{ t: 0.003, e: 1 }];
if (rolls) {
const count = Math.min(7, Math.floor((sev - 0.5) * 9) + (Math.random() < 0.5 ? 1 : 0));
let t = 0.003;
let gap = rnd(0.2, 0.34);
let e = 1;
for (let i = 0; i < count; i++) {
t += gap * rnd(0.8, 1.2);
gap *= rnd(0.72, 0.95);
e *= rnd(0.45, 0.75);
hits.push({ t, e });
}
} else if (heavy && sev > 0.45) {
hits.push({ t: rnd(0.12, 0.22), e: rnd(0.5, 0.7) });
}
let glassLeft = sev > 0.22 ? 1 : 0;
hits.forEach((hit, n) => {
const { t, e } = hit;
const pan = n === 0 ? 0 : c.spread();
// Thump and the sharp crack of first contact.
c.thump(t, (heavy ? 48 : 64) * rnd(0.9, 1.1), 0.1 + 0.3 * e * sev, e * (0.45 + 0.6 * sev));
c.grit(t, 0.0025, e * (0.5 + 0.7 * sev), pan * 0.3, 1);
// Crumpling metal: a burst of grains, denser and longer the harder the hit.
const grains = Math.round((12 + 160 * sev) * e * (heavy ? 1.25 : 1));
const tau = 0.025 + 0.13 * sev * e;
for (let i = 0; i < grains; i++) {
const late = Math.random() < chaos * 0.25;
const at = t + (late ? rnd(0.05, 0.45) : expDelay(tau));
c.crumpleGrain(at, logRnd(0.001, 0.02), e * logRnd(0.04, 1));
}
// A few big buckling pops.
for (let i = 0, pops = Math.round((1 + 6 * sev) * e); i < pops; i++) {
c.crumpleGrain(t + expDelay(tau * 1.5), rnd(0.015, 0.045), e * rnd(1.2, 2));
}
// Plastic bumper and trim cracking.
for (let i = 0, cracks = Math.round((3 + 22 * sev) * e); i < cracks; i++) {
c.ping(t + expDelay(0.02), rnd(900, 2600), rnd(0.002, 0.007), e * rnd(0.1, 0.35), c.spread(), 1.6);
}
// Glass: the first hard hit breaks the windshield, later bounces break the rest.
if (glassLeft > 0 && e * sev > 0.18) {
const share = n === 0 ? (rolls ? 0.55 : 1) : glassLeft;
glassLeft -= share * 0.5;
const shards = Math.round((40 + 950 * sev) * e * share);
c.grit(t + 0.002, 0.004, e * 0.5, pan, 1);
c.grit(t + 0.004, 0.08 + 0.1 * sev, e * 0.18 * sev, pan, 1);
for (let i = 0; i < shards; i++) {
const falling = Math.random() < 0.4;
const at = falling ? t + rnd(0.1, 0.25 + 0.7 * sev) : t + expDelay(0.035);
const amp = e * (falling ? logRnd(0.01, 0.1) : logRnd(0.02, 0.22));
c.ping(at, logRnd(2400, 12500), logRnd(0.002, falling ? 0.012 : 0.03), amp, c.spread(), 2.32);
}
}
// Scraping along the ground after each hit, longest after the main impact.
if (sev > 0.18) {
const next = hits[n + 1]?.t ?? t + 0.2 + 1.6 * sev + (heavy ? 0.4 : 0);
const length = n === 0 && !rolls ? next - t : (next - t) * 0.85;
c.scrape(t + 0.03, length, e * (0.12 + 0.3 * sev), pan, heavy ? rnd(1400, 2200) : rnd(2200, 3600));
}
});
// Tires squealing as the car is shoved sideways.
if (!heavy && sev > 0.32) c.squeal(0.015, 0.25 + 0.8 * sev, 0.07 + 0.08 * sev, c.spread());
// Debris: pieces flying off, landing and bouncing to a stop.
const pieces = Math.round(2 + 8 * sev + 34 * chaos);
for (let i = 0; i < pieces; i++) {
const metal = Math.random() < 0.6;
const freq = metal ? logRnd(700, 5200) : rnd(300, 900);
const pan = c.spread();
let at = rnd(0.08, 0.3 + 0.9 * sev);
let amp = rnd(0.06, 0.28) * (0.5 + 0.5 * sev);
let gap = rnd(0.07, 0.16);
for (let b = 0, bounces = 1 + Math.floor(Math.random() * 4); b < bounces; b++) {
if (metal) c.ping(at, freq * rnd(0.97, 1.03), rnd(0.01, 0.045), amp, pan, 2.76);
else c.grit(at, 0.006, amp * 1.4, pan, 0.3);
at += gap;
gap *= 0.6;
amp *= 0.55;
}
}
if (!heavy && sev > 0.6 && Math.random() < 0.6) c.hubcap(rnd(0.3, 0.6), rnd(1.1, 1.8), 0.1, c.spread());
// Ring the crumple through the body panels.
const modeCount = 8 + Math.round(6 * sev);
const modes = Array.from({ length: modeCount }, () => ({
f: heavy ? logRnd(95, 2600) : logRnd(220, 4800),
bw: logRnd(3, heavy ? 18 : 28),
g: logRnd(0.3, 1),
}));
c.ringPanels(modes, 0.5, 0.35);
if (kind === 'pileup') c.lowpass(2600);
c.normalize(0.5 + 0.45 * Math.min(1, sev));
return { left: c.left, right: c.right, severity: sev };
}
