How to Add Sound to a Browser Game (Web Audio API)

high rise buildings during night time

Your browser game looks fine, but it is silent. A jump should make a noise. A coin should chime. A hit should land with weight. This guide walks through how to add sound to a browser game using the Web Audio API — the modern standard for audio in JavaScript. No frameworks, no libraries, just AudioContext and a few node primitives that ship with every browser.

Key takeaways

  • The Web Audio API is built into every modern browser; no library required.
  • AudioContext is the central object — create it once, route everything through it.
  • Oscillator nodes generate procedural sound; AudioBufferSourceNode plays sample files.
  • GainNode controls volume; multiple gains let you mix sound effects against background music.
  • Browsers block audio until user interaction — resume the AudioContext after the first click or keypress.

The Web Audio API in one paragraph

The Web Audio API is a graph-based audio system. You build a chain of nodes — sources, processors, and a destination — and audio flows through them. The destination is usually the speakers. A typical chain looks like: source → gain → destination. More complex games add filters, panners, and effects in between. The MDN Web Audio API documentation is the reference.

Creating the AudioContext

Everything starts with an AudioContext. Create one global instance and reuse it across the whole game.

const audioCtx = new (window.AudioContext || window.webkitAudioContext)();

The fallback to webkitAudioContext handles older Safari versions. Most modern code can drop it, but it costs nothing to keep.

The autoplay restriction

Browsers refuse to start audio until the user has interacted with the page. This is a deliberate anti-autoplay-ad rule. Your AudioContext starts in a “suspended” state and you have to resume it after the first click or keypress.

document.addEventListener('click', resumeAudio, { once: true });
document.addEventListener('keydown', resumeAudio, { once: true });

function resumeAudio() {
  if (audioCtx.state === 'suspended') {
    audioCtx.resume();
  }
}

The { once: true } option auto-removes the listener after firing. If you skip this step, no sound will play and the console will quietly log a warning.

Generating sound with oscillators

The simplest sound source is an oscillator — a pure tone generator. Oscillators are perfect for arcade-style beeps, chiptune effects, and procedural sound design. No assets needed.

function playJumpSound() {
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();

  osc.type = 'square';
  osc.frequency.setValueAtTime(440, audioCtx.currentTime);
  osc.frequency.exponentialRampToValueAtTime(880, audioCtx.currentTime + 0.1);

  gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
  gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.15);

  osc.connect(gain);
  gain.connect(audioCtx.destination);

  osc.start();
  osc.stop(audioCtx.currentTime + 0.15);
}

That code generates a quick rising beep — exactly the kind of sound you would attach to a jump action. The frequency ramp gives the tone its upward sweep; the gain ramp creates a quick fade-out so the sound does not click off abruptly.

Oscillator types and what they sound like

Oscillators have four basic types, each with a distinct sonic character.

  • sine: Pure tone, soft and clean. Good for menu sounds, gentle UI feedback.
  • square: Buzzy and aggressive. Classic chiptune sound, suits arcade effects.
  • sawtooth: Bright and harsh. Useful for explosions or laser sounds.
  • triangle: Hollow and woody. Sits between sine and square.

Try each in your code by swapping osc.type between the four values. The character difference is immediate.

Loading and playing sample audio

For sound effects that need more character than oscillators can produce — voice clips, complex hits, recorded music — load actual audio files. The Web Audio API uses fetch and decodeAudioData for this.

async function loadSound(url) {
  const response = await fetch(url);
  const arrayBuffer = await response.arrayBuffer();
  const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
  return audioBuffer;
}

const sounds = {};
sounds.hit = await loadSound('/audio/hit.wav');
sounds.coin = await loadSound('/audio/coin.wav');

Decode all sounds upfront during a loading screen. Playing them later is then instant — no disk I/O during gameplay.

Playing a loaded sample

function playSample(buffer, volume = 1.0) {
  const source = audioCtx.createBufferSource();
  const gain = audioCtx.createGain();

  source.buffer = buffer;
  gain.gain.value = volume;

  source.connect(gain);
  gain.connect(audioCtx.destination);

  source.start();
}

BufferSourceNodes are single-use — you create one each time you play the sound. The buffer itself stays in memory; only the source node is disposable. This is the standard pattern in the Web Audio API and is more efficient than HTML5 Audio elements for short effects.

The master gain node

For volume control, route everything through a master gain node before reaching the destination. This lets you mute or fade the entire game’s audio in one place.

const masterGain = audioCtx.createGain();
masterGain.gain.value = 1.0;
masterGain.connect(audioCtx.destination);

// In playJumpSound, playSample, etc:
gain.connect(masterGain);  // not directly to destination

Now masterGain.gain.value = 0 mutes everything. You can also expose the master gain to a volume slider in your UI.

Separate channels for music and effects

Most games want independent control over background music and sound effects. Two gain nodes solve this cleanly.

const musicGain = audioCtx.createGain();
const sfxGain = audioCtx.createGain();
musicGain.connect(masterGain);
sfxGain.connect(masterGain);

Route music through musicGain, sound effects through sfxGain. Users can mute music while keeping hit sounds. The graph stays flat and easy to reason about.

Background music with looping

To play looping background music, set the loop property on the BufferSourceNode.

function playMusic(buffer) {
  const source = audioCtx.createBufferSource();
  source.buffer = buffer;
  source.loop = true;
  source.connect(musicGain);
  source.start();
  return source;
}

let musicSource = playMusic(sounds.bgm);

// To stop:
musicSource.stop();
musicSource = null;

Keep a reference to the music source so you can stop it later. Once a source is stopped, it cannot be restarted — you have to create a new one.

Pitch variation for repeated sounds

