import { useCallback, useEffect, useRef, useState } from "react"; export interface AudioLevelMeterOptions { enabled: boolean; deviceId?: string; smoothingFactor?: number; } export function useAudioLevelMeter(options: AudioLevelMeterOptions) { const [level, setLevel] = useState(0); const audioContextRef = useRef(null); const analyserRef = useRef(null); const streamRef = useRef(null); const animationFrameRef = useRef(null); const cleanup = useCallback(() => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); animationFrameRef.current = null; } if (streamRef.current) { streamRef.current.getTracks().forEach((track) => track.stop()); streamRef.current = null; } if (audioContextRef.current) { audioContextRef.current.close(); audioContextRef.current = null; } analyserRef.current = null; }, []); useEffect(() => { if (!options.enabled) { cleanup(); setLevel(0); return; } let mounted = true; const startMonitoring = async () => { try { const constraints: MediaStreamConstraints = { audio: options.deviceId ? { deviceId: { exact: options.deviceId } } : true, video: false, }; const stream = await navigator.mediaDevices.getUserMedia(constraints); if (!mounted) { stream.getTracks().forEach((track) => track.stop()); return; } streamRef.current = stream; const audioContext = new AudioContext(); if (audioContext.state === "suspended") { await audioContext.resume(); } audioContextRef.current = audioContext; const analyser = audioContext.createAnalyser(); analyser.fftSize = 256; analyser.smoothingTimeConstant = options.smoothingFactor ?? 0.8; analyserRef.current = analyser; const source = audioContext.createMediaStreamSource(stream); source.connect(analyser); const dataArray = new Uint8Array(analyser.frequencyBinCount); const updateLevel = () => { if (!mounted || !analyserRef.current) return; analyser.getByteFrequencyData(dataArray); let sum = 0; for (let i = 0; i < dataArray.length; i++) { sum += dataArray[i] * dataArray[i]; } const rms = Math.sqrt(sum / dataArray.length); const normalizedLevel = Math.min(100, (rms / 255) * 100 * 2); setLevel(normalizedLevel); animationFrameRef.current = requestAnimationFrame(updateLevel); }; updateLevel(); } catch (err) { console.error("Error starting audio level monitoring:", err); if (mounted) { setLevel(0); } } }; startMonitoring(); return () => { mounted = false; cleanup(); }; }, [options.enabled, options.deviceId, options.smoothingFactor, cleanup]); return { level }; }