How to Build Reactive Audio in the Browser: A Web Audio API Guide
Reactive sound design isn't continuous ambient — it's event-triggered, information-bearing sound. Step-by-step on how to do it in a browser game with the Web Audio API.
Reactive sound is not two things: continuous music or random effect. Reactive sound is triggered by an event inside a game, carries information, then goes silent. This article shows how to do it in the browser — with code and principles.
1. The base: AudioContext
The center of the Web Audio API is AudioContext. Create it once, let it live for the game's lifetime. It can't start without user interaction (autoplay policy) — wait for a "START" button click.
const ctx = new AudioContext();
document.querySelector("#start")?.addEventListener("click", () => {
if (ctx.state === "suspended") ctx.resume();
});2. Audio files: preload, decode
Loading a file when the event fires is too late. fetch the bytes, turn them into an AudioBuffer with decodeAudioData, keep them in memory.
async function loadSample(url: string): Promise<AudioBuffer> {
const res = await fetch(url);
const bytes = await res.arrayBuffer();
return await ctx.decodeAudioData(bytes);
}
const chime = await loadSample("/audio/chime.wav");3. Playing: BufferSource + Gain
Create a new AudioBufferSourceNode for every sound event (single-use). Control level with GainNode. The connect chain: source → gain → destination.
function playChime(volume = 0.6) {
const src = ctx.createBufferSource();
src.buffer = chime;
const gain = ctx.createGain();
gain.gain.value = volume;
src.connect(gain).connect(ctx.destination);
src.start();
}The golden rule of reactive sound: silence is default. If nothing is playing, the game is running. Sound means event.
4. Ducking: an important event silences the rest
Route every sound through a master GainNode. When an important event happens (e.g. a goal), drop the master gain to 0.2, then ramp back to 1.0. The pullback of sound creates the drama.
const master = ctx.createGain();
master.connect(ctx.destination);
function duck(durationMs = 800) {
const now = ctx.currentTime;
master.gain.cancelScheduledValues(now);
master.gain.setValueAtTime(master.gain.value, now);
master.gain.linearRampToValueAtTime(0.2, now + 0.05);
master.gain.linearRampToValueAtTime(1.0, now + durationMs / 1000);
}5. Spatialization: positional information
PannerNode tells you where the sound comes from. In a 2D game, feed x,y coordinates to the panner. If danger approaches from the left, the player knows without hearing explicitly.
6. Performance: not ScriptProcessor, AudioWorklet
In modern browsers ScriptProcessorNode is deprecated. If you need custom DSP use AudioWorklet — runs on its own thread, no jank. For most games the built-in nodes are enough.
7. Anti-patterns
- Loop drone: continuous ambient = no information = boring.
- Effect on every click: sound inflation, ear fatigue.
- Same sample on repeat: prepare 3-5 variations, pick at random.
- Forgetting master gain: a single user mute should toggle the master.
8. Practical checklist
- Single AudioContext instance, resumed at the game's start
- All samples loaded up front
- Ducking through the master gain
- 3+ variations per effect
- Silence baseline — your script should have a "when shouldn't there be sound" list
9. Conclusion
Reactive audio gives huge impact for little code. The whole infrastructure fits in 100 lines of Web Audio. The rest is design: deciding which event deserves a sound, when to stay silent. Signal Pitch's doctrine — sound = information — drives that decision.