Repeated identical sounds get monotonous fast. If your player picks up a coin every two seconds, a slight pitch variation each play makes the sequence sound less robotic.

function playSampleVaried(buffer, volume = 1.0) {
  const source = audioCtx.createBufferSource();
  const gain = audioCtx.createGain();

  source.buffer = buffer;
  source.playbackRate.value = 0.95 + Math.random() * 0.1;
  gain.gain.value = volume;

  source.connect(gain);
  gain.connect(sfxGain);
  source.start();
}

A playbackRate between 0.95 and 1.05 produces a subtle variation that breaks up repetition without being noticeable as effect. Wider ranges sound deliberately processed.

Procedural sound effects

Oscillators plus envelopes plus filters cover most arcade-style sound effects without any audio files at all. A common shopping list:

  • Coin pickup: Two short tones in quick succession, ascending — 660Hz then 880Hz, 50ms each.
  • Jump: Sweep from 440Hz to 880Hz over 100ms with a fade-out.
  • Hit: Sawtooth wave from 200Hz to 50Hz over 80ms, gain dropping fast.
  • Explosion: White noise (via AudioBuffer filled with random values) through a low-pass filter that sweeps down over 500ms.
  • Power-up: Rising arpeggio — four tones at 660, 880, 990, 1320 Hz, 50ms each.

These are cheap to implement and customizable. For larger games, dedicated sound effect generators like jsfxr (a JavaScript port of the bfxr tool) provide visual interfaces for tuning procedural sounds.

Spatial audio with PannerNode

If your game has a sense of space — left, right, distance — the PannerNode places sounds in a 3D environment.

const panner = audioCtx.createPanner();
panner.positionX.value = playerX - sourceX;
panner.connect(sfxGain);

source.connect(panner);

For 2D games, the simpler StereoPannerNode handles left-right panning without 3D math.

const stereoPanner = audioCtx.createStereoPanner();
stereoPanner.pan.value = -1; // full left
// or 1 for full right, 0 for center
source.connect(stereoPanner);
stereoPanner.connect(sfxGain);

Performance considerations

The Web Audio API is fast, but a few practices matter for game audio.

  • Pre-decode all sounds. Decoding during gameplay causes hitches. Load and decode during a loading screen.
  • Create source nodes on demand. Reusing source nodes is not allowed — each play creates a new one. The cost is minimal.
  • Limit simultaneous sounds. A few dozen concurrent sources is fine; thousands will lag. Cap rapid-fire effects with a cooldown.
  • Use buffers, not Audio elements. The HTMLAudioElement is fine for streaming long music tracks but is slower and less flexible than AudioBufferSourceNode for short effects.

Putting it together

A minimal complete audio system looks like this:

const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const masterGain = audioCtx.createGain();
const sfxGain = audioCtx.createGain();
const musicGain = audioCtx.createGain();

masterGain.connect(audioCtx.destination);
sfxGain.connect(masterGain);
musicGain.connect(masterGain);

document.addEventListener('click', () => audioCtx.resume(), { once: true });

const sounds = {};
async function loadAll() {
  sounds.jump = await loadSound('/audio/jump.wav');
  sounds.coin = await loadSound('/audio/coin.wav');
  sounds.bgm  = await loadSound('/audio/bgm.mp3');
}

function play(name, volume = 1.0) {
  const source = audioCtx.createBufferSource();
  const gain   = audioCtx.createGain();
  source.buffer  = sounds[name];
  gain.gain.value = volume;
  source.connect(gain);
  gain.connect(sfxGain);
  source.start();
}

That is the whole framework. Wire play('jump') into your jump handler, play('coin') into your pickup handler, and you have audio.

Where to get sounds

Free sound effect libraries with permissive licenses:

  • freesound.org: Massive Creative Commons library; check license per file.
  • opengameart.org: Games-specific assets including sound packs.
  • bfxr.net / jsfxr: Procedural sound effect generators with browser UIs.
  • kenney.nl: Curated game asset packs, audio included, public domain.

For game-development context beyond audio, our Phaser tutorial for beginners walks through the broader process of building a browser game. Once the sound is wired up, you can focus on the game itself — or take a break with the Chrome Dino game, which uses Web Audio for its tiny library of sound effects.

Frequently asked questions

Why doesn’t audio play until I click?

Browsers block audio playback until the user interacts with the page, as an anti-autoplay measure. Your AudioContext starts in a suspended state and must be resumed in a click or keydown handler. This is the single most common source of silent-game bugs.

Should I use the Audio element or the Web Audio API?

For short sound effects, use the Web Audio API — it is faster, more flexible, and handles overlapping playback cleanly. For long streaming music where you do not need processing, the HTMLAudioElement is simpler and uses less memory.

What audio formats should I use?

WAV for short sound effects (small files, fast decode), MP3 or OGG for music (smaller files, longer durations). Modern browsers handle all three; OGG has the widest free-codec support, MP3 the widest legacy support.

How do I add reverb or other effects?

The Web Audio API includes ConvolverNode for reverb, BiquadFilterNode for EQ and filtering, DelayNode for echo, and DynamicsCompressorNode for compression. Chain them between the source and the destination in the audio graph.

Can I generate music programmatically?

Yes. Oscillators, envelopes, and the AudioContext’s precise timing let you build sequencers in JavaScript. Libraries like Tone.js layer a friendlier API on top of the Web Audio API specifically for music generation.

The takeaway

Web Audio gives every browser a complete audio engine with no library required. AudioContext, oscillators, buffer sources, and gain nodes cover nearly every game-audio need. For more on building browser games end-to-end, our Phaser beginners’ guide picks up where this article leaves off. And if you need a one-button arcade test target for your audio code, the T-Rex Runner is a working browser game you can study directly.