Skip to content

Sound ​

Play sound effects from files, stream music, write music and effects in code with 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 ​

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 });
OptionDoes
volumeGain for this sound, 1 by default.
ratePlayback speed; 1 is the original pitch, 2 an octave up.
rateJitterA random amount added to or taken from rate on each play. 0.05 keeps a sound you hear often from repeating exactly.
panStereo position from -1 (left) to 1 (right).
loopRepeat until stopped.
bus"sfx" (default) or "music". See 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 ​

Music and ambience files are large, and decode holds the whole sound in memory. stream plays them through an <audio> 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 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

From 0.24.2, coding agents use plonk when the game needs sound and you haven't named another tool (Making sound).

Placeholder blips ​

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
OptionDoes
freq, freqEndStart and end pitch in Hz.
attack, decayFade-in and fade-out time in seconds.
wave"square" (default), "sine", "triangle", "sawtooth" or "noise".
volume, busAs for play; volume is 0.3 by default.

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 ​

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

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). Whether it sounds right is still a job for your ears.

API reference