npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@schmooky/zvuk

v1.14.0

Published

Audio Engine for the Web — Wwise-grade routing, sprites, sidechain ducking. Tiny, ESM-only, type-safe.

Downloads

1,596

Readme

@schmooky/zvuk

Audio engine for the web. Wwise-grade routing, sprites, sidechain ducking, snapshots and music stems, in a tiny type-safe ESM package.

npm version bundle size types license CI

Docs · Quickstart · Concepts · FX · API · llms.txt


Install

pnpm add @schmooky/zvuk      # or npm i / yarn add / bun add
import { createEngine } from '@schmooky/zvuk';

const engine = createEngine({
  buses: {
    music: { level: 0.8 },
    sfx:   { level: 1.0 },
  },
  master: { headroom: -3, limiter: { threshold: -1 } },
});

await engine.unlock();                                   // call from a user gesture
await engine.loadSound('coin', ['/sfx/coin.webm', '/sfx/coin.m4a'], { bus: 'sfx' });

const v = engine.sound('coin').play({ volume: { jitter: 0.05 } });
await v.fade({ to: 0, duration: 0.8 });

engine.bus('music').fadeTo(0.1, 0.8);

Every time-valued option in zvuk is in seconds, matching the Web Audio API.

Why zvuk?

HTMLAudioElement doesn't survive past one menu blip. The Web Audio API does, and then you write your own bus graph, your own scheduler, your own iOS unlock dance, your own codec ladder, your own sidechain envelope and your own snapshot crossfader. That happens on every project I've shipped sound for, from scratch each time.

zvuk is that layer, done once. It gives you the routing primitives a game-audio team already reaches for in Wwise, FMOD or RAD, without the 60 MB editor, behind an API small enough to keep in your head.

  • Tiny and honest. 17 kB min+gzip for the whole library, 15 kB if you only ever call createEngine. Zero runtime dependencies. The FX classes (Reverb, Compressor, Filter, Ducker, StretchProcessor) drop out when unused; the engine core does not, because it reaches every source type. CI fails the build above 18 kB full / 16 kB core.
  • ESM-only and TypeScript-strict. Typed sound names, typed bus names, no any at the edges.
  • No magic. The AudioContext is lazy, so importing the package does nothing. Lifecycle is explicit, and the real Web Audio nodes are reachable when you need them.
  • Built for slot and casino audio, useful anywhere sound matters.

Features

| Mix & routing | FX & processing | Loading & sources | Developer experience | | --- | --- | --- | --- | | Named buses with FX inserts | Compressor (DynamicsCompressor + makeup) | Codec ladder, ['x.webm', 'x.m4a'] | Lazy AudioContext, no constructor side effects | | Master headroom + soft limiter | Filter, BiquadFilter, all 6 modes | Audio sprites, one buffer, N regions | iOS Safari unlock + visibility resume | | Voice concurrency + stealing | Reverb, convolution + synthetic IR | Variants, random / no-repeat / shuffle-bag | Audio-clock scheduler (scheduleAt) | | Sidechain ducking (Ducker) | Pitch-preserving time-stretch + realtime varispeed | Music stems: intro, loop, outro + skipToOutro | Async cues async-iterator on every Voice | | Snapshots, capture & crossfade the mix | Spatializer, 2D pan + 3D HRTF | Stream long media via MediaElementSource | AbortSignal cancellation everywhere | | Aux sends + bus groups | Bring-your-own FxInsert contract | Batch preload with progress + loudness normalize | "Did you mean…?" hints on bus/sound typos | | Parameter macros, bind any value to a 0..1 control | | | Zero deps, provenance-signed npm releases |

Examples

Variants: kill the machine-gun repeat

await engine.loadVariants('footstep', [
  ['/sfx/step-1.webm', '/sfx/step-1.m4a'],
  ['/sfx/step-2.webm', '/sfx/step-2.m4a'],
  ['/sfx/step-3.webm', '/sfx/step-3.m4a'],
], { strategy: 'shuffle-bag', bus: 'sfx' });

// Every trigger fires a different take, the classic slot coin/win pattern.
engine.variants('footstep').play({ volume: { jitter: 0.04 } });

Music stems: intro, loop, outro

const theme = await engine.loadMusic('level-1', {
  intro: ['/music/intro.webm', '/music/intro.m4a'],
  loop:  ['/music/loop.webm',  '/music/loop.m4a'],
  outro: ['/music/outro.webm', '/music/outro.m4a'],
}, { bus: 'music', loopCrossfade: 0.1 });

theme.play({ fadeIn: 1.2 });            // intro plays, then loops forever
theme.skipToOutro({ at: 'loop-end' });  // on win/level-end: finish the bar, then resolve

// Need two flat tracks instead? engine.crossfade('a', 'b', { duration: 1.5 }).

Audio sprites: one buffer, many regions

