fix: audio bar buggy

This commit is contained in:
talksik
2026-02-21 13:31:11 -08:00
parent 5ff73a173f
commit 02db9c1ebb
+60 -15
View File
@@ -1,16 +1,33 @@
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef } from "react";
interface AudioLevelBarsProps {
sourceNode: AudioNode;
}
const BAR_COUNT = 3;
const MIN_HEIGHT_PX = 6;
const MAX_HEIGHT_PX = 48;
// dB scale
const NOISE_FLOOR_DB = -50;
const DB_RANGE = -NOISE_FLOOR_DB; // 50dB dynamic range
// Asymmetric smoothing time constants
const ATTACK_MS = 30;
const RELEASE_MS = 300;
// Bar activation thresholds on the 0..1 normalized dB scale
const BAR_THRESHOLDS = [0.0, 0.15, 0.35];
/**
* 3-bar VU meter that visualizes audio levels from any AudioNode source.
* Works with both live MediaStream sources and MediaElement sources.
*
* Uses direct DOM manipulation with exponential smoothing on a dB scale
* for smooth, jitter-free animation independent of frame rate.
*/
export function AudioLevelBars({ sourceNode }: AudioLevelBarsProps) {
const [levels, setLevels] = useState([0, 0, 0]);
const rafRef = useRef<number>(0);
const barRefs = useRef<(HTMLDivElement | null)[]>([]);
useEffect(() => {
const ctx = sourceNode.context as AudioContext;
@@ -27,7 +44,15 @@ export function AudioLevelBars({ sourceNode }: AudioLevelBarsProps) {
const dataArray = new Uint8Array(analyser.frequencyBinCount);
let smoothedLevel = 0;
let lastTime = performance.now();
let rafId = 0;
function tick() {
const now = performance.now();
const dt = now - lastTime;
lastTime = now;
analyser.getByteTimeDomainData(dataArray);
// Compute RMS of waveform (128 = silence baseline)
@@ -38,20 +63,37 @@ export function AudioLevelBars({ sourceNode }: AudioLevelBarsProps) {
}
const rms = Math.sqrt(sumSquares / dataArray.length);
// VU meter: 3 bars with staggered thresholds
// Typical speech RMS is ~0.02-0.15 from time-domain data
const bar0 = Math.min(1, rms * 10);
const bar1 = Math.max(0, Math.min(1, (rms - 0.02) * 8));
const bar2 = Math.max(0, Math.min(1, (rms - 0.06) * 6));
setLevels([bar0, bar1, bar2]);
// Convert to dB, clamp to noise floor, normalize to 0..1
const db = rms > 0 ? 20 * Math.log10(rms) : NOISE_FLOOR_DB;
const normalizedDb = Math.max(0, (db - NOISE_FLOOR_DB) / DB_RANGE);
rafRef.current = requestAnimationFrame(tick);
// Asymmetric exponential smoothing (frame-rate independent)
const timeConstant =
normalizedDb > smoothedLevel ? ATTACK_MS : RELEASE_MS;
const alpha = 1 - Math.exp(-dt / timeConstant);
smoothedLevel += alpha * (normalizedDb - smoothedLevel);
// Update bar heights via direct DOM writes
for (let i = 0; i < BAR_COUNT; i++) {
const el = barRefs.current[i];
if (!el) continue;
const threshold = BAR_THRESHOLDS[i];
const barLevel =
smoothedLevel <= threshold
? 0
: Math.min(1, (smoothedLevel - threshold) / (1 - threshold));
const height = MIN_HEIGHT_PX + barLevel * (MAX_HEIGHT_PX - MIN_HEIGHT_PX);
el.style.height = `${height}px`;
}
rafId = requestAnimationFrame(tick);
}
rafRef.current = requestAnimationFrame(tick);
rafId = requestAnimationFrame(tick);
return () => {
cancelAnimationFrame(rafRef.current);
cancelAnimationFrame(rafId);
try {
sourceNode.disconnect(analyser);
analyser.disconnect(silentGain);
@@ -64,11 +106,14 @@ export function AudioLevelBars({ sourceNode }: AudioLevelBarsProps) {
return (
<div className="flex items-end gap-1.5">
{levels.map((level, i) => (
{Array.from({ length: BAR_COUNT }, (_, i) => (
<div
key={i}
className="w-1.5 rounded-full bg-green-400 transition-all duration-75"
style={{ height: `${Math.max(6, level * 48)}px` }}
ref={(el) => {
barRefs.current[i] = el;
}}
className="w-1.5 rounded-full bg-green-400"
style={{ height: `${MIN_HEIGHT_PX}px` }}
/>
))}
</div>