In the Mountains · audio campaign · 2026-06-07

The Valley Found Its Voice

A research-driven rebuild of the game's procedural soundscape — from a combat-only, essentially mono mix with no reverb and silence between firefights, to a living Korengal valley: a canyon that echoes, a wind-and-water bed that makes silence tense, terrain that muffles fire behind a ridge, and weapons whose timbre tells you who's shooting. Every claim below is a number from an offline render of the real synthesis code, and an audio clip you can play.

corr 0.981→0.392stereo image (firefight): mono→wide -120→-42.62 dBambient bed: silent→alive 23.1 dBcalm→combat dynamic range 2450→7100 mscanyon reverb tail (firefight) -2.62 dBFSno clipping (busiest scene)

The bar, and the gap

The success condition for this project is literal: a skeptical soldier plays it and says "holy shit, an AI built this?" Sound is half of immersion, and on HEAD it was the weakest half. The old system was a solid spine — ~17 procedurally-synthesized combat cues — but the offline oracle exposed three structural deficits the ear would just call "flat":

It was mono

L/R correlation was 1–1.0 on every sound — even a firefight with fire from the left flank and a ridge on the right (9.8% width). StereoPanner only changes level; the waveforms stayed identical, so nothing enveloped you.

It had no reverb

The defining sound of a Korengal firefight is the report rolling off the far ridge. There was none — every shot was a dry pop. (The "tail" the old metric saw was just synth decay.)

Silence was empty

Between firefights: −120 dBFS. Literal digital silence, which reads as "audio is broken," not "tense calm."

And it was uncontrolled

No master bus, no limiter, loudness drifting per event — and gunshots were 3 thin layers with no mechanical action, brass, or tail.

How it was built — research → design → implement → verify

1 · RESEARCH6 specialist agents (gunfire DSP, canyon reverb, ambient design, Web-Audio spatialization, adaptive mixing, real Afghan-valley acoustics) fanned out across GDC talks, AES papers, forensic-acoustics studies, and war-doc sound design → one cited dossier.
2 · DESIGNThe dossier became an additive bus architecture, parameter table, and build order — grounded in (and verified against) the real code.
3 · BUILD3 parallel agents built the isolated modules (reverb, ambient, 5-layer gunfire) to interface contracts while the spine (bus graph, HDR, ducking, occlusion) was integrated by hand.
4 · MEASUREAn offline oracle renders the real synth to PCM and turns "20×" into pass/fail numbers; tuned to all-green.
5 · ATTACKAn adversarial agent re-read everything and caught a real measurement bug — fixed, then re-measured honestly.

Everything is 100% procedural (Web Audio only, zero audio-file assets — even the reverb impulse response is generated from decaying noise) and deterministic (same seed ⇒ same sound; the event→cue mapping is a pure function — proven by scripts/audio-probe.ts).

The signal chain

The whole upgrade is one idea applied everywhere: a small set of shared buses carrying a dry path plus a wet reverb send, glued by a master limiter. Two wins fall out for free — a stereo reverb impulse decorrelates the tail (fixing the mono mix), and a reverb send that rises with distance makes far events read as "distant," not just "quiet."

per positional cue ─┬─ gain → lowpass(air+occlusion) → [elev shelf] → pan ─────────────┐ └─ send(×distance×kind) → highpass 300 → preDelay ─┐ │ ▼ ▼ AmbientEngine ───────────────────────────────────────► atmosBus valley convolver combatBus (wind·river·generator·birds·dogs·adhan, day+weather) │ (decaying-noise IR │ radio cue → in-handset chain (HP550·LP3k·sat·comp) ──► radioBus + ridge slap-taps)│ tic_sting ──────────────────────────────────────────► scoreBus │ │ └──────┬─────────┴────────────┘ ▼ master → limiter(brickwall) → output

Before / after — see it, hear it

Each panel is an offline render of the real engine: waveform (top) + spectrogram (bottom), HEAD vs the new system. Press play to hear the same scene through both.

Calm — between firefights
The living valley. HEAD is a flat line — total silence (−120 dBFS). The new bed is a continuous, frequency-partitioned wash: wind howl in the low-mids, the river's babble, the COP generator's drone, day-birds — all driven by the sim's real time-of-day, wind speed and weather, and ducked hard the instant contact starts. This single change is the biggest immersion jump.
▸ HEAD
▸ new
Distant PKM on the high ground
Rolling valley thunder. Same scene, same gun ~700 m up-ridge. HEAD (top): dry pops with dead air between. New (bottom): every burst trails a multi-second reverb tail bouncing off the canyon walls, and the stereo image opens right up (correlation 1→0.037) — the sound of a real firefight in a draw.
▸ HEAD
▸ new
Terrain occlusion: open line-of-sight vs a ridge between
The valley is the enemy — applied to sound. The same PKM 300 m east, fired with a clear line of sight (top) vs with a 120 m ridge in the way (bottom). The ridge version is 9.0 dB quieter and visibly darker (high-frequency energy 42.03%→30.08%): a gun behind a spur sounds dull and bassy, then snaps bright the instant it has line-of-sight. No other top-down shooter has terrain-masked audio because none carry elevation data.
▸ new
M4 muzzle — a single shot
Five layers, not three. Each shot is now a transient click + a weapon-identity body + a sub thump + a mechanical bolt-clack + a reverb tail, with a true sub-millisecond supersonic crack. Transient sharpness (crest) climbs 16.79→24.96 dB, and the 5.56 vs 7.62 brightness split is now real and correct (see the timbre chart below).
▸ HEAD
▸ new
A sustained TIC
The full mix under load. HEAD (top): sparse dry cracks. New (bottom): a denser, more realistic firefight whose reverb tails fill the gaps into rolling thunder, with a genuinely wide image (correlation 0.981→0.392) and a master limiter holding the peak under 0 dBFS with no clipping. (The test scene was also made denser/more realistic for the new render, so compare the character — width, tails, headroom — rather than absolute level here.)
▸ HEAD
▸ new

