audiocn-svelte
Esc

Start typing to search the docs

Mixer

The container for channel strips, with shared meter settings and keyboard navigation.

2 min read · 382 words

Audio mixer

Microphone
0.0 dB
System audio
0.0 dB
Music
0.0 dB
Master

The preview meters each synthetic channel after its fader, mute and solo settings. Muting or excluding a channel with solo clears its meter immediately. The master combines those channels, then applies the master fader. It clears immediately when no channels are audible.

Copy the demo signal helper

The example uses a docs-only useDemoMixer helper. To copy the preview, save the code below as src/lib/docs/use-demo-mixer.svelte.ts. It uses useDemoSignal and useMixer.

Mono channels feed both sides. The helper estimates peak by summing amplitudes and RMS by summing powers. For audio samples, connect useWebAudioMixer to the same components.

src/lib/docs/use-demo-mixer.svelte.ts
/* eslint-disable svelte/prefer-svelte-reactivity -- frame subscribers stay off the reactive graph. */
import { extract, type MaybeGetter } from "runed";

import { createDemoSignal, type DemoSignalOptions } from "#lib/hooks/use-demo-signal.svelte.js";
import { isChannelAudible, type MixerState } from "#lib/hooks/use-mixer.svelte.js";
import { dbToGain, gainToDb } from "#lib/audio/decibels.js";
import { subscribeFrame } from "#lib/audio/frame-loop.js";
import type { FrameSource, MeterFrame } from "#lib/audio/types.js";

export interface DemoMixerChannel extends DemoSignalOptions {
	id: string;
}

/** Docs-only metering: sum peak amplitudes and RMS powers, with mono on both sides. */
export const createDemoMixer = (channels: readonly DemoMixerChannel[]) => {
	const signals = channels.map(({ id, ...options }) => ({
		id,
		signal: createDemoSignal(options),
	}));
	let masterGain = 1;
	const subscribers = new Set<(frame: MeterFrame) => void>();
	let release: (() => void) | undefined;

	const master: FrameSource<MeterFrame> = {
		subscribe: (listener) => {
			subscribers.add(listener);
			if (!release) {
				const frames = new Map<string, MeterFrame>();
				const unsubscribes = signals.map(({ id, signal }) =>
					signal.meter.subscribe((frame) => frames.set(id, frame))
				);
				// All inputs update first, so each mix uses the same animation frame.
				const stop = subscribeFrame(() => {
					const frame: MeterFrame = { channels: [] };
					for (let side = 0; side < 2; side += 1) {
						let peak = 0;
						let power = 0;
						for (const input of frames.values()) {
							const level = input.channels[input.channels.length === 1 ? 0 : side];
							if (level) {
								peak += dbToGain(level.peakDb);
								power += dbToGain(level.rmsDb ?? level.peakDb) ** 2;
							}
						}
						frame.channels.push({
							peakDb: gainToDb(peak * masterGain),
							rmsDb: gainToDb(Math.sqrt(power) * masterGain),
						});
					}
					for (const subscriber of subscribers) {
						subscriber(frame);
					}
				}, "update");
				release = () => {
					stop();
					for (const unsubscribe of unsubscribes) {
						unsubscribe();
					}
				};
			}
			return () => {
				subscribers.delete(listener);
				if (subscribers.size === 0) {
					release?.();
					release = undefined;
				}
			};
		},
	};

	return {
		configure: (state: MixerState) => {
			for (const { id, signal } of signals) {
				const channel = state.channels.find((item) => item.id === id);
				signal.configure({
					gainDb: channel?.gainDb ?? 0,
					playing: isChannelAudible(state, id),
				});
			}
			masterGain = state.master.muted ? 0 : dbToGain(state.master.gainDb);
		},
		master,
		sources: Object.fromEntries(signals.map(({ id, signal }) => [id, signal.meter])),
	};
};

/** Keep channel definitions stable; gain, mute and solo follow mixer state. Call during component setup. */
export const useDemoMixer = (
	channels: readonly DemoMixerChannel[],
	state: MaybeGetter<MixerState>
) => {
	const demo = createDemoMixer(channels);
	$effect(() => {
		demo.configure(extract(state));
	});
	return demo;
};

Installation

$ pnpm dlx shadcn-svelte@latest add https://juddisjudd.github.io/audiocn-svelte/r/mixer.json

Usage

<script lang="ts">
	import { ChannelStrip } from "#lib/components/ui/channel-strip/index.js";
	import {
		Mixer,
		MixerChannels,
		MixerHeader,
		MixerMaster,
		MixerSeparator,
		MixerTitle,
	} from "#lib/components/ui/mixer/index.js";
</script>

<Mixer orientation="vertical">
	<MixerHeader>
		<MixerTitle>Audio mixer</MixerTitle>
	</MixerHeader>
	<MixerChannels>
		{#each channels as channel (channel.id)}
			<ChannelStrip>…</ChannelStrip>
		{/each}
	</MixerChannels>
	<MixerSeparator />
	<MixerMaster>
		<ChannelStrip variant="master">…</ChannelStrip>
	</MixerMaster>
</Mixer>

The mixer holds no audio and no state. Pair it with useMixer for state and useWebAudioMixer for sound, or with your own engine.

Anatomy

<Mixer>
	<MixerHeader>
		<MixerTitle />
		<MixerActions />
	</MixerHeader>
	<MixerChannels />
	<MixerEmpty />
	<MixerSeparator />
	<MixerMaster />
</Mixer>

Examples

Console

Sixteen strips in a console. Meters paint outside Svelte's reactivity, so a full console keeps the frame rate with no component updates while it meters.

Console

In 1
0.0 dB
In 2
0.0 dB
In 3
0.0 dB
In 4
0.0 dB
In 5
0.0 dB
In 6
0.0 dB
In 7
0.0 dB
In 8
0.0 dB
In 9
0.0 dB
In 10
0.0 dB
In 11
0.0 dB
In 12
0.0 dB
In 13
0.0 dB
In 14
0.0 dB
In 15
0.0 dB
In 16
0.0 dB

Empty

Audio mixer

A complete mixer

The System Audio Mixer block is a working mixer built from these parts.

Behaviour

  • Every meter in the mixer shares minDb, maxDb, zones and ballistics, so levels compare fairly across channels.
  • With focus on a control in a strip, Ctrl + arrow keys move to the same control on the neighbouring strip: left and right in a console, up and down in rows. Tab still moves through every control in order.
  • MixerChannels scrolls along the strip axis when strips overflow; the master area stays in place.
  • MixerEmpty appears when MixerChannels has no children.
  • useMixerContext() reads the mixer's orientation in your own parts.

API reference

Mixer

PropTypeDefault
orientation

horizontal stacks row strips; vertical lays console strips side by side.

"horizontal" | "vertical""horizontal"
size

Inherited by every strip and control.

"sm" | "default" | "lg"—
minDb / maxDb

Shared meter range.

number-60 / 0
zones

Shared meter zones.

MeterZone[]DEFAULT_ZONES
ballistics

Shared meter movement.

BallisticsInput"peak"
disabled booleanfalse

MixerChannels

PropTypeDefault
scrollable

Scroll when strips overflow.

booleantrue
Last updated on October 7, 2026