Using your own audio engine
Drive audiocn from a native backend, a WebSocket, a worker or anything else.
The Web Audio hooks are optional. If your audio runs somewhere else, such as a Rust or C++ engine in a desktop app, a server, or an AudioWorklet, use the components and the audio core on their own.
Levels from anywhere
Wrap your data in a frame source with createFrameEmitter:
import { createFrameEmitter } from "@/lib/audio/frame-source";
import type { MeterFrame } from "@/lib/audio/types";
export const micLevels = createFrameEmitter<MeterFrame>();
backend.on("levels", ({ peakDb, rmsDb }) => {
micLevels.emit({ channels: [{ peakDb, rmsDb }] });
});<LevelMeter aria-label="Microphone" source={micLevels} />Meters apply their own ballistics, so any update rate works: at 30 updates a second or more they look like a hardware meter, and at one a second they still move smoothly.
Controls to anywhere
Controls report values; they never touch audio. Send changes to your engine:
<Fader
onValueChange={(gainDb) => backend.setGain("mic", gainDb)}
onValueCommitted={(gainDb) => settings.save({ micGainDb: gainDb })}
value={micGainDb}
/>Use onValueChange for live updates and onValueCommitted for anything slow, such as saving settings.
Mixer state without Web Audio
useMixer holds gain, mute, solo, pan and monitor for every channel and knows which channels are audible. Send its state to your engine:
const mixer = useMixer({
channels,
onStateChange: (state) => backend.applyMixer(state),
});Devices from anywhere
AudioDeviceSelect takes its devices as a prop, including their status, so a native device list works the same as the browser's.
Swapping sources
If the audio behind a meter restarts, use a relay so components keep their subscription:
import { createFrameRelay } from "@/lib/audio/frame-source";
const relay = createFrameRelay<MeterFrame>();
relay.setSource(currentSession.levels);