The numbers

Stereo image — L/R correlation (lower = wider, 1.0 = mono)firefight0.9810.392distant PKM10.037HEAD (before)new system Loudness floor — calm RMS (dBFS): from digital silence to a living, subtle bedambient calm-120 dB-42.62 dBambient night-120 dB-42.42 dBHEAD (before)new system Weapon brightness — spectral centroid (Hz). The new layered shots restore the correct 5.56>7.62 'tell'M4 (5.56) us79806860AK (7.62) ins80586212SAW us80176435PKM ins79345472HEAD (before)new system
Ambient calm — spectrum (silence → full-band bed)100Hz1kHz10kHzbeforeafter Distant fire — spectrum (reverb adds sustained energy)100Hz1kHz10kHzbeforeafter

How each piece works

Valley reverb — a canyon from pure noise

A shared ConvolverNode whose impulse response is generated, not recorded: exponentially-decaying stereo noise (Moorer's classic result) shaped by a highpass + an air-absorption lowpass that sweeps bright→dark across the tail, with 4 discrete "slap-taps" baked in at 0.18 / 0.42 / 0.85 / 1.4 s — the echoes off named cliff faces, panned to different walls. RT60 ≈ 1.8 s. The two channels use independent noise, so the tail is decorrelated — that's what gives the mix its width. Distance raises each cue's wet send; only the bright crack is fed in (the sub stays dry) so the tail rings without mud.

Ambient bed — geophony · biophony · anthrophony

A persistent voice bank: procedural wind (filtered noise, gust LFOs, weather-driven brightness), river (band-limited babble, notched to leave room for birds), the COP generator (additive diesel drone, distance-gated), and Poisson-scheduled birds / insects / dogs / call-to-prayer — all keyed to the sim's real solarLight(), windVector(), weather and time. Layers occupy reserved frequency bands so they never turn to mud. On the contact rising edge the whole bed ducks ~85% in 0.4 s and exhales back over 12 s — the readable silence of the calm-before, made into an instrument.

HDR mix + ducking — somber by dynamics, not volume

The loudest current sound raises a floating window that quieter sounds duck beneath (and below the window, get culled) — so an IED makes the world go quiet around it rather than just louder. Radio ducks the ambient; heavy HE ducks everything. The result is a 23.1 dB swing from a held breath to a blast, with a brickwall limiter guaranteeing it never clips.

Spatialization — for a top-down listener

StereoPanner by screen position (HRTF would be wasted on an overhead camera), distance attenuation + air-absorption lowpass, the speed-of-sound crack→thump split (the ranging cue every soldier knows), an elevation brightness shelf (high-ground guns sit forward), and a terrain line-of-sight raycast that muffles fire behind a ridge.

How do you know it's real? (the verification)

Audio is normally judged by ear, which doesn't satisfy "no fix without a number." So the campaign is built on an offline oracle (scripts/audio-render.ts): it renders the actual synthesis + spatialization + bus graph through a headless OfflineAudioContext to PCM, and computes peak, K-weighted loudness, spectral centroid, stereo correlation, reverb tail and more — and writes a listenable .wav. Renders are seed-pinned so every A/B delta is real, not noise. It asserts the goals as pass/fail:

✓ no scene clips (busiest = firefight -2.62 dBFS)  ·  ✓ calm→combat spread 23.1 dB (>15)  ·  ✓ ambient alive but subdued (-42.62 dB)  ·  ✓ firefight image wide (corr 0.392)  ·  ✓ occlusion muffles (9.0 dB + 42.03%→30.08% HF)

Then an adversarial reviewer re-read every file looking for lies. It confirmed determinism, layer purity, no leaks — and caught a real one: the oracle's voice counter only incremented, so the busy firefight was being measured on just its first 32 cues. That was fixed (proper voice retirement), the gain staging it had masked was corrected, and the firefight was re-measured honestly. It was also verified live in the browser: the full graph builds, the ambient bed plays at -40 dBFS during the calm, combat cues fire through the real AudioContext, zero console errors.

What we deliberately did not do (restraint logged)