# Mixer · zvuk

zvuk v1.14.1 · MIT · https://zvuk.schmooky.dev/concepts/mixer/

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

Concept

# Mixer

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

Every project ships a volume slider. Most ship three. The interesting part is never the slider; it is having somewhere for it to point that survives the twentieth sound you add.

The mixer is the declarative routing model. It is a set of [named buses](/api/Bus/), each with a level, optional FX inserts, an optional voice cap, and an optional sidechain key. You name them at construction and the engine builds the audio graph. Bus outputs sum at the master, which applies headroom and sends to the destination.

## Why declare buses up front?

Because the alternative is deciding routing at every call site, and call sites multiply. With named buses, "duck the music while dialogue plays" is one rule written once, and it applies to dialogue lines that don't exist yet.

bus graph

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

Three buses, six samples, a live voice counter. Sliders and mute drive the running engine.

engine.state = cold

0 voicesPop out

music

mute

level: 0.80

sfx

mute

level: 1.00

ui

mute

level: 0.70

chimemusicbellsmusicdice-rollsfxdice-shakesfxgemuiheartui

Each sample is shipped as `.webm` (Opus) +`.m4a` (AAC). The engine picks whichever your browser supports.

Unlock audio & load samples

## 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` at ratio 20 with a 1 ms attack, used as a soft limiter on the master output. It catches most of the transients headroom alone can't tame. It is not a brick wall: with a finite attack and no lookahead, peaks above the threshold still get through, so don't treat it as a guaranteed 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

-   [Bus](/concepts/bus/) covers a single bus and its FX chain.
-   [Snapshot](/concepts/snapshot/) captures and crossfades the whole mix.
-   [Building your mix](/guides/mix/) is the practical version of this page.
