# Playing and making sounds

> Wire sound into a web app the right way (unlock on a gesture, off by default, a visual twin for every sound) and make the files with code, a model or a free library.

Module: Sound · Level: intermediate · 7 min · https://school.agents.org.in/media/sound/sound-on-the-web

Here is a tiny chat. Send a message with sound off: you still see it land, with "Sent" under it. Turn sound effects on in the setting above and send again: now a two-note tone joins the checkmark. Reload the page and the setting is still how you left it.

*[Interactive demo: Off by default, remembered, and every sound has a visual twin.]*

That small demo holds the five rules for sound on the web. Each one exists because someone shipped the opposite.

## The five rules

1. **Off by default, with a switch people can find.** In settings, reachable by keyboard, and remembered across visits. This is why sound on the web is trusted at all.
2. **No sound-only information.** Every sound has a visual twin: the checkmark, the toast, the shake. Most people are muted most of the time.
3. **No autoplay, no sound on page load.** A sound the person did not cause is a notification, and notifications need permission.
4. **Respect the device's silence.** On iPhone, the silent switch mutes sounds like yours. Do not play UI sounds in a hidden tab.
5. **Treat the mute switch as reduced motion for your ears.** There is no CSS setting for sound. If someone has asked for reduced motion, keep sound off and skip any "turn on sound?" prompt.

> **In plain words.** Browsers play sound through an AudioContext (The browser's little audio engine that a page creates to play sound. It starts paused until the person clicks or presses a key), a little audio engine your page creates. Browsers keep it **suspended** until the person clicks or presses a key, so pages cannot blast noise at you. Your code has to wake it up inside that first click.

## The bug everyone hits first

Create the audio engine when the page loads and it seems to work, except nothing plays. It starts suspended, and every sound is silently dropped until something wakes it. The symptom: "sound only works on the second click."

**Before:** created at load, on by default, sound on focus.

```ts
// starts suspended
const ctx = new AudioContext();
let enabled = true;
// talks over screen readers
button.onfocus = () =>
  play("tick");
```

**After:** created in the gesture, off until chosen.

```ts
let ctx: AudioContext | null = null;
const saved =
  localStorage.getItem("sound");
let enabled = saved === "on";

// call inside a click or keydown
function unlock() {
  ctx ??= new AudioContext();
  if (ctx.state === "suspended") {
    ctx.resume();
  }
}
button.onclick = () => {
  unlock();
  if (enabled) play("send");
};
```

Two more details. Play each sound from a fresh `AudioBufferSourceNode`; they are cheap, and it is the only web primitive with no start-up delay (an `<audio>` element adds tens of milliseconds). And never attach sound to focus: screen readers are already speaking when focus moves, and your tick lands on top of their voice.

*("Works on the second click" means the context was created at load. Create it in the gesture.)*

## Where the sounds come from

Three paths without paying anyone, and one with a key. Pick by what you need, not by what happens to be installed.

| You need | Path |
|---|---|
| Sounds right now, no files, tweakable in code | **Synthesize them.** A few lines of Web Audio, or ZzFX (under 1 KB) |
| Safe, decent sounds today | **A free library** with a CC0 (A licence that gives a work to the public with no conditions: you may use, edit and sell it without credit. The safest licence for sounds) licence: Kenney's interface packs, soundcn, Freesound |
| A custom material that matches the brand | **An open-weight model** like Stable Audio 3 Small-SFX, which runs on a laptop CPU |
| Iterate in words ("more felt, less glass") | **ElevenLabs** sound effects, with an API key |

### Synthesis

Every sound in this module is procedural (Sound built by code from oscillators and envelopes instead of loaded from a file. Weighs nothing and its pitch and length can follow the UI): built in code, zero bytes of audio. For ticks, taps and two-note tones it beats any model. It is the same every time, and its pitch and length are numbers you can tie to the UI.

```ts
// A tick: 60 ms of high sine, fast decay.
// No file, no fetch.
function tick(ctx: AudioContext,
  freq = 2400, ms = 60) {
  const t = ctx.currentTime;
  const end = t + ms / 1000;
  const o = ctx.createOscillator();
  const g = ctx.createGain();
  o.frequency.value = freq;
  g.gain.setValueAtTime(0.5, t);
  g.gain.exponentialRampToValueAtTime(
    0.001, end);
  o.connect(g).connect(ctx.destination);
  o.start(t);
  o.stop(end);
}
```

### Libraries

Kenney's packs are the most used free UI sounds in existence, which is also their problem: people recognize them. Use them as a starting family and re-pitch or layer. Whatever the source, keep one material. A grab-bag of free sounds is the audio version of the default icon set.

### Models

Prompt any sound model with **material + action + envelope + contour + purpose**, never a UI noun. "A success sound" gets you a stock jingle. This gets you a sound:

**Generate a sound family with ElevenLabs (paste into your agent):**

```text
Generate this family with ElevenLabs text-to-sound-effects (model eleven_text_to_sound_v2), all in one session with the same material phrase:
tick: "single very short tick, fingernail on thin plastic, dry, no tail, bright, one-shot UI feedback"
send: "two quick rising notes on a muted kalimba, warm, dry, short decay, confirmation"
error: "short low dull thud with a slight downward pitch, rubber on wood, dry, no tail"
Use duration_seconds 0.5 and prompt_influence 0.8 or higher. Then trim leading silence to zero, high-pass at 150 Hz, peak at -3 dBFS, mono. Commit the files and record the prompts and settings in a manifest.
```

Say "dry" and "no tail" every time; the default has room echo. Ask for half a second and cut it down. And commit the files: regenerating one sound next month gives you a *different* sound, and a family with one re-rolled member sounds patched.

> **Open source is not the same as commercial use.** Meta's AudioGen code is open, but its weights are non-commercial. Stability's licence changes above US $1M a year in revenue. Some sound packs mix free and restricted files. Write the licence next to every file.

**Check yourself:** Sound plays on the second click but never on the first. What is the likely cause?

- [ ] The file is too large
- [x] The AudioContext was created at page load, so it started suspended and only woke up later
- [ ] The sound is mono
- [ ] The volume is set to -3 dBFS

*Why:* Browsers start an AudioContext suspended until a user gesture. Create and resume it inside the click or keydown handler, before playing.

## In the skill

- https://school.agents.org.in/skill/sound/sound-playback-web.md
- https://school.agents.org.in/skill/sound/sound-generation-open-source.md
- https://school.agents.org.in/skill/sound/sound-generation-elevenlabs.md
