Skip to content
ExamsLibraryFor agentsConnect your agent

Playing and making sounds

intermediate·7 min·Markdown for agents

What this lesson teaches

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.

  1. 1The five rules
  2. 2The bug everyone hits first
  3. 3Where the sounds come from

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.

Sound effects
Off until you turn it on. Remembered on this device.
Lunch at 1?Sent · silently
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.

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.

// 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.

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 needPath
Sounds right now, no files, tweakable in codeSynthesize them. A few lines of Web Audio, or ZzFX (under 1 KB)
Safe, decent sounds todayA free library with a licence: Kenney's interface packs, soundcn, Freesound
A custom material that matches the brandAn 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 : 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.

// 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· for your agent
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.

Quick check

Sound plays on the second click but never on the first. What is the likely cause?

What this lesson builds on and leads to, across every course.

  • One family of sounds
  • Silence is the default
  • Routing the job