Quickstart
Two function calls and a user gesture.
1. Install
pnpm add @schmooky/zvuk 2. Construct the engine
Declare your buses up front. createEngine does not touch the
AudioContext. It records the shape of the mix and nothing else, so calling
it at module load is safe.
import { createEngine } from '@schmooky/zvuk';
export const engine = createEngine({
buses: {
music: { level: 0.8 },
sfx: { level: 1.0 },
ui: { level: 0.7 },
},
master: { headroom: -3 }, // dB
}); 3. Unlock from a user gesture
Browsers block audio until the user does something. Call
engine.unlock() from a click, tap or keypress handler. It is
idempotent, so calling it from several handlers is fine.
document.querySelector('button')!.addEventListener('click', async () => {
await engine.unlock();
await engine.loadSound('coin', ['/sfx/coin.webm', '/sfx/coin.m4a'], { bus: 'sfx' });
engine.sound('coin').play();
}); Pass an array of URLs to ship a codec ladder. The first one your browser can decode wins. Asset formats explains why that matters more than it sounds.
4. Play, fade, mix
Every time-valued option in zvuk is in seconds. Fades, crossfades, attack and release, snapshot applies, all of them. That matches the Web Audio API, so there is one convention to remember rather than two.
const v = engine.sound('coin').play({
volume: { jitter: 0.05 },
pitch: { jitter: 0.04 },
});
// Bus-level fade — click-free (duration in seconds)
engine.bus('music').fadeTo(0.1, 0.8);
// Voice-level fade with curve
await v.fade({ to: 0, duration: 0.8, curve: 'equal-power' });
await v.ended; 5. Clean up
On an SPA route change or a game teardown, call
engine.close(). Every voice stops, the AudioContext closes,
and listeners detach. Close is terminal. Construct a new engine if you
need audio again.
await engine.close(); Where to go next
- The Engine covers the full lifecycle, state machine and scheduler.
- Bus covers routing, concurrency and sidechain.
- Recipes has six patterns you can click and copy.
- Asset formats walks through webm/Opus and m4a/AAC.