Sound
A loaded sample. Owns one decoded AudioBuffer and spawns Voices on play().
Fire the same coin sound eight times in one cascade and you want eight independent playbacks, each with its own volume, pitch and stop time, from one download.
A Sound is the decoded, immutable
representation of an audio asset. Every call to sound.play()
spawns a fresh Voice on the sound's default
bus, or on one you name. The buffer is shared across all of them; the
voices are not.
API surface
class Sound {
readonly name: string;
readonly duration: number; // seconds
play(options?: PlayOptions): Voice;
}
interface PlayOptions {
volume?: number | { jitter?: number };
pitch?: number | { jitter?: number };
loop?: boolean;
bus?: string;
priority?: number;
signal?: AbortSignal;
spatializer?: { pan?: number; position?: [number, number, number] };
} Live demo
engine.sound("hit").play()
Recipes
Load + play
const sword = await engine.loadSound('sword', '/sfx/sword.webm', { bus: 'sfx' });
console.log(sword.duration); // seconds Codec ladder for cross-browser
// Codec ladder — first decodable wins. Opus everywhere except old iOS, AAC there.
await engine.loadSound('coin', [
'/sfx/coin.webm',
'/sfx/coin.m4a',
], { bus: 'sfx' }); See the asset-formats guide for the encoding pipeline.
Spawn many voices from one sound
// One Sound, many Voices — buffer is shared, each play() is independent.
const coin = engine.sound('coin');
for (let i = 0; i < 8; i++) coin.play({ pitch: { jitter: 0.05 } }); Tie playback to an AbortSignal
const ac = new AbortController();
const v = engine.sound('coin').play({ signal: ac.signal });
// later, e.g. on unmount:
ac.abort(); Loudness-normalize on load
RMS-based normalization runs once per buffer at decode time. It takes the edge off a library where every asset was mastered by a different person at a different level, and it never touches the file on disk.
// Pass { normalize: true } to apply RMS-target loudness at decode time.
// All normalized sounds will sit at the same perceived loudness.
await engine.loadSound('crowd', '/sfx/crowd.webm', {
bus: 'sfx',
normalize: true,
});
// Or tune the target — defaults are RMS 0.1 (~ -20 dBFS), peak ceiling 0.99.
await engine.loadSound('alert', '/sfx/alert.webm', {
normalize: { targetRms: 0.15, peakCeiling: 0.95 },
}); Audio sprites
Engine has the full sprite story.
Briefly, loadSprite shares the same fetch, decode and cache
path as loadSound, then maps named regions onto the
result.
// One buffer, many regions — drop in a sprite when you'd otherwise load
// 5+ tiny one-shots that share a session/scene.
await engine.loadSprite('ui', '/sfx/ui-strip.webm', {
click: { start: 0, duration: 0.04 },
hover: { start: 0.1, duration: 0.05 },
error: { start: 0.2, duration: 0.18 },
success: { start: 0.5, duration: 0.3 },
}, { bus: 'sfx' });
engine.sprite('ui').play('click'); Pitfalls
play() is synchronous and returns the Voice. Awaiting waits forever
(it's not a Promise). Use v.ended to await completion.
Related
- Voice is what
play()returns. - Loading sounds covers bulk load patterns.
- Asset formats covers the codec ladder.