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.13.0

Published

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

Readme

@schmooky/zvuk

Audio Engine for the Web — Wwise-grade routing, audio sprites, sidechain ducking, snapshots, in a tiny ESM package.

npm version bundle size types license CI

Documentation · Concepts · API · Roadmap · llms.txt


Install

pnpm add @schmooky/zvuk
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', { 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);

All time-valued options in zvuk are seconds (matching the Web Audio API).

Why zvuk?

HTMLAudioElement doesn't scale past one menu sound. The Web Audio API does, but you end up writing your own bus graph, your own scheduler, your own iOS unlock dance, your own codec ladder, your own sidechain envelope, your own snapshot crossfader — every time.

zvuk is the layer above. It gives you the routing primitives a real game audio team uses (Wwise / FMOD / RAD) without a 60 MB editor, behind an API surface small enough to fit in your head. ESM-only, type-safe, zero runtime dependencies, ~16 KB min+gzip.

Features

| Mixer | FX | Loading | DX | | --- | --- | --- | --- | | Named buses with FX inserts | Compressor (DynamicsCompressor + makeup) | Codec ladder (['x.webm', 'x.m4a']) | Lazy AudioContext (no constructor side-effects) | | Master headroom + soft limiter (DynamicsCompressor) | Filter (BiquadFilter, all 6 modes) | Audio sprites (one buffer, N regions) | iOS Safari unlock + visibility resume | | Voice concurrency + stealing | Reverb (convolution + synthetic IR) | Stream long media via MediaElementSource | Audio-clock scheduler (scheduleAt) | | Sidechain ducking (Ducker) | Pitch-preserving time-stretch (offline SOLA) + realtime varispeed Worklet | Loudness normalization on load | Async cues iterator on every Voice | | Snapshots — capture & crossfade the mix | Spatializer (2D pan + 3D HRTF) | AbortSignal cancellation | "Did you mean…" on bus/sound name typos |

Examples

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 } });

Stream long media (don't decode 4 minutes into RAM)

const intro = engine.loadStream('intro', '/music/intro.m4a', { bus: 'music' });
await intro.play({ loop: true, volume: 0.6 });

Crossfade music tracks (equal-power)

await engine.loadSound('intro', '/music/intro.webm', { bus: 'music' });
await engine.loadSound('main',  '/music/main.webm',  { bus: 'music' });

engine.sound('intro').play({ loop: true });
engine.crossfade('intro', 'main', { duration: 1.5 });

Sidechain ducking — music breathes under VO

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

// The source bus (keyed from) is the 2nd argument; the target bus is
// whichever bus you add the ducker to.
const ducker = new Ducker(engine.context, engine.bus('voice'), {
  amount: 0.7, attack: 0.08, release: 0.6,
});
engine.bus('music').addFx(ducker);

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 everything in one call

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 — bundle a webm + m4a pair via the codec ladder |

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

Documentation

  • Quickstart — running in 30 lines
  • Concepts — Engine, Bus, Sound, Voice, Snapshot, Spatializer, Concurrency
  • FX — Compressor, Filter, Reverb, Pitch & time-stretch, Ducker
  • Guides — asset formats, loading, mix building, ducking, migrating from Howler
  • Recipes — short, copy-paste-ready solutions
  • API reference — auto-generated TypeDoc, regenerated each build
  • llms.txt — agent-readable index of the entire site

Working in this repo

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 docs:build      # astro + pagefind index + OG cards + llms.txt
pnpm bench           # vitest bench/

The package's exports field resolves to src/index.ts for workspace consumers. publishConfig overrides it to dist/... at publish time so npm consumers get the compiled artifact. Concept pages embed live React-island demos that drive the real engine — no mocks.

zvuk/
├── src/                package source
├── test/               vitest suite
├── bench/              vitest bench/
├── examples/           vanilla deployable demos (slot-machine, match-3, fps-footsteps)
├── docs/               Astro site → zvuk.schmooky.dev
└── tsup.config.ts      ESM build config

Release flow

Releases are driven by Changesets on every push to main:

  1. Add a changeset describing your change: pnpm changeset
  2. Open a PR; CI gates on lint + typecheck + tests + lib build + docs build.
  3. On merge to main, the release workflow opens (or updates) a Version Packages PR that bumps the version and regenerates CHANGELOG.md.
  4. Merging that PR publishes to npm with provenance attestation and creates a GitHub Release. Auth uses npm OIDC trusted publishing — no NPM_TOKEN secret; the workflow exchanges a GitHub OIDC token for a short-lived npm publish token.

The same release.yml workflow handles per-branch preview publishes: pushing a non-main branch (or running it via workflow_dispatch) snapshot-publishes the pending changesets under a sanitized branch dist-tag — install with pnpm add @schmooky/zvuk@<branch>.

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

Credits

The demo audio shipped under docs/public/audio/ includes selections from Kenney's Digital Audio pack, released under CC0 and used here without modification. Kenney's full license file is preserved at docs/public/audio/KENNEY-LICENSE.txt. All other audio is original to this project.

License

MIT © schmooky