import { useEffect, useRef, useState } from "react"; /** * Manage a microphone AudioContext and AnalyserNode. * Returns the AnalyserNode when active, null otherwise. * * @example * let analyser = useMic(recording); * // Pass to or useMicLevel(analyser) */ function useMic(active: boolean): AnalyserNode | null { let [analyser, setAnalyser] = useState(null); let ctx = useRef(null); let source = useRef(null); let stream = useRef(null); useEffect(() => { if (!active) return; let cancelled = false; async function start() { let media = await navigator.mediaDevices.getUserMedia({ audio: true }); if (cancelled) { media.getTracks().forEach(t => t.stop()); return; } let audio = new AudioContext(); let node = audio.createAnalyser(); node.fftSize = 64; node.smoothingTimeConstant = 0.6; let src = audio.createMediaStreamSource(media); src.connect(node); ctx.current = audio; source.current = src; stream.current = media; setAnalyser(node); } start(); return () => { cancelled = true; source.current?.disconnect(); stream.current?.getTracks().forEach(t => t.stop()); ctx.current?.close(); ctx.current = null; source.current = null; stream.current = null; setAnalyser(null); }; }, [active]); return analyser; } /** * Derive a 0–1 amplitude scalar from an AnalyserNode each frame. * Uses frequency data for built-in smoothing via the analyser's smoothingTimeConstant. * * @param gain - Sensitivity multiplier (default 3). * * @example * let level = useMicLevel(analyser); * // Use to drive glow opacity, scale, etc. */ function useMicLevel(analyser: AnalyserNode | null, gain = 3): number { let [level, setLevel] = useState(0); let raf = useRef(0); useEffect(() => { if (!analyser) { setLevel(0); return; } let data = new Uint8Array(analyser.frequencyBinCount); function tick() { analyser!.getByteFrequencyData(data); let sum = 0; for (let i = 0; i < data.length; i++) sum += data[i]; setLevel(Math.min(1, sum / data.length / 255 * gain)); raf.current = requestAnimationFrame(tick); } raf.current = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf.current); }, [analyser, gain]); return level; } export { useMic, useMicLevel };