await engine.loadSprite('cascade', '/sfx/cascade.webm', {
  small:  { start: 0,    duration: 0.2 },
  medium: { start: 0.25, duration: 0.4 },
  big:    { start: 0.7,  duration: 0.6 },
}, { bus: 'sfx' });

engine.sprite('cascade').play('medium', { volume: { jitter: 0.05 } });

Sidechain ducking: music breathes under dialogue

import { Ducker } from '@schmooky/zvuk';

// Keyed from the `voice` bus; added to (and therefore ducks) the `music` bus.
const ducker = new Ducker(engine.context, engine.bus('voice'), {
  amount: 0.7, attack: 0.08, release: 0.6,
});
engine.bus('music').addFx(ducker);

Parameter macros: one knob, many targets

const intensity = engine.parameter('intensity', 0);

intensity.bindTo((v) => { engine.bus('music').level = v; }, { from: 0.4, to: 1 });
intensity.bindTo((v) => { engine.bus('drone').level = v; }, { from: 0,   to: 0.6 });

intensity.set(0.85);   // both buses ramp, eased; .subscribe() to drive anything else

Live spatial audio: hold the Voice, steer it per frame

const v = engine.sound('engine-loop').play({
  loop: true,
  spatializer: { position: [0, 0, 0] },
});

requestAnimationFrame(function tick() {
  v.spatializer?.setPosition(player.x, 0, player.z);
  requestAnimationFrame(tick);
});

Snapshots: capture the mix, crossfade back to it

const calm = engine.captureSnapshot('calm');

engine.bus('music').fadeTo(0.2, 0.2);
engine.bus('voice').fadeTo(1.5, 0.2);

await calm.apply({ fade: 0.6 });   // restore the whole mix in one call

More copy-paste recipes live in the docs.

Browser support

| Browser | Minimum | Notes | | -------------------- | ------- | ----- | | Chrome, Edge, Opera | 76+ | Opus + AAC, AudioWorklet, HRTF Spatializer | | Firefox | 88+ | Opus + AAC | | Safari macOS | 14.1+ | Opus from 14.5; pickSource falls back to AAC on older | | Safari iOS | 14.5+ | Same. Ship a webm + m4a pair via the codec ladder |

zvuk is ESM-only and assumes a working AudioContext. No polyfills, no IE shims.

Documentation

The full site lives at zvuk.schmooky.dev:

  • Quickstart gets sound playing in about 30 lines.
  • Concepts covers Engine, Bus, Sound, Voice, Snapshot, Spatializer, Concurrency and Parameter.
  • FX covers Compressor, Filter, Reverb, pitch and time-stretch, and Ducker.
  • Guides covers asset formats, loading, building your mix, ducking, and migrating from Howler.
  • API reference is auto-generated TypeDoc, regenerated each build.
  • llms.txt and llms-full.txt are the machine-readable index and the whole corpus.

Every Concept page embeds a live React island running the actual engine. Three buses, six samples, a voice counter that moves.

CLI

npx @schmooky/zvuk transcode raw/*.wav   # ffmpeg ladder → webm/opus + m4a/aac
npx @schmooky/zvuk gen bank.json         # typed sound-name module from a manifest

Contributing

pnpm install
pnpm test            # vitest, happy-dom + Web Audio mock
pnpm typecheck       # tsc --noEmit across src/ and test/
pnpm lint            # biome check
pnpm build           # tsup → dist/index.js + dist/cli.js + dist/index.d.ts
pnpm docs:dev        # astro dev at http://localhost:4321
pnpm bench           # vitest bench/
zvuk/
├── src/          package source
├── test/         vitest suite
├── bench/        vitest benchmarks
├── examples/     vanilla deployable demos (slot-machine, match-3, fps-footsteps)
├── docs/         Astro site → zvuk.schmooky.dev
└── tsup.config.ts

The package's exports resolve to src/index.ts for workspace consumers; publishConfig swaps it to dist/… at publish time so npm consumers get the compiled artifact.

Release flow

Releases run on Changesets:

  1. Run pnpm changeset and describe your change.
  2. Open a PR; CI gates on lint + typecheck + tests + lib build + docs build.
  3. On merge to main, the workflow opens/updates a Version Packages PR that bumps the version and regenerates CHANGELOG.md.
  4. Merging it publishes to npm with provenance via OIDC trusted publishing (no NPM_TOKEN), and cuts a GitHub Release.

Pushing any non-main branch, or running release.yml via workflow_dispatch, snapshot-publishes the pending changesets under a branch dist-tag. Install one with pnpm add @schmooky/zvuk@<branch>.

Credits

Demo audio lives under docs/public/audio/ and backs the interactive demos on the documentation site. None of it ships in the npm package. Sources, licences and the normalisation applied are recorded in docs/public/audio/CREDITS.md.

License

MIT © schmooky