clone

an endless marble-race generator in the browser; the url is the seed, so every track can be shared

main
marbles / tests / audio_test.ts
audio_test.ts 2.9K
1/**2 * Ambient audio (M13) — rule tests over the PURE mapping layer (the3 * WebAudio realization is browser-only; nothing here touches an4 * AudioContext). USER VALIDATION STYLE: rules, not time.5 */6import { assert } from "@std/assert";7import {8 cueFor,9 type CueKind,10 padForMood,11 rollParams,12} from "../src/client/audio.ts";13import { MOODS } from "../src/client/atmosphere.ts";1415Deno.test("audio pads: every atmosphere mood has a bounded, ascending drone", () => {16 for (const mood of MOODS) {17 const pad = padForMood(mood.name);18 assert(pad !== null, `mood "${mood.name}" has no pad row`);19 assert(pad.freqs.length >= 2, `${mood.name}: drone needs ≥2 partials`);20 for (let i = 0; i < pad.freqs.length; i++) {21 assert(22 pad.freqs[i] >= 30 && pad.freqs[i] <= 2000,23 `${mood.name}: partial ${i} out of band (${pad.freqs[i]} Hz)`,24 );25 if (i > 0) {26 assert(27 pad.freqs[i] > pad.freqs[i - 1],28 `${mood.name}: partials not ascending`,29 );30 }31 }32 assert(pad.gain > 0 && pad.gain <= 1, `${mood.name}: pad gain`);33 assert(pad.airGain >= 0 && pad.airGain <= 1, `${mood.name}: air gain`);34 assert(35 pad.cutoff >= 100 && pad.cutoff <= 4000 &&36 pad.airCutoff >= 100 && pad.airCutoff <= 8000,37 `${mood.name}: cutoffs out of range`,38 );39 }40 // Unknown moods are an explicit null, never a silent default.41 assert(padForMood("no-such-mood") === null);42});4344Deno.test("audio roll: the marble's voice is monotone in speed and bounded", () => {45 let prevGain = -1;46 let prevCutoff = -1;47 for (let s = 0; s <= 12; s += 0.25) {48 const r = rollParams(s);49 assert(r.gain >= prevGain, `gain not monotone at ${s}`);50 assert(r.cutoff >= prevCutoff, `cutoff not monotone at ${s}`);51 assert(r.gain >= 0 && r.gain <= 0.5, `gain out of bounds at ${s}`);52 assert(53 r.cutoff >= 200 && r.cutoff <= 2400,54 `cutoff out of bounds at ${s}`,55 );56 prevGain = r.gain;57 prevCutoff = r.cutoff;58 }59 // Silent at rest — the bed carries the quiet, not the roll.60 assert(rollParams(0).gain === 0);61});6263Deno.test("audio cues: every event kind is a short, bounded envelope", () => {64 const kinds: CueKind[] = [65 "boost",66 "launch",67 "land",68 "fall",69 "milestone",70 "checkpoint",71 ];72 for (const k of kinds) {73 const c = cueFor(k);74 assert(c.dur >= 0.05 && c.dur <= 2, `${k}: duration ${c.dur}`);75 assert(c.gain > 0 && c.gain <= 0.5, `${k}: gain ${c.gain}`);76 assert(77 c.freq0 >= 60 && c.freq0 <= 4000 && c.freq1 >= 60 && c.freq1 <= 4000,78 `${k}: sweep out of band`,79 );80 }81 // Directional grammar: rises announce energy in, falls announce loss.82 assert(cueFor("boost").freq1 > cueFor("boost").freq0);83 assert(cueFor("launch").freq1 > cueFor("launch").freq0);84 assert(cueFor("land").freq1 < cueFor("land").freq0);85 assert(cueFor("fall").freq1 < cueFor("fall").freq0);86});