Lesson 11

Audio: make a browser game feel responsive without annoying the player.

Sound is not decoration. In a game it confirms input, rewards progress, warns about danger and makes silent canvas actions feel physical. The trick is to start small, keep volume controlled and never fight browser autoplay rules.

IntermediateWeb AudioFeedback30-45 minutes

1. Why Web Audio fits small games

For short browser games, Web Audio is often better than loading many tiny audio files. You can synthesize quick tones for jumping, shooting, upgrading, losing a life or completing a wave. The file stays lightweight, the effect responds instantly, and you can adjust pitch or duration in code. This is especially useful for Supagames-style games where many pages are self-contained HTML or small JavaScript bundles.

The most important rule: create or resume audio only after a user gesture. Browsers intentionally block sound that starts by itself. Treat that as good design. A player who clicks Start, taps the canvas or presses a control has given you a natural place to unlock sound.

class AudioManager {
    constructor() {
        this.ctx = null;
        this.sfxGain = null;
        this.initialized = false;
        this.muted = false;
    }

    init() {
        if (this.initialized) return;
        const AudioContext = window.AudioContext || window.webkitAudioContext;
        this.ctx = new AudioContext();
        this.sfxGain = this.ctx.createGain();
        this.sfxGain.gain.value = 0.14;
        this.sfxGain.connect(this.ctx.destination);
        this.initialized = true;
    }

    tone(type, freq, freqEnd, duration, volume) {
        if (!this.initialized || this.muted) return;
        const osc = this.ctx.createOscillator();
        const gain = this.ctx.createGain();
        osc.type = type;
        osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
        if (freqEnd) {
            osc.frequency.exponentialRampToValueAtTime(freqEnd, this.ctx.currentTime + duration * 0.8);
        }
        gain.gain.setValueAtTime(volume, this.ctx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration);
        osc.connect(gain);
        gain.connect(this.sfxGain);
        osc.start();
        osc.stop(this.ctx.currentTime + duration);
    }

    jump() { this.tone("triangle", 420, 760, 0.12, 0.08); }
    hit() { this.tone("sawtooth", 160, 70, 0.16, 0.1); }
    collect() { this.tone("sine", 720, 1100, 0.1, 0.07); }
}

Source pattern: Supagames big games such as Guardian Towers use a compact `AudioManager` with one `AudioContext`, a gain node and short synthesized SFX.

2. Unlock audio from input, not from page load

A common mistake is calling `new AudioContext()` during script loading and then wondering why nothing plays. Put `audio.init()` inside Start, pointer down, key down or a menu button. If the player uses touch controls, the first touch can also unlock audio. Keep this centralized so every scene does not create a new audio context.

const audio = new AudioManager();

function startGame() {
    audio.init();
    state.running = true;
    state.paused = false;
    audio.collect();
}

canvas.addEventListener("pointerdown", function (event) {
    audio.init();
    if (!state.running) startGame();
    else playerFlapOrShoot(event);
});

Do not make the user hunt for a separate "enable sound" step before playing. Starting the game is already a clear gesture. If audio initialization fails, catch the error and keep the game playable. Silence is better than a crash.

3. Use sound as information

The best SFX answer a gameplay question. Did my click register? Did I take damage? Did the upgrade happen? Did the enemy wave start? If two events are similar, give them related sounds. If one event is dangerous, make it lower, shorter or harsher. A jump can rise in pitch, a hit can fall, and a reward can play two quick notes.

const sfx = {
    build() {
        audio.tone("sine", 300, 520, 0.15, 0.09);
    },
    warning() {
        audio.tone("square", 220, 180, 0.09, 0.08);
        setTimeout(() => audio.tone("square", 220, 180, 0.09, 0.08), 120);
    },
    victory() {
        [420, 540, 680, 880].forEach((freq, i) => {
            setTimeout(() => audio.tone("sine", freq, null, 0.18, 0.08), i * 90);
        });
    }
};

Small pitch families help the player learn. Coins can be bright sine tones, damage can be rough sawtooth tones, movement can be soft triangles. You do not need a full orchestra; you need consistent language.

4. Volume, mute and saved preferences

Audio must be respectful. Keep default volume modest, add a mute toggle, and save it. This matters on desktop, but it matters even more on phones where players may be in public. You can store only a boolean and still provide a much better experience.

function loadAudioSettings() {
    audio.muted = localStorage.getItem("supagames_audio_muted") === "1";
    if (audio.sfxGain) audio.sfxGain.gain.value = audio.muted ? 0 : 0.14;
}

function toggleMute() {
    audio.init();
    audio.muted = !audio.muted;
    localStorage.setItem("supagames_audio_muted", audio.muted ? "1" : "0");
    audio.sfxGain.gain.value = audio.muted ? 0 : 0.14;
}

For longer games, consider separate sliders for music and effects. For lightweight games, one toggle is enough. The important part is that the setting survives restart and never blocks play.

5. Avoid audio bugs that feel random

  • Do not create a new `AudioContext` for every shot or jump. Create one manager and reuse it.
  • Do not start long looping music before the player presses Start.
  • Do not let many overlapping effects stack to painful volume. Route through a gain node.
  • Do not call audio code from tests or static page checks unless the environment has browser APIs.
  • Do not treat sound as required feedback. Also show visual feedback for players with muted audio.

Sound should make the game richer, not fragile. If a player blocks audio, the game should still explain itself through animation, text, color, screen shake or particles.

6. A practical sound plan for a new game

Start with five effects: input accepted, collect reward, take damage, complete objective and fail. Add more only after the core game is fun. Then tune duration and pitch while playing, because values that look reasonable in code can feel sharp or weak during actual movement.

For a 30-minute big game, audio becomes part of pacing. Quiet exploration, sharper quest completion, a short warning before danger and a calmer ending cue can make the story feel intentional. For a one-minute arcade game, keep it punchy and minimal. In both cases, the sound map should match the player's mental map of the game.

Previous: Publishing checklist. Next: Dialogue, quests and story state.