Skip to content

Sound Map fight simulator (brief, 2026-10-16)

Chris approved this on 2026-10-16: “create a game/sound simulator in the sound tool, it launches the sounds in a way that mirrors how they would happen in the game”. The goal is to judge candidate SFX inside a realistic fight mix without a device, and to let Claude measure the mix, which is the closest Claude gets to hearing it.

Build (implementer agent in its own worktree, branched off sfx-synth-all)

Section titled “Build (implementer agent in its own worktree, branched off sfx-synth-all)”
  1. Record. Extend tools/sfx_freq/sfx_freq.gd, which runs headless autopilot through the real AudioManager routing. Add a --timeline=<out.json> option that writes every play() / UI play as {t, key, file_variant, volume_db, pitch, priority, bus, pos?} in sim time, plus the main.gd one-shots it already mirrors. Check that its CountingAudio.play override still matches AudioManager.play’s current signature, which has gained a bus param. Presets: early game (mortal), a busy wave about 8 min in (--invuln), and a Warden, Graviton and FunZo fight (--force_boss). Commit the small preset timelines under tools/sound_map/timelines/.
  2. Replay. Add a “Fight” panel to the Sound Map (tools/sound_map/src) that plays a timeline with WebAudio:
    • a 16-voice cap with the game’s steal rule (AudioManager POOL_SIZE 16, boss reserve, priority);
    • an approximate SfxMix chain: low cut 80 Hz on non-boss, Space reverb wet −32 dB, Stinger ducking Music;
    • optional music bed. Per key, the panel swaps in any candidate: either a “use this take in the fight” toggle on a take row, or “all assigned/picked”. Add transport (play/stop/seek), a solo for one key, and a voice-count meter.
  3. Measure. Add an offline Python mixdown of the same timeline (tools/sound_map/fight_mix.py) that writes a WAV plus metrics: voices in use over time, dropped plays, low-mid (180-410 Hz) energy, and duck time. This is the 10-14 mud diagnosis, automated.
  4. Add node and py tests. Browser-verify with chrome-devtools on a spare port: 8080 and 8081 are in use, never kill them.

The mix chain approximates the game’s, and laptop speakers are not a TV or phone, so picks still get one real ATV run before shipping.

In the Sound Map (python3 tools/sound_map/server.py, then the Fight button top right):

  1. Pick a preset (early, busy8, warden, warden_end, graviton, funzo; 90 s each). ▶ plays the fight through a WebAudio copy of the game’s 16-voice pool and SfxMix chain; click the track to seek. The meter shows voices in use (the violet three are the boss reserve), the sparkline the whole fight’s voice count.
  2. Swap takes in. Swaps go into the game’s round-robin slots (<key>, <key>_2, …) the way Assign does; slots nobody swaps keep the game’s file. On a take row, Fight puts the shown take in the slot its slot selector points at, and Fight ×N puts the whole take family (v1, v2, …) in slots 1..N. In the drawer: Use families (per key, the first take family whose root matches, default synth_.*), Use v1 only (that family’s v1 in slot 1, the game’s other slots still play), Use all assigned, Clear. When a swap changes a key’s slot count, the replay re-runs the game’s round-robin pick (uniform, never the same slot twice running) on a fixed per-key random stream; otherwise it keeps the game’s recorded picks.
  3. Solo one key (the pool still runs with every sound, so cut-offs stay real); Music adds the area / boss track, ducked by the Stinger bus (approximately).
  4. Analyze runs fight_mix.py on the server for the current swap set and draws each check as a lane; click a mark (or a row of the warning list) to jump there. Compare puts live sounds vs the swap set side by side, metric by metric. “What the checks approximate” lists every simplification and threshold. Loudness is measured after the game’s master hard limiter (audio/bus_layout.tres), so the squash measure is the limiter’s gain reduction (max, time over 1/3/6 dB), not a clipping peak.

From the shell:

bash scripts/record-fight-timelines.sh # re-record every preset (tools/sound_map/timelines/)
python3 tools/sound_map/fight_mix.py graviton # checks on the live sounds
python3 tools/sound_map/fight_mix.py graviton --swap-family 'synth_.*' --compare # every synth family into the slots
python3 tools/sound_map/fight_mix.py graviton --swap-pattern 'synth_.*_v1' --compare # v1 into slot 1 only
python3 tools/sound_map/fight_mix.py graviton --swap pulse_zap_2=synth_pulse_zap_v3 # one slot
python3 tools/sound_map/fight_mix.py warden --swap-picked --out /tmp/warden.wav --report /tmp/r.json --music

The mix chain approximates the game’s, and laptop speakers are not a TV or phone, so picks still get one real ATV run before shipping.