Game sound effects, and why your game is silent
No error, no sound, and everything else working. The browser is waiting for a click, and once it has one you can make every effect in code.
Jump to a section
Your audio code is probably fine. A browser refuses to make a sound until the player has interacted with the page, so an audio context built while the page loads is built dead. Add a start button, build the context inside that click, and then write every effect in code rather than hunting for files.
Create or resume the audio context inside the handler for a real click, tap or key press. Not on load, not in a timer, not when a fetch comes back. That one move separates a silent game from a loud one, and it is why nearly every browser game opens with a button.
Why your game is silent and nothing says so#
This fault has no symptom to search for. The console stays clean, the rest of the game runs, and the lines that play a sound run too. The browser is holding the audio context in a suspended state, where work piles up and none of it reaches the speaker.
The rule exists because sound that starts by itself is hostile. A page that blares at you from a background tab is the behaviour browsers spent years stamping out. The answer was to demand a gesture first, and a gesture means a click, a tap or a key press that the person actually made.
- Nothing is logged. A suspended context is a normal state rather than a fault, so there is nothing to report. Silence next to a clean console points here long before it points at your own code.
- Everything else works. Scoring, collisions, drawing, saving. Audio alone is missing, because audio alone needs permission.
- It works if you click first. Click an empty part of the page, then make the game fire a sound. If that one arrives, the gesture was the missing ingredient.
- It may work for you and nobody else. Browsers treat a site you open twenty times a day differently from a cold first visit, and you are the person opening it twenty times a day.
So the start button is not decoration. It is the only place the sound can legally come alive. Give it the rest of the first-frame work too: read the saved high score, build the level, show the controls. Work hidden behind a button is work the player never sees.
Paste this into the game you already have#
This is written for a game that already runs and makes no noise. It asks for the gesture rule, generated effects, one volume control for all of them and a mute the game remembers.
Add sound to this game. Do not add any audio files and do not add a
library. Every sound is made in code with Web Audio.
GESTURE: nothing may play until the player has interacted with the
page. Put a START button over the game. Create the AudioContext inside
that click handler, call resume() on it, and only then begin play. If
the context is ever suspended afterwards, resume it from the next
click.
MASTER GAIN: one gain node that every sound connects through, and it
is the only thing connected to the destination. Set it near 0.3 so
several effects at once do not clip.
EFFECTS: write one small function taking a waveform, a start
frequency, an end frequency and a duration. Inside it, create an
oscillator and a gain node, connect oscillator to gain to master,
schedule the pitch slide and the gain fade against the context's own
currentTime, then start the node and book its stop. Use it for jump,
coin, hit and game over. Build the explosion from a short buffer of
random samples instead of a tone.
LENGTH: every effect under 200 milliseconds except the explosion and
the game over sound. Ramp the gain down to a tiny value rather than
cutting it off, so nothing clicks at the end.
ONE PER IMPACT: play a sound when a condition becomes true, never
while it stays true. Keep last frame's state and compare. Give
repeating sounds a minimum gap in seconds. Vary the start frequency by
a small random amount so repeats are not identical.
MUTE: a mute button in a corner and the M key. Save the choice in
localStorage and read it back on load, so a player who muted once
stays muted after a reload. Mute sets the master gain to zero rather
than pausing the game.
CONFIG: one object at the top holding the master volume, the mute key,
and the frequency and duration of every effect, each commented.
The gesture paragraph is the one to keep. Everything else is taste, and you will be changing the frequencies within a minute of hearing them. Drop that paragraph and the code still looks right, still runs without errors, and still makes no sound.
Sound effects you write instead of download#
A sound effect is a tone whose pitch moves and whose volume falls away. That is the whole of a coin, a jump or a thud, and a browser can build one out of nothing. No files, no asset folder, no second request, nothing to go missing when the game moves.
An oscillator makes the tone. A gain node in front of it is the volume. Wire the first into the second, wire that into the speakers, then book two changes: where the pitch travels and how fast the volume drops to nothing.
let ctx = null;
let master = null;
// Called from a real click. Until this runs, nothing can be heard.
function startAudio() {
if (!ctx) {
ctx = new AudioContext();
master = ctx.createGain();
master.gain.value = 0.3; // headroom, because effects overlap
master.connect(ctx.destination);
}
ctx.resume(); // safe to call again later
}
// oscillator -> envelope -> master -> speakers
function blip(type, from, to, seconds, peak = 0.6) {
if (!ctx) return; // no gesture yet, so nothing to do
const t = ctx.currentTime; // the audio clock, not Date.now()
const osc = ctx.createOscillator();
const env = ctx.createGain();
osc.type = type; // square, sine, sawtooth, triangle
osc.frequency.setValueAtTime(from, t);
osc.frequency.exponentialRampToValueAtTime(to, t + seconds);
// Ramp to almost zero, never to zero: the exponential ramp needs a
// positive target, and a hard stop is an audible click anyway.
env.gain.setValueAtTime(peak, t);
env.gain.exponentialRampToValueAtTime(0.0001, t + seconds);
osc.connect(env);
env.connect(master);
osc.start(t);
osc.stop(t + seconds); // a node is used once and discarded
}
const sfx = {
jump: () => blip('square', 180, 540, 0.12),
coin: () => blip('square', 900, 1600, 0.09),
hit: () => blip('sawtooth', 320, 90, 0.15),
over: () => blip('triangle', 440, 110, 0.6, 0.5),
};
Every time in there is a time on the context's own clock, read once at the top as currentTime. The node is created, connected, started and stopped in the same breath, and then it is rubbish. You do not keep oscillators around and replay them. You make a new one each time, which is cheaper than it sounds.
An explosion is the one that will not come from a tone, because an explosion is noise. Fill a short buffer with random numbers, play it once through the same falling gain, and you have a burst. Run it through a low-pass filter and the hiss turns into a thump.
function burst(seconds = 0.35, peak = 0.5) {
if (!ctx) return;
const t = ctx.currentTime;
const frames = Math.floor(ctx.sampleRate * seconds);
const buf = ctx.createBuffer(1, frames, ctx.sampleRate);
const data = buf.getChannelData(0);
for (let i = 0; i < frames; i++) data[i] = Math.random() * 2 - 1;
const src = ctx.createBufferSource();
src.buffer = buf;
const lp = ctx.createBiquadFilter();
lp.type = 'lowpass';
lp.frequency.setValueAtTime(1800, t);
lp.frequency.exponentialRampToValueAtTime(200, t + seconds);
const env = ctx.createGain();
env.gain.setValueAtTime(peak, t);
env.gain.exponentialRampToValueAtTime(0.0001, t + seconds);
src.connect(lp);
lp.connect(env);
env.connect(master);
src.start(t);
}
Short, and then shorter than that#
Length is where a first attempt goes wrong once the sound exists at all. A half-second whoosh on every jump is charming the first time and unbearable by the second level. Trim the duration before you touch the pitch.
The test is not how an effect sounds once. It is how it sounds thirty times in two minutes, which is what a player who is good at your game will hear. Most of what makes an effect tiring is simply that it lasts longer than the action it belongs to.
| Effect | Rough length | Shape to ask for |
|---|---|---|
| Coin or pickup | 60 to 100 ms | High square tone, pitch sliding up |
| Jump | 100 to 150 ms | Mid tone rising, cut short |
| Hit or damage | 120 to 200 ms | Lower tone falling, harsher waveform |
| Explosion | 300 to 500 ms | Filtered noise, volume falling away |
| Game over | 500 ms to 1 s | Three falling notes, played once |
Numbers to start from rather than rules. Only the game over sound has room to be slow, because the player hears it once and is not doing anything else.
Volume, and the tab mute button#
Run every sound through one gain node on its way out, and let only that node touch the speakers. It gives you one number for the whole game. It also gives you a mute that works, because setting one value to zero silences everything, including the sounds already playing.
What you are avoiding is the player reaching for the tab's own mute. That player is gone for the rest of the session, and they will not come back to turn it on. Nothing in the game can argue with them once they have done it.
- Start quieter than feels right. You have heard your own effects hundreds of times and stopped noticing them. A level around a third of full scale leaves room for several sounds at once without clipping.
- Put the mute where a hand already is. A corner of the screen, visible without pausing, and bound to a key as well. A mute buried in a settings panel is the tab mute with extra steps.
- Remember the choice. Keep the mute state in localStorage and read it back on load. A player who silenced you yesterday has told you something, and asking again tomorrow is rude.
- Mute, do not pause. Setting the master gain to zero is instant and reversible. Tearing the context down means rebuilding it, and rebuilding it means needing another gesture.
One sound per impact, not one per frame#
Hang a sound on a collision test and you have hung it on the frame loop. A player resting on a spike plate is colliding sixty times a second, so the hurt sound fires sixty times a second. Sixty copies of one tone stacked together are not sixty times louder. They are a buzz, and they clip.
The cure is to play on the change rather than on the state. Remember what was true last frame. Fire the sound only where the condition was false then and is true now. That is the moment of impact, and it happens exactly once per impact.
- Edges, not levels. Touching counts as one event. Still touching is not a new event, however many frames it goes on for.
- Give repeats a floor. A footstep every 300 milliseconds while a character walks, not one per frame. Store the time of the last play and compare.
- Cap how many play at once. Twelve bullets leaving on the same frame should sound like a volley, not a wall. Play two or three and drop the rest.
- Nudge the pitch each time. Multiply the start frequency by a small random amount. Identical repeats are most of what makes an effect grating.
Music, and whether you want any#
Music is a different trade, and for a one-file game the honest answer is often none at all. A loop is a file. A file is weight on the first load, a second request, somewhere to host it and a question about who owns it. All of that for a track the player may mute in a minute.
Generated music is the other road, and it is real but modest. A short arpeggio from the same oscillators costs no bytes and no requests. Four notes going round, a steady pulse, and a filter opening and closing will carry a menu screen or an idle game without pretending to be a soundtrack.
Anything timed to a beat is a harder problem than it looks, and it is not this article's. Notes fired from the frame loop drift apart over the length of a song, and the fix is a scheduling habit rather than a setting. The music game guide covers that clock properly, so this one will not repeat it.
Silence is a legitimate choice. Sharp effects over a quiet background beat a loop the player turns off, and no loop at all is one less thing to load.
🔊Games on this site
Common questions#
Why does my browser game make no sound?
Almost always because the audio was set up before the player touched anything. A context built during page load sits suspended, and a suspended context swallows everything sent to it without complaining. Build or resume it inside a click handler and the same code works.
Do I need sound files to add audio to a game?
No. An oscillator and a gain node give you a jump, a coin, a hit and a game over in a few lines each. A buffer of random numbers gives you an explosion. For a single-file game this is the better answer, because there is nothing extra to load and nothing to go missing.
How long should a game sound effect be?
Most of them under 200 milliseconds. A pickup can be as short as 60. Only the game over sound has room to run on, because it plays once at a moment when the player is not doing anything. Judge the length by hearing the effect thirty times, never once.
Why does one hit play the sound dozens of times?
The sound is tied to a collision being true rather than to it becoming true, so it fires on every frame of contact. Keep last frame's answer and play only on the change. A minimum gap between repeats of the same effect catches whatever that misses.
How loud should the effects be?
Quieter than you think, and all of them through one master gain node. Around a third of full scale is a sensible start, because several effects can land on the same frame and a full-scale stack will clip. The real measure is whether anyone reaches for the tab mute.
Should I add background music to a small game?
Only if you can do it without a file. A loop is bytes, hosting and ownership questions for something many players switch off. A short generated arpeggio costs nothing, and silence under good effects is a respectable finish.
Where to go next#
Sound is the cheapest thing you can do to a finished game and the one players notice fastest. When a jump sounds right, publish it. The page records the model and the prompt behind the game, so the next person can see how it was built.
The general method is in how to make a game with AI, and prompts for particular faults live in the prompt guide. The games on Agent Games are worth opening with the volume up, because you can hear within a second which ones bothered.
Built something? Put it on Agent Games.
Publishing takes a file and a minute. Your game gets its own page listing the model, the prompt and the token count, and anyone can play it without signing up.
Read next