import { useState, useEffect, useRef, useCallback } from 'react'; export interface AudioAnalyzerState { isInitialized: boolean; isStreaming: boolean; error: string | null; } export const useAudioAnalyzer = (fftSize: number = 8192) => { const [state, setState] = useState({ isInitialized: false, isStreaming: false, error: null, }); const audioContextRef = useRef(null); const analyserRef = useRef(null); const streamRef = useRef(null); const sourceRef = useRef(null); const init = useCallback(async () => { try { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { throw new Error("Microphone API not available (requires HTTPS)"); } const AudioContextCtor = window.AudioContext || (window as any).webkitAudioContext; const ctx = new AudioContextCtor({ sampleRate: 48000 }); // iOS requires AudioContext to be resumed synchronously during user interaction if (ctx.state === 'suspended') { await ctx.resume(); } const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: false, autoGainControl: false, noiseSuppression: false, }, }); const analyser = ctx.createAnalyser(); analyser.fftSize = fftSize; analyser.smoothingTimeConstant = 0.3; const source = ctx.createMediaStreamSource(stream); source.connect(analyser); audioContextRef.current = ctx; analyserRef.current = analyser; streamRef.current = stream; sourceRef.current = source; setState({ isInitialized: true, isStreaming: true, error: null, }); return true; } catch (err: any) { console.error('Failed to init audio context', err); setState(s => ({ ...s, error: err.message || 'Microphone access denied' })); return false; } }, [fftSize]); const stop = useCallback(() => { if (streamRef.current) { streamRef.current.getTracks().forEach(track => track.stop()); } if (audioContextRef.current) { audioContextRef.current.close(); } setState({ isInitialized: false, isStreaming: false, error: null, }); }, []); useEffect(() => { return () => { stop(); }; }, [stop]); // Expose a method to grab the latest frequency data const getFrequencyData = useCallback(() => { if (!analyserRef.current || !audioContextRef.current) return null; const analyser = analyserRef.current; // Uint8Array for performance. Values 0-255. const dataArray = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(dataArray); return { data: dataArray, sampleRate: audioContextRef.current.sampleRate, fftSize: analyser.fftSize, binCount: analyser.frequencyBinCount }; }, []); return { ...state, init, stop, getFrequencyData }; };