Feeding data
Every meter and visualizer accepts levels three ways, so it fits any audio source.
audiocn components never open a microphone or connect to Web Audio themselves. They receive levels, and they accept them three ways.
1. A source
A frame source is anything with a subscribe method:
interface FrameSource<T> {
subscribe: (callback: (frame: T) => void) => () => void;
}Pass one as source and the component subscribes and paints itself on every animation frame, without re-rendering React. This is the fastest way, and the one to use for live audio.
const analyser = useAudioAnalyser(stream);
<LevelMeter source={analyser.meter} />;
<BarVisualizer source={analyser.visual} />;The hooks in this library return sources, and so can your own code:
import { createFrameEmitter } from "@/lib/audio/frame-source";
const levels = createFrameEmitter<MeterFrame>();
socket.on("levels", (frame) => levels.emit(frame));
<LevelMeter source={levels} />;2. Values
For data that arrives slowly, such as once a second from a server, pass plain values. The meter's ballistics still move it smoothly between updates.
<LevelMeter peakDb={-9.1} rmsDb={-18} />
<LevelMeter channels={[{ peakDb: -12 }, { peakDb: -14 }]} />3. A handle
If you already run your own frame loop, paint through actionsRef:
const meter = useRef<LevelMeterActions>(null);
onFrame((levels) => {
meter.current?.paint({ channels: levels });
});
<LevelMeter actionsRef={meter} />;Frames
interface MeterFrame {
channels: { peakDb: number; rmsDb?: number }[];
}
interface VisualFrame {
bands: Float32Array; // 0..1 per frequency band
history: Float32Array; // ring of recent levels, 0..1
historyStart: number;
historyLength: number;
timeDomain?: Float32Array; // −1..1 samples
peakDb: number;
}Sources reuse their frame objects to avoid allocating on every frame. Copy a frame if you need to keep it.
In React state
When you need a level in React, for a label or a warning, use useLevel. It samples a source a few times a second.