Public: anyone can read this; the crew can change it.

forked from trevato/cs · proposes · 4 behind

The range makes its own noise: a shot, a hit and the clock through the engine’s sound device #82

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

The range has a voice now, and it is the engine's device with the range's own recipes: `src/range/audio.ts` is five verbs — `shot()`, `hit(distance)`, `pop(distance)`, `tick()` and `over()` — over `createSoundDevice`, each a named constant a test can read. The crack is a 2,400 Hz burst for 70 ms with no thump under it, against `src/client/audio.ts`'s rifle at 1,500 Hz ringing for 240 ms with one;…

shipped production is serving it deploy ae07c2d

opened this

The range has a voice now, and it is the engine's device with the range's own recipes: src/range/audio.ts is five verbs — shot(), hit(distance), pop(distance), tick() and over() — over createSoundDevice, each a named constant a test can read. The crack is a 2,400 Hz burst for 70 ms with no thump under it, against src/client/audio.ts's rifle at 1,500 Hz ringing for 240 ms with one; the hit is a clean sine, a plate standing up is a knock that drops, the tick is short and the clock running out is that same note falling away. Distance is the engine's attenuate on this room's numbers — EARSHOT 320 and CLOSE 160, each with a sentence saying why — so a plate hit at 1,200 units is a quarter the gain of one at 200, and every gain the module asks for is inside 0..1.

main.ts plays them off crossings in the game's own facts, never off a comparison that stays true: a plate whose id the frame before had not reached popped, the Shot that shoot handed back cracks and (when it found a plate) hits, the clock passing 3, 2 and 1 ticks once each, and wasLive && phase === "over" plays the last tone. A ready or over run is silent, a trigger pulled outside a run is refused and therefore silent, and a frame with nothing to say allocates nothing. The device is opened on the gesture that starts a run — audio.resume() on the Enter edge, wrapped so a browser that refuses never throws — and never at boot.

Worth looking at: src/range/audio.test.ts drives a fake AudioContext constructor of its own (the shape src/engine/sound.test.ts uses, written again rather than imported, because it reads nodes as well as calls); dom.ts now hands the stub page an AudioContext that counts constructors and voices, and boot.test.ts reads those voices by name — zero constructors at boot and for a page nobody plays, one for a whole run, one crack for a held trigger, and exactly three ticks plus one run-over tone from a thirty second run of 190 frames. bun test is green (474 pass). The range's ceiling is raised from 26,700/11,600 to 29,400/12,700: sound cost 2,444 raw and 956 gzipped bytes and the bundle measures 26,658 raw / 11,456 gzipped today, which would have fitted the old ceiling by 42 bytes — not a ceiling anybody is holding. No mute control: the page has no button for one and nothing here stores a preference, so it stayed out.

In the preview, open /range, click Capture the mouse, press Enter and listen: plates knock as they stand, the crack and the tone separate a hit from a miss, and the last three seconds count themselves out.

🤖 Generated with Claude Code

Closes #52

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs