Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
import { type ReactNode, useEffect, useRef, useState } from "react";
import { useBus } from "../../hooks/use-bus/use-bus.js";
export interface MasterMeterProps {
readonly className?: string;
/** Name of the master bus to subscribe to. Defaults to `"master"`. */
readonly busName?: string;
/** Number of channels to display. Defaults to 2. */
readonly channels?: number;
/** Sample window in frames per second. Defaults to 60. */
readonly sampleRateHz?: number;
/** Initial peak hold time in ms. Defaults to 1000. */
readonly peakHoldMs?: number;
}
interface MeterLevels {
readonly peak: ReadonlyArray<number>;
readonly rms: ReadonlyArray<number>;
readonly hold: ReadonlyArray<number>;
}
const EMPTY_LEVELS: MeterLevels = { peak: [], rms: [], hold: [] };
/**
* RAF-sampled master output meter. Subscribes to the named bus on the
* provider's `BusRegistry` (defaults to `"master"`) and reads block
* samples on every interval tick, computing per-channel peak + RMS
* levels in dB. A peak-hold envelope decays to the live peak after
* `peakHoldMs`. When no publisher exists for the bus the meter shows
* silence (-Infinity dB) on every channel.
*/
export function MasterMeter(props: MasterMeterProps): ReactNode {
const { className, busName, channels, sampleRateHz, peakHoldMs } = props;
const bus = useBus(busName ?? "master");
const channelCount = channels ?? 2;
const sampleHz = Math.max(1, sampleRateHz ?? 60);
const hold = Math.max(0, peakHoldMs ?? 1000);
const [levels, setLevels] = useState<MeterLevels>(EMPTY_LEVELS);
const holdRef = useRef<{ value: number; until: number }[]>([]);
useEffect(() => {
holdRef.current = Array.from({ length: channelCount }, () => ({
value: 0,
until: 0,
}));
const buf: Float32Array[] = Array.from(
{ length: channelCount },
() => new Float32Array(128)
);
const tick = () => {
const now = Date.now();
const peak: number[] = new Array(channelCount).fill(0);
const rms: number[] = new Array(channelCount).fill(0);
readPeakAndRms(bus, channelCount, buf, peak, rms);
const holdNext: number[] = updateHoldEnvelope(
holdRef.current,
peak,
now,
hold
);
setLevels({
peak: peak.map(toDb),
rms: rms.map(toDb),
hold: holdNext.map(toDb),
});
};
const interval = setInterval(tick, Math.floor(1000 / sampleHz));
return () => clearInterval(interval);
}, [bus, channelCount, sampleHz, hold]);
return (
<section
className={
className === undefined
? "rax-master-meter"
: `rax-master-meter ${className}`
}
data-bus={busName ?? "master"}
aria-label={`${busName ?? "master"} output meter`}
>
{Array.from({ length: channelCount }, (_, c) => `ch-${c}`).map(
(key, c) => {
const p = levels.peak[c] ?? Number.NEGATIVE_INFINITY;
const r = levels.rms[c] ?? Number.NEGATIVE_INFINITY;
const h = levels.hold[c] ?? Number.NEGATIVE_INFINITY;
return (
<div
key={key}
className="rax-master-meter__channel"
data-channel={c}
>
<span className="rax-master-meter__peak">
{p === Number.NEGATIVE_INFINITY
? "-∞"
: `${p.toFixed(1)}`}{" "}
dB
</span>
<span className="rax-master-meter__rms">
{r === Number.NEGATIVE_INFINITY
? "-∞"
: `${r.toFixed(1)}`}{" "}
dB
</span>
<span className="rax-master-meter__hold">
{h === Number.NEGATIVE_INFINITY
? "-∞"
: `${h.toFixed(1)}`}{" "}
dB
</span>
</div>
);
}
)}
</section>
);
}
function toDb(amp: number): number {
if (amp <= 0) return Number.NEGATIVE_INFINITY;
return 20 * Math.log10(amp);
}
function updateHoldEnvelope(
holdRefs: ReadonlyArray<{ value: number; until: number }>,
peak: ReadonlyArray<number>,
now: number,
hold: number
): number[] {
const out: number[] = new Array(holdRefs.length).fill(0);
for (let c = 0; c < holdRefs.length; c++) {
const entry = holdRefs[c]!;
const exceeds = peak[c]! >= entry.value;
const expired = now >= entry.until;
if (exceeds || expired) {
entry.value = peak[c]!;
entry.until = now + hold;
}
out[c] = entry.value;
}
return out;
}
function readPeakAndRms(
bus: { channels: number; read(into: Float32Array[]): void } | undefined,
channelCount: number,
buf: Float32Array[],
peak: number[],
rms: number[]
): void {
if (bus === undefined) return;
const wide = Math.min(channelCount, bus.channels);
const into = buf.slice(0, wide);
bus.read(into);
for (let c = 0; c < wide; c++) {
const arr = into[c]!;
let p = 0;
let sumSq = 0;
for (let i = 0; i < arr.length; i++) {
const v = arr[i]!;
const absV = Math.abs(v);
if (absV > p) p = absV;
sumSq += v * v;
}
peak[c] = p;
rms[c] = Math.sqrt(sumSq / arr.length);
}
}