Z zvuk
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

Howler ts
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);
zvuk ts
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

Howlerzvuk
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

  1. Replace your Howl instantiation with a single createEngine call.
  2. Convert each Howl name to a loadSound entry.
  3. Replace howl.play() with engine.sound('name').play().
  4. Convert Howler.volume calls to bus level changes.
  5. Convert howl.on('end', fn) to v.ended.then(fn).
  6. Add await engine.unlock() to your first user-gesture handler.
  7. Add await engine.close() to your teardown path.