LLearnCode
Academy/games/Production game systems

intermediate · 23 minute study

Audio is a resource graph with a lifecycle

Silent music, stacked sound effects and noise after leaving a game often come from ownership and browser lifecycle mistakes, not from the volume setting.

Loading progressEvidence grows with practice
Useful preparation · 1 prerequisite

Use this as a map, not a lock. If these ideas are familiar, continue.

First, build a mental model

An audio graph connects sources through controls to an output. A source has a lifetime, the context has a state, and the player has preferences. Treat all three as explicit state rather than a sound command that always succeeds.

  1. Start or resume audio from a user gesture according to browser autoplay policy. A suspended context can have a correctly connected graph and still produce no sound. Surface a failed resume instead of claiming playback began.
  2. Separate music and effects through gain nodes so a player's settings can control each channel. Changing a visual mute icon must update the actual audio graph as well as the saved preference.
  3. A scheduled oscillator or buffer source is not a reusable play button. Create the appropriate new source for a new sound; reuse shared decoded data or channel nodes when ownership allows it.
  4. Use an envelope to avoid sudden jumps in amplitude. Schedule against the audio context's clock for audio timing; do not assume an animation-frame callback is an accurate audio scheduler.
  5. Stop and disconnect owned sources when they end or their scene is disposed. Define what pause, blur and navigation mean for music and effects. Do not close a context still shared by another owner.
Read the codejavascript · educational example
let audio;
async function playBeep() {
  audio ??= new AudioContext();
  if (audio.state === "suspended") await audio.resume();
  const source = audio.createOscillator();
  const gain = audio.createGain();
  const start = audio.currentTime;
  source.frequency.value = 440;
  gain.gain.setValueAtTime(0, start);
  gain.gain.linearRampToValueAtTime(0.08, start + 0.01);
  gain.gain.linearRampToValueAtTime(0, start + 0.15);
  source.connect(gain).connect(audio.destination);
  source.addEventListener("ended", () => { source.disconnect(); gain.disconnect(); });
  source.start(start);
  source.stop(start + 0.16);
}
// Call playBeep from a labelled user-gesture control and handle rejection.
// The lesson does not execute or autoplay this audio.
  1. Gesture

    A player chooses to play. The function creates or resumes the context before scheduling a source.

  2. Envelope

    The gain rises briefly, then falls to zero. Scheduling uses audio.currentTime rather than frame count.

  3. End

    The source stops and its owned graph connections are released. Shared context ownership is preserved.

Production thinking

Take this beyond the example.

Provide accessible mute and volume controls, preserve preferences, and avoid autoplaying the lesson or game at full volume. Test first play, resume, navigation, repeated scene entry and failure to load. Listening is still required to assess balance; a source-code check cannot establish that music sounds right.

Sources and further reading

Your evidence

unseen is a local learning signal, not a credential or proof of production competence.

Explain the mental model, then explicitly acknowledge it. Opening a page is not evidence.

Supported retries can complete this lesson. Independent evidence and delayed review determine the skill stage.

Next lesson · A save is untrusted, versioned input →