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 lib/docs/use-demo-mixer.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.
import type { Accessor } from "solid-js";
import { createDemoSignal } from "@/hooks/use-demo-signal";
import type { DemoSignalOptions } from "@/hooks/use-demo-signal";
import { isChannelAudible } from "@/hooks/use-mixer";
import type { MixerState } from "@/hooks/use-mixer";
import { dbToGain, gainToDb } from "@/lib/audio/decibels";
import { subscribeFrame } from "@/lib/audio/frame-loop";
import type { FrameSource, MeterFrame } from "@/lib/audio/types";
import { createCompatEffect } from "@/lib/solid/effect";
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))
);
// Input subscriptions run first, so every mix uses the same 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])
),
};
};
/** Stable demo sources; gain, mute and solo follow the reactive mixer state. */
export const useDemoMixer = (
channels: readonly DemoMixerChannel[],
state: Accessor<MixerState>
) => {
const demo = createDemoMixer(channels);
createCompatEffect(state, demo.configure);
return demo;
};Installation
$ pnpm dlx shadcn@latest add @audiocn-solid/mixerUsage
import {
Mixer,
MixerChannels,
MixerHeader,
MixerMaster,
MixerTitle,
} from "@/components/ui/mixer";<Mixer orientation="vertical">
<MixerHeader>
<MixerTitle>Audio mixer</MixerTitle>
</MixerHeader>
<MixerChannels>
{channels.map((channel) => (
<ChannelStrip key={channel.id}>…</ChannelStrip>
))}
</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 Solid, so a full console keeps the frame rate with no re-renders 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.
API reference
Mixer
Prop
Type
MixerChannels
Prop
Type