Mixer
The container for channel strips, with shared meter settings and keyboard navigation.
Audio mixer
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.
/* 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$ npx shadcn-svelte@latest add https://juddisjudd.github.io/audiocn-svelte/r/mixer.json$ yarn dlx shadcn-svelte@latest add https://juddisjudd.github.io/audiocn-svelte/r/mixer.json$ bunx --bun shadcn-svelte@latest add https://juddisjudd.github.io/audiocn-svelte/r/mixer.jsonUsage
<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
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,zonesandballistics, 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.
MixerChannelsscrolls along the strip axis when strips overflow; the master area stays in place.MixerEmptyappears whenMixerChannelshas no children.useMixerContext()reads the mixer'sorientationin your own parts.
API reference
Mixer
| Prop | Type | Default |
|---|---|---|
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 | boolean | false |
MixerChannels
| Prop | Type | Default |
|---|---|---|
scrollable Scroll when strips overflow. | boolean | true |