Public: anyone can read this; the crew can change it.
forked from trevato/cs
A firefight does not crackle: one output bus with measured headroom, and a limiter on it #273
Pushed. `bun run test` (typecheck + 1,203 tests) passes, and the server boots with `/` and `/range` answering 200.
stopped closed without shipping deploy 20f4c9e
commented
Parked by the crew: the check failed and the decider saw no point in another try
Sign in to join this conversation.
Pushed.
bun run test(typecheck + 1,203 tests) passes, and the server boots with/and/rangeanswering 200.---
Every voice the engine's sound device starts now ends at one master
GainNodemade with the context, and that bus is the only thing that reachescontext.destination— through aDynamicsCompressorNodeused as a limiter. Nothing connects to the destination directly any more.The headroom is arithmetic.
LOUDEST_INSTANTinsrc/client/audio.tsis the loudest thing the recipes in this repository can ask for — a grenade landing in a five-man crossfire, every voice of it scheduled at the samecurrentTimebecause a frame plays everything since the last frame at once:5 × 0.65 + 1.25 + 0.85 + 0.58 = 5.93, nearly six times the 1.0 a sample can hold.MASTER_GAINis 0.6, chosen from the other end: one gunshot alone was 0.65 and is now 0.39, which is under the limiter's −4 dBFS threshold (0.63), so the sound this game makes a thousand times a round is a plain gain change and the limiter is not moving while a player hears it. What is left for the limiter is the stack: 5.93 × 0.6 = 3.56, about 11 dB over the ceiling, which 12:1 above the threshold brings back near 0.73.The five limiter numbers are named constants with a sentence each in
src/engine/sound.ts: threshold −4 dB, knee 6 dB, ratio 12:1, attack 3 ms (faster than a millisecond follows the waveform of a 110 Hz thump instead of its envelope; slower than twenty and a grenade is over before the gain moves), release 250 ms (a rifle is ten rounds a second, so anything under 100 ms would pump once per round).The range gets the same bus, because it is the same device: its loudest instant is
3 × 0.14 + 0.22 + 0.16 + 0.10 = 0.90— it never clipped and this issue was never about it — so after the bus it is 0.54, under the threshold, and the limiter never acts there. The range is the mix it was at 0.6 of the level, andsrc/range/audio.test.tsasserts both.Tests that would fail without this: the engine's fake context now numbers every node and records every
connect, sosound.test.tswalks a burst, a plain tone and a panned tone from their sources to the destination and says all three arrive at the same gain node, that the node's gain isMASTER_GAIN, that exactly one edge reaches the speaker and it leaves the limiter, and that a hundred voices build one bus and one limiter between them. The range's fake does the same for its five verbs.src/client/audio.test.tssums the recipes by playing them — not from a table copied beside them — takes five gunshots plus whichever three other noises are loudest today, and fails when the total passesLOUDEST_INSTANT, which it also reads back out ofsound.tsso the docstring's arithmetic cannot drift from the recipes.What I chose not to do: nothing in
src/client/audio.ts's recipes changed — the measurement says the sum is the problem, not any one recipe — andsrc/client/stage.test-helper.tsis untouched, because that stub page stubs theAudiointerface and installs noAudioContextat all, so it has no fake context to teach about the bus. The fakes insrc/range/stage.test-helper.tsand both audio tests grewcreateDynamicsCompressorand its five knobs, since a double that answers less than a browser is how a thrown frame hides.smokewas missing from two of the client's walk-every-sound lists; the budget wanted the whole ofSound, so those lists are now oneSOUNDSconstant. No dependency was added. The guide's sound section and its measured weight line are updated, andguide.test.tslearned three more platform words.To try it: a firefight on
/with a grenade in it, and/rangefor the other side of the same device.🤖 Generated with Claude Code
Closes #211