# Bus · zvuk

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

A named mix bucket with its own gain stage, FX chain, voice limit, and optional sidechain key.

Concept

# Bus

A named mix bucket with its own gain stage, FX chain, voice limit, and optional sidechain key.

Twenty sounds are playing and the player drags the "music" slider. Without somewhere to put that instruction, you are iterating live voices and hoping you catch the ones that started half a frame ago.

A bus is a named mix bucket. It owns a gain stage, an FX chain, a voice limit, and an optional sidechain key. Sounds are routed to it by name at play time, and the level you set on the bus applies to everything on it, including voices that start later. See [`Bus`](/api/Bus/) for the full reference.

## What does a bus actually do?

You declare it once in `createEngine` and then talk to it by name. Level changes and mutes ramp over 10 ms internally, so a slider drag doesn't click. `fadeTo` takes a curve when you want something longer than a slider.

A bus has three nodes: input → fxInput (FX chain head) → output. Voices target input; master takes output.

## API surface

The Bus interface ts

```
class Bus {
  readonly name: string;
  readonly input: GainNode;          // connect voices/sources here
  readonly output: GainNode;         // connected to master

  level: number;                     // setter ramps over 10ms (click-free)
  muted: boolean;                    // setter ramps over 10ms

  fadeTo(target: number, duration: number, curve?: FadeCurve): Promise<void>;  // duration in seconds

  voiceCount: number;
  voices(): readonly Voice[];

  concurrency: ConcurrencyConfig | null;
  setConcurrency(c: ConcurrencyConfig | null): void;

  addFx(fx: FxInsert): void;
  removeFx(fx: FxInsert): void;
  fx(): readonly FxInsert[];

  meter(): { rms: number; peak: number };  // live amplitude readout (lazy analyser tap)

  send(target: Bus, options?: { amount?: number; post?: boolean }): Send;
  removeSend(send: Send): void;
  sends(): readonly Send[];

  solo(on?: boolean): void;                // engine coordinates "any soloed → mute the rest"
  unsolo(): void;
  readonly soloed: boolean;
}
```

## Live demo

Slam the slider. No click. The fade buttons run the same gain through a curve.

Use your own sound

music bus\-inf dB

bus outputidle

music.level0.60

write output.gain.value directly (no 10 ms ramp)

fadeTo(1, 0.6)fadeTo(0.1, 0.8)fadeTo(0, 1.2)stop voice

Unlock & start loop

## Recipes

### Direct level vs. fadeTo

ts ts

```
// Smooth fade — won't pop because of the 10ms internal ramp on direct writes,
// or use fadeTo() with a curve for longer transitions.
engine.bus('music').level = 0.5;
await engine.bus('music').fadeTo(0, 1.2, 'equal-power');
```

### Enumerate active voices on a bus

ts ts

```
const bus = engine.bus('sfx');
console.log('voices on this bus:', bus.voiceCount);
for (const v of bus.voices()) v.fade({ to: 0, duration: 0.2 });
```

### Live VU meter via `bus.meter()`

Returns `{ rms, peak }` as linear values in `[0..1]`. The first call attaches a passive AnalyserNode tap. Nothing is allocated until you read, and the audio path is unchanged. The [`rhythm-metronome`](https://github.com/schmooky/zvuk/tree/main/examples/rhythm-metronome) example drives a VU bar and a per-voice peak meter from it.

ts ts

```
// Drive a VU bar from bus.meter() inside your render loop.
function tick() {
  const m = engine.bus('music').meter();
  vuBar.style.width = (m.rms * 200) + '%';
  peakDot.style.left = (m.peak * 100) + '%';
  requestAnimationFrame(tick);
}
requestAnimationFrame(tick);

// First call lazily attaches an AnalyserNode as a sibling of bus.output —
// no cost until you read. Subsequent calls reuse the same analyser.
```

### Sends: route a copy of one bus into another

A send routes a configurable share of one bus's signal into another, instead of inserting an effect directly on the source. It is the Wwise primitive for shared reverb. "Send 30 % of music to a verb-only bus" is two lines, and the amount is adjustable while audio is playing.

ts ts

```
// Send 30% of music to a dedicated reverb bus.
const verbSend = engine.bus('music').send(engine.bus('reverb'), { amount: 0.3 });

// Adjust live; the setter ramps over 10ms to avoid clicks.
verbSend.amount = 0.5;
await verbSend.fadeTo(0, 1.2);   // smooth fade-out

// Remove the send entirely.
verbSend.dispose();
// or
engine.bus('music').removeSend(verbSend);

// Pre-fader / pre-FX tap (rare — useful for monitor sends that should
// hear the dry signal regardless of how the source bus is faded).
engine.bus('music').send(engine.bus('monitor'), { post: false });
```

### Solo: A/B one bus without disturbing the rest

The engine coordinates the global rule. While any bus is in the solo set, every non-soloed bus is muted through the same 10 ms ramp, and when the set drains, every bus is restored. Solo is tracked separately from `muted`, so un-soloing returns each bus to whatever the user had set.

ts ts

```
// Solo this bus — every other bus is muted while the solo set is non-empty.
engine.bus('voice').solo();
engine.bus('music').solo();      // additive — both still audible

engine.bus('voice').unsolo();    // music is still soloed; everyone else still muted
engine.bus('music').unsolo();    // solo set drains — every bus restored

// Solo state is independent of muted: un-soloing returns each bus to its
// own .muted setting, not unconditionally to "audible".
```

### Bus groups: address several buses at once

A [`BusGroup`](/api/BusGroup/) is a logical handle rather than an audio node. It applies `level`, `fadeTo`, `muted` and `solo` to every member in parallel. Reach for it when several buses always move together. Combat is usually weapons plus enemies plus environment; a voice group is usually dialogue plus effort sounds.

ts ts

```
// Group several buses so a single call addresses all of them.
const combat = engine.busGroup('combat', [
  engine.bus('weapons'),
  engine.bus('enemies'),
  engine.bus('environment'),
]);

combat.level = 0.5;              // sets every member's level
await combat.fadeTo(0, 0.8);     // fades every member in parallel
combat.muted = true;             // mutes the whole group
combat.solo();                   // solos every member at once

// Look up later by name.
engine.busGroup('combat').level = 1;
```

## Pitfalls

Don't write to `output.gain.value` directly.

The Bus class wraps that with a 10 ms ramp. Bypassing it produces audible clicks in Chrome and Firefox.

Don't share an FxInsert between buses.

Each FX node has its own internal connections. Construct one per bus, dispose when removed.

## Related

-   [Concurrency](/concepts/concurrency/) sets the voice-limit policy on a bus.
-   [Mixer](/concepts/mixer/) is the wider bus graph this sits in.
-   [Ducking](/guides/ducking/) sidechains one bus from another.
