fix: audio bar buggy
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user