Guide
Migrating from Howler
If your codebase calls Howl + Howler.volume directly, here's the shape of the port.
Mental shift
Howler thinks in terms of one Howl per asset, with that Howl owning playback.
zvuk separates the asset (Sound) from the playback instance
(Voice), and routes every voice through a named bus. That gives
you per-bus volumes, fades, and FX for free.
Side-by-side
import { Howl, Howler } from 'howler';
Howler.volume(0.8);
const coin = new Howl({ src: ['/sfx/coin.mp3'], volume: 0.6 });
coin.play();
coin.fade(0.6, 0, 800); import { createEngine } from 'zvuk';
const engine = createEngine({
buses: { sfx: { level: 0.8 } },
});
await engine.unlock();
await engine.loadSound('coin', ['/sfx/coin.webm', '/sfx/coin.m4a'], { bus: 'sfx' });
const v = engine.sound('coin').play({ volume: 0.6 });
await v.fade({ to: 0, ms: 800 }); Mapping table
| Howler | zvuk |
|---|---|
new Howl({ src }) | engine.loadSound(name, urls, opts) |
howl.play() | engine.sound(name).play() |
howl.volume(0.5) | voice.fade({ to: 0.5, ms: 0 }) |
howl.fade(from, to, ms) | voice.fade({ to, ms, curve }) |
Howler.volume(0.8) | engine.bus('master') — or just lower headroom |
howl.on('end', fn) | await voice.ended or for await of voice.cues() |
howl.stop() | voice.stop() |
howl.unload() | internal; engine.close() tears down the entire context |
What you gain
- Real bus model — group volumes, mute, fade per bus, not per Howl.
- Snapshots — crossfade your whole mix between gameplay states.
- Parameters — drive multiple values from one knob, with curves.
- Concurrency policies — bound polyphony per bus.
- Codec-aware loading — webm/m4a array out of the box.
- Strict TypeScript — no Howler-style any-typed events.
What you lose
- HTML5 mode — zvuk is Web Audio only. For very long music files, use
a future
Stream source (planned). - Spatial audio with Howler's simple
pos() — zvuk's
Spatializer is more capable but slightly more verbose.
Step-by-step migration
- Replace your
Howl instantiation with a single
createEngine call. - Convert each
Howl name to a loadSound entry. - Replace
howl.play() with engine.sound('name').play(). - Convert
Howler.volume calls to bus level changes. - Convert
howl.on('end', fn) to v.ended.then(fn). - Add
await engine.unlock() to your first user-gesture handler. - Add
await engine.close() to your teardown path.