# Sound · zvuk

zvuk v1.14.1 · MIT · https://zvuk.schmooky.dev/concepts/sound/

A loaded sample. Owns one decoded AudioBuffer and spawns Voices on play().

Concept

# 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`](/api/Sound/) is the decoded, immutable representation of an audio asset. Every call to `sound.play()` spawns a fresh [Voice](/concepts/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

Sound + PlayOptions ts

```
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

Use your own sound

Play sound

Unlock & load

engine.sound("hit").play()

## Recipes

### Load + play

ts ts

```
const sword = await engine.loadSound('sword', '/sfx/sword.webm', { bus: 'sfx' });
console.log(sword.duration);   // seconds
```

### Codec ladder for cross-browser

ts ts

```
// 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](/guides/asset-formats/) for the encoding pipeline.

### Spawn many voices from one sound

ts ts

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

ts ts

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

ts ts

```
// 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](/concepts/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.

ts ts

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

Don't store the AudioBuffer yourself.

The Sound owns it. If you read raw bytes for visualization, copy them out and let the Sound stay the source of truth.

Don't await sound.play().

`play()` is synchronous and returns the Voice. Awaiting waits forever (it's not a Promise). Use `v.ended` to await completion.

## Related

-   [Voice](/concepts/voice/) is what `play()` returns.
-   [Loading sounds](/guides/loading/) covers bulk load patterns.
-   [Asset formats](/guides/asset-formats/) covers the codec ladder.
