# zvuk > Wwise-grade web audio in a tiny ESM package — lazy AudioContext, mixer buses, sidechain ducking, snapshots, sprites, codec-aware loading. ## Docs - [Quickstart](https://zvuk.schmooky.dev/docs/quickstart/): Two function calls and a user gesture. That's it. - [Why another audio lib?](https://zvuk.schmooky.dev/docs/why/): Howler exists. Tone.js exists. Raw Web Audio exists. Here's the gap zvuk fills. - [zvuk — audio engine for the web](https://zvuk.schmooky.dev/): Landing page — what zvuk is, what it ships, and a one-page tour of the API. ## Concepts - [Bus](https://zvuk.schmooky.dev/concepts/bus/): A named mix bucket with its own gain stage, FX chain, voice limit, and (optional) sidechain key. - [Concurrency](https://zvuk.schmooky.dev/concepts/concurrency/): Voice limits with a steal strategy. Keeps polyphony bounded; chooses which voice dies when the limit is hit. - [Engine](https://zvuk.schmooky.dev/concepts/engine/): The root object. Owns the AudioContext, the bus graph, the scheduler, every loaded sound. - [Mixer](https://zvuk.schmooky.dev/concepts/mixer/): The named bus graph that routes every voice from source to speakers. - [Parameter](https://zvuk.schmooky.dev/concepts/parameter/): A named float you drive at runtime. Subscribers update immediately. Bind it to bus levels, FX values, anything. - [Snapshot](https://zvuk.schmooky.dev/concepts/snapshot/): Capture a mix state. Apply it later with a smooth crossfade. Your menu/gameplay/boss switcher in one call. - [Sound](https://zvuk.schmooky.dev/concepts/sound/): A loaded sample. Owns one decoded AudioBuffer and spawns Voices on play(). - [Spatializer](https://zvuk.schmooky.dev/concepts/spatializer/): Stereo pan or full 3D positional audio. PannerNode wrapped, HRTF on by default. - [Voice](https://zvuk.schmooky.dev/concepts/voice/): One playback instance. What sound.play() returns. ## FX - [Compressor](https://zvuk.schmooky.dev/fx/compressor/): Dynamic range compression as a bus FX insert. Wraps DynamicsCompressorNode + makeup gain. Bypass is a graph swap — no parameter latency. - [Filter](https://zvuk.schmooky.dev/fx/filter/): Biquad filter as a bus FX insert. Lowpass for muffles, highpass for radios, bandpass for telephone, peaking for EQ. - [Pitch & time-stretch](https://zvuk.schmooky.dev/fx/pitch/): Two ways to change 'speed': cheap playback-rate (alters pitch + tempo) and granular time-stretch (preserves pitch). - [Reverb](https://zvuk.schmooky.dev/fx/reverb/): Convolution reverb with synthetic-IR fallback. Use a real impulse response for spaces, or skip it for ambient wash. ## Guides - [Asset formats](https://zvuk.schmooky.dev/guides/asset-formats/): The encoding pipeline you want: WebM/Opus first, M4A/AAC fallback for older iOS Safari. - [Sidechain ducking](https://zvuk.schmooky.dev/guides/ducking/): Drop the music when the VO speaks. Smoothly. Without writing your own envelope follower. - [Loading sounds](https://zvuk.schmooky.dev/guides/loading/): The patterns for getting audio off the network and into the engine — single, bulk, with progress, with cancellation. - [Migrating from Howler](https://zvuk.schmooky.dev/guides/migration/): If your codebase calls Howl + Howler.volume directly, here's the shape of the port. - [Building your mix](https://zvuk.schmooky.dev/guides/mix/): The shape of a five-bus production mix. Pick the buses, declare them at construction, route everything through them. ## Project - [Changelog](https://zvuk.schmooky.dev/changelog/): Every released version of @schmooky/zvuk, sourced from the root CHANGELOG.md. - [Roadmap](https://zvuk.schmooky.dev/roadmap/): What's next in zvuk — public, prioritised, sized. ## API ### Class - [BankNotLoadedError](https://zvuk.schmooky.dev/api/BankNotLoadedError/) - [Bus](https://zvuk.schmooky.dev/api/Bus/): A Bus is a named mix bucket with its own gain stage, optional FX inserts, a voice-concurrency limit, and an optional sidechain key. Voices are connected to bus.input. The master receives bus.output. level/mute use 10ms ramps to avoid clicks; raw gain.value writes would pop audibly on browsers that don't smooth the parameter. - [BusNotFoundError](https://zvuk.schmooky.dev/api/BusNotFoundError/) - [Compressor](https://zvuk.schmooky.dev/api/Compressor/): Dynamics compressor as a bus FX insert. Wraps DynamicsCompressorNode and a make-up gain node, and exposes `input`/`output` so the Bus can wire it into its FX chain. Bypass is a graph swap, not a parameter — when bypassed, the input is connected directly to the output (avoids the compressor's non-zero look-ahead latency leaking into the dry signal). - [DecodeError](https://zvuk.schmooky.dev/api/DecodeError/) - [Ducker](https://zvuk.schmooky.dev/api/Ducker/): Sidechain ducker. Inserts on the *target* bus (e.g. music) and listens to the level of a source bus (e.g. voice). When the source is loud, the target's gain drops; when quiet, it returns. Implementation: an envelope follower running on the source bus's RMS, driving an additional gain node spliced into the target's FX chain. The envelope follower runs on the main thread at ~60 Hz — fine for a speech ducker, not for sample-accurate audio-rate sidechaining. For that, use a custom AudioWorklet (planned). - [EngineClosedError](https://zvuk.schmooky.dev/api/EngineClosedError/) - [Filter](https://zvuk.schmooky.dev/api/Filter/): Common FX insert contract. Every FX exposes a single input + a single output node so the bus can splice it into the (fxInput → output) hop. - [Master](https://zvuk.schmooky.dev/api/Master/): The Master stage. Headroom-aware gain into an optional brick-wall limiter, then to destination. Buses connect their output to master.input. Headroom is a static gain offset; the limiter is a fast-attack DynamicsCompressorNode that catches peaks the headroom can't tame when heavy FX stack on top of busy mixes. - [Parameter](https://zvuk.schmooky.dev/api/Parameter/): A named float you can drive at runtime. Set it from anywhere; subscribers (bus levels, FX wet, voice gain) update immediately. Bind a target to a parameter with `bindTo` — when the parameter changes, the curve maps [0..1] to a target range and applies the value. Repeated `set` calls override each other (no queue), making parameters ideal for "intensity"/"distance"/"tension" knobs that change continuously. - [Reverb](https://zvuk.schmooky.dev/api/Reverb/): Convolution reverb. Mixes a dry signal with a wet path that runs through a ConvolverNode. If no impulse response is provided, a synthetic noise decay is generated — quick and free, but not as nice as a real IR. - [Snapshot](https://zvuk.schmooky.dev/api/Snapshot/): A captured mix-state preset. Capture it once with the engine in a known good state ("menu mood"), then `apply({ fadeMs: 250 })` to crossfade the entire mix back to that snapshot — bus levels, mutes, parameter values, everything in one call. Snapshots are immutable; mutate the engine and re-capture if you need a new one. - [Sound](https://zvuk.schmooky.dev/api/Sound/): A loaded sample — owns one decoded AudioBuffer, spawns Voices on play(). Sound is created via engine.loadSound() / bank load; constructor is package-private. - [SoundNotFoundError](https://zvuk.schmooky.dev/api/SoundNotFoundError/) - [Spatializer](https://zvuk.schmooky.dev/api/Spatializer/): PannerNode wrapper. Inserted between a Voice and its Bus when a play call passes a `spatializer` option. 2D pan uses StereoPannerNode (cheap), 3D uses PannerNode in HRTF mode (one node per voice — fine for the dozens of simultaneous voices a typical game holds, expensive past hundreds). - [Sprite](https://zvuk.schmooky.dev/api/Sprite/): One buffer, many named regions, one fetch. Use for cascades, UI variants, low-latency one-shots — anything where the cost of N separate decodes outweighs the cost of N region offsets into a single buffer. Built on top of an underlying Sound (the buffer); regions are cooperative — overlapping regions just produce overlapping voices. - [StreamSound](https://zvuk.schmooky.dev/api/StreamSound/): Stream a long media file through HTMLAudioElement → MediaElementAudioSource. Use for music tracks > 30s where decoding the whole file into RAM (the loadSound path) would waste memory and stall on iOS. The element handles progressive download and seek; we just route its output into the engine's bus graph so it picks up the same FX/sidechain as buffer-based voices. Created lazily — the underlying MediaElementAudioSource is only built on first play(), since it can't be reattached to a different bus once created. - [StretchProcessor](https://zvuk.schmooky.dev/api/StretchProcessor/): Pitch-preserving time-stretch via overlap-add granular synthesis with cross-correlation alignment (a SOLA-style approximation). Used to render an offline stretched copy of an AudioBuffer at load time — not realtime. For realtime tempo control, see the (planned) AudioWorklet implementation. stretchFactor > 1 = play faster (shorter buffer). stretchFactor < 1 = not currently supported (use rate via PlaybackRate). - [Voice](https://zvuk.schmooky.dev/api/Voice/): One playback instance, returned from sound.play(). Owns its source node and gain stage; disposes itself on natural end, signal abort, or stop(). Voice constructor is package-private — callers obtain instances via Sound.play(). - [ZvukError](https://zvuk.schmooky.dev/api/ZvukError/) ### Function - [applyLoudnessNormalization](https://zvuk.schmooky.dev/api/applyLoudnessNormalization/) - [canPlay](https://zvuk.schmooky.dev/api/canPlay/) - [computeNormalizationGain](https://zvuk.schmooky.dev/api/computeNormalizationGain/) - [createEngine](https://zvuk.schmooky.dev/api/createEngine/) - [createStretchWorkletNode](https://zvuk.schmooky.dev/api/createStretchWorkletNode/) - [ensureStretchWorklet](https://zvuk.schmooky.dev/api/ensureStretchWorklet/) - [mimeForUrl](https://zvuk.schmooky.dev/api/mimeForUrl/) - [pickSource](https://zvuk.schmooky.dev/api/pickSource/) ### Interface - [ApplyOptions](https://zvuk.schmooky.dev/api/ApplyOptions/) - [BusConfig](https://zvuk.schmooky.dev/api/BusConfig/) - [CompressorConfig](https://zvuk.schmooky.dev/api/CompressorConfig/) - [ConcurrencyConfig](https://zvuk.schmooky.dev/api/ConcurrencyConfig/) - [CrossfadeOptions](https://zvuk.schmooky.dev/api/CrossfadeOptions/) - [DuckerConfig](https://zvuk.schmooky.dev/api/DuckerConfig/) - [Engine](https://zvuk.schmooky.dev/api/Engine/) - [EngineConfig](https://zvuk.schmooky.dev/api/EngineConfig/) - [FadeOptions](https://zvuk.schmooky.dev/api/FadeOptions/) - [FilterConfig](https://zvuk.schmooky.dev/api/FilterConfig/) - [FxInsert](https://zvuk.schmooky.dev/api/FxInsert/): Common FX insert contract. Every FX exposes a single input + a single output node so the bus can splice it into the (fxInput → output) hop. - [LoadSoundOptions](https://zvuk.schmooky.dev/api/LoadSoundOptions/) - [LoudnessOptions](https://zvuk.schmooky.dev/api/LoudnessOptions/): RMS-based loudness normalization on a decoded AudioBuffer. `engine.loadSound(..., { normalize: true })` runs this at decode-time and produces a buffer pre-scaled so all sounds sit at the same perceived loudness — removing a workflow tax that game-audio teams currently pay in their DAW. Pass an object to override the target RMS or peak ceiling. - [MasterConfig](https://zvuk.schmooky.dev/api/MasterConfig/) - [MasterLimiterConfig](https://zvuk.schmooky.dev/api/MasterLimiterConfig/) - [PlayOptions](https://zvuk.schmooky.dev/api/PlayOptions/) - [ReverbConfig](https://zvuk.schmooky.dev/api/ReverbConfig/) - [SidechainConfig](https://zvuk.schmooky.dev/api/SidechainConfig/) - [SnapshotState](https://zvuk.schmooky.dev/api/SnapshotState/) - [SpatialOptions](https://zvuk.schmooky.dev/api/SpatialOptions/) - [SpriteMap](https://zvuk.schmooky.dev/api/SpriteMap/) - [SpriteRegion](https://zvuk.schmooky.dev/api/SpriteRegion/) - [StretchWorkletNode](https://zvuk.schmooky.dev/api/StretchWorkletNode/) - [StretchWorkletOptions](https://zvuk.schmooky.dev/api/StretchWorkletOptions/) - [VoiceJitter](https://zvuk.schmooky.dev/api/VoiceJitter/) ### TypeAlias - [AudioMimeType](https://zvuk.schmooky.dev/api/AudioMimeType/): Codec capability + asset-source picking. Recommended encoding pipeline: - Primary: WebM/Opus — smallest, best quality/byte, supported in Chrome, Firefox, Edge, and Safari 14.1+ on macOS / iOS 17+. - Fallback: AAC in M4A — required for older iOS Safari (≤16) and older macOS Safari without Opus support. Ship both; pickSource() returns the first URL the browser can decode. canPlay() uses HTMLAudioElement.canPlayType — it gives a sound (no pun intended) prediction without actually fetching anything. The Web Audio decoder will accept anything HTMLAudioElement says it can play, plus a few extras (uncompressed WAV always works), so canPlay is conservative. - [EngineState](https://zvuk.schmooky.dev/api/EngineState/) - [FadeCurve](https://zvuk.schmooky.dev/api/FadeCurve/) - [FilterKind](https://zvuk.schmooky.dev/api/FilterKind/) - [NormalizeFlag](https://zvuk.schmooky.dev/api/NormalizeFlag/) - [ParameterCurve](https://zvuk.schmooky.dev/api/ParameterCurve/) - [SpriteRegionPlayOptions](https://zvuk.schmooky.dev/api/SpriteRegionPlayOptions/) ## Changelog - [v0.1.1 (1 patch)](https://zvuk.schmooky.dev/changelog/#v0.1.1) — patch - [v0.1.0 (2 minor, 1 patch)](https://zvuk.schmooky.dev/changelog/#v0.1.0) — minor