# Sound

Play sound effects from files, stream music, write music and effects in code with [plonk](/plonk/), and let players set the volume. Everything goes through one `Audio` from `@debuffjp/audio`, so a single volume setting covers it all.

```ts
import { Audio } from "@debuffjp/audio";

const audio = new Audio(); // one per game
```

Browsers keep a page silent until the player clicks, taps or presses a key. `Audio` starts on that first input by itself, so you don't need to handle it. `audio.unlocked` is `true` once sound is allowed. A title screen that waits for a key or a click covers it for most games.

## Sound files {#files}

Put the files in `assets/` like any other asset, load them, then decode each one once:

```ts
import { Assets } from "@debuffjp/assets";

const assets = new Assets(await (await fetch("assets/manifest.json")).json());
await assets.load("default");
await audio.decode("coin", assets.audio("coin")); // assets/coin.ogg

audio.play("coin");
audio.play("coin", { volume: 0.8, rateJitter: 0.05 });
```

| Option | Does |
| --- | --- |
| `volume` | Gain for this sound, 1 by default. |
| `rate` | Playback speed; 1 is the original pitch, 2 an octave up. |
| `rateJitter` | A random amount added to or taken from `rate` on each play. 0.05 keeps a sound you hear often from repeating exactly. |
| `pan` | Stereo position from -1 (left) to 1 (right). |
| `loop` | Repeat until stopped. |
| `bus` | `"sfx"` (default) or `"music"`. See [Volume](#volume). |

`play` returns a voice. Keep it to stop a looping sound, with an optional fade in seconds:

```ts
const engine = audio.play("engine", { loop: true, volume: 0.4 });
engine.stop(0.3);
```

Calling `play` with an id that wasn't decoded throws, naming the `decode` call you missed.

## Long tracks {#streaming}

Music and ambience files are large, and `decode` holds the whole sound in memory. `stream` plays them through an `` element instead: it starts straight away and stays light on phones.

```ts
const music = audio.stream(assets.url("music/theme"), { volume: 0.7 }); // music bus, loops by default

music.volume(0.3, 1); // duck to 0.3 over a second, say under a dialogue box
music.pause();
music.resume();
music.stop(2); // fade out over two seconds
```

Keep long tracks in a folder you never pass to `assets.load()`, such as `assets/music/`, so they aren't downloaded twice: `assets.url` gives the address without fetching the file. A stream started before the first click or key press begins playing on that input.

## Music and effects made in code {#plonk}

[plonk](/plonk/) writes songs as a few lines of data and synthesizes them live, with no audio files. Its sound effects play in the song's key, so a combo that climbs the scale fits the music instead of clashing with it. Install it with `npm install plonkjs`, then point it at the music bus so it follows the game's volume:

```ts
import { Player } from "plonkjs";
import { theme } from "./theme"; // a plonk Song

const player = new Player({ context: audio.ctx, destination: audio.bus("music") });
player.play(theme, { fade: 1 });

const POP = { wave: "sine", drop: 7, decay: 0.12, sustain: 0, volume: 0.3 } as const;
player.sfx(POP, { degree: 5 }); // the fifth note of the song's scale
```

- [Getting started with plonk](/plonk/getting-started) writes a first song.
- [Music that reacts](/plonk/reacting) changes tempo and layers with what happens in the game.
- [Sound design for games](/plonk/sound-design) covers mixing, variation and effects in key.

From 0.24.2, coding agents use plonk when the game needs sound and you haven't named another tool ([Making sound](/docs/agents#making-sound)).

## Placeholder blips {#synth}

`audio.synth` makes a short beep, zap or hit from one oscillator with a pitch slide. Use it to hear that an event fires while the game has no sound yet, then replace it with a file or a plonk effect.

```ts
audio.synth({ freq: 660, freqEnd: 220, decay: 0.15 }); // a falling blip
audio.synth({ wave: "noise", freq: 400, freqEnd: 80, decay: 0.3, volume: 0.2 }); // a crunch
```

| Option | Does |
| --- | --- |
| `freq`, `freqEnd` | Start and end pitch in Hz. |
| `attack`, `decay` | Fade-in and fade-out time in seconds. |
| `wave` | `"square"` (default), `"sine"`, `"triangle"`, `"sawtooth"` or `"noise"`. |
| `volume`, `bus` | As for `play`; `volume` is 0.3 by default. |

## Volume {#volume}

Sounds go through two buses, `"sfx"` and `"music"`, which both feed `"master"`. Set any of them from 0 to 1; the change is smoothed, so a slider doesn't click.

```ts
audio.volume("music", 0.5);
audio.volume("master", muted ? 0 : 1); // mute covers music and sound effects
```

Save the player's choice so it survives a reload, as the platformer template does:

```ts
import { Storage } from "@debuffjp/core";

const save = new Storage("my-game", { volume: 10 });
audio.volume("master", save.get("volume") / 10);

function setVolume(v: number) {
  save.set("volume", Math.max(0, Math.min(10, v)));
  audio.volume("master", save.get("volume") / 10);
}
```

To route your own Web Audio nodes through the game's volume, connect them to `audio.bus("sfx")` or `audio.bus("music")`. `audio.ctx` is the `AudioContext`, and `audio.buffer(id)` returns a decoded sound to play through your own nodes.

## Sound in the world {#positional}

For sounds that come from a place, get quieter with distance and are muffled by walls, use [Earshot](/earshot/). [Earshot with debuff](/earshot/debuff) builds the walls from your tilemap and plays through the sfx bus.

## Testing {#testing}

`debuff test` runs your game without a browser, so nothing is heard: audio calls are accepted and do nothing. Test that a sound is triggered by checking the game state that triggers it. To check music without speakers, plonk lists the notes a song plays as data ([Testing music](/plonk/testing)). Whether it sounds right is still a job for your ears.

[API reference](/api/@debuffjp/audio/)