# Migrating from Howler · zvuk

zvuk v1.14.1 · MIT · https://zvuk.schmooky.dev/guides/migration/

If your codebase calls Howl + Howler.volume directly, here's the shape of the port.

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 '@schmooky/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, duration: 0.8 });
```

## 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, duration: 0 })`

`howl.fade(from, to, ms)`

`voice.fade({ to, duration, curve })` — duration in seconds

`Howler.volume(0.8)`

lower a bus level (`engine.bus('sfx').level = 0.8`), or set master headroom at construction via `createEngine({ master: { headroom: -2 } })`

`howl.on('end', fn)`

`await voice.ended` or `for await of voice.cues()`

`howl.stop()`

`voice.stop()`

`howl.unload()`

`engine.unloadSound(name)` (drop one sound + evict its buffer); `engine.close()` tears down the whole engine

``   ## 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 decodes into Web Audio buffers. For very long music files, use `engine.loadStream(name, url)` / `engine.stream(name)`, which streams via `HTMLAudioElement` + `MediaElementAudioSource` (volume + loop only, not the full Voice API). -   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.   ``
