Concept

Mixer

The named bus graph that routes every voice from source to speakers.

TL;DR

The Mixer is your declarative routing model. You name your buses (music, sfx, voice, etc.), give each one a level and behavior, and the engine builds the audio graph for you. The mixer is what separates "I called .play()" from "I shipped a mix."

Mental model

Sources fan in through their default bus. Each bus has its own level, optional FX inserts, optional concurrency cap, and an optional sidechain key. Bus outputs sum at the master, which applies headroom and sends to the destination.

bus graph
SOURCES music.mp3 crowd.webm coin.webm click.webm voice.m4a BUSES music level 0.8 · concurrency 4 fx: reverb sfx level 1.0 · concurrency 32 steal: oldest voice level 1.0 · ducks music fx: ducker(from: voice) MASTER Master headroom -3 dB 🔊 dest sidechain (voice → music)
Three buses, FX inserts, sidechain key (voice → music). Master sums and applies headroom.

API surface

Declare buses up front ts
const engine = createEngine({
  buses: {
    music: { level: 0.8, concurrency: { max: 4 } },
    sfx:   { level: 1.0, concurrency: { max: 32, steal: 'oldest' } },
    voice: { level: 1.0, sidechain: { from: 'music', amount: 0.5, attack: 0.08, release: 0.4 } },
  },
  master: {
    headroom: -3,
    // Fast-attack soft limiter on master out — best-effort peak control for
    // when FX stack on busy mixes. Disable by omitting the field.
    limiter: { threshold: -1, ratio: 20, attack: 0.001, release: 0.05 },
  },
});
Talk to a bus by name ts
engine.bus('music').level = 0.5;
engine.bus('sfx').fadeTo(0, 0.8);
engine.bus('voice').muted = true;

Live demo

Sliders, mute, real voice counter — live engine, real samples.

engine.state = cold
0 voices

Recipes

Insert an FX chain on a bus

ts ts
import { Compressor, Reverb } from '@schmooky/zvuk';

const comp = new Compressor(engine.context, { threshold: -18, ratio: 4 });
const reverb = new Reverb(engine.context, { wet: 0.3, decay: { seconds: 1.4 } });

engine.bus('music').addFx(reverb);
engine.bus('sfx').addFx(comp);   // FX run between bus.input and bus.output

Master limiter

Headroom is a static gain offset. The optional limiter is a fast-attack DynamicsCompressor (ratio 20, ~1 ms attack) used as a soft limiter on the master output — it catches most transients headroom alone can't tame, but with a finite attack and no lookahead it does not guarantee a hard 0 dBFS ceiling.

ts ts
// The master limiter is configured at construction (see above). There is
// no public runtime accessor yet — to change or disable it, recreate the
// engine with a different master.limiter (or omit the field).
createEngine({ master: { limiter: { threshold: -0.5, ratio: 20 } } });

Pitfalls

Don't add buses dynamically after createEngine.
Bus topology is declared once. If you need a temporary effect, route into an existing bus and bypass when not in use, or add a fresh FX insert.
Don't write directly to ctx.destination.
Going around the master skips headroom and breaks snapshots. Always route through a declared bus.

Related