"use client"; import { useState, useRef, useCallback, useEffect } from "react"; export type RecorderState = "idle" | "requesting" | "recording" | "stopped" | "error"; export interface UseAudioRecorderReturn { state: RecorderState; seconds: number; audioBlob: Blob | null; analyserNode: AnalyserNode | null; startRecording: () => Promise; stopRecording: () => void; reset: () => void; error: string | null; } export function useAudioRecorder(maxSeconds = 60): UseAudioRecorderReturn { const [state, setState] = useState("idle"); const [seconds, setSeconds] = useState(0); const [audioBlob, setAudioBlob] = useState(null); const [analyserNode, setAnalyserNode] = useState(null); const [error, setError] = useState(null); const mediaRecorderRef = useRef(null); const chunksRef = useRef([]); const timerRef = useRef | null>(null); const audioContextRef = useRef(null); const streamRef = useRef(null); const cleanup = useCallback(() => { if (timerRef.current) clearInterval(timerRef.current); if (audioContextRef.current && audioContextRef.current.state !== "closed") { audioContextRef.current.close(); audioContextRef.current = null; } if (streamRef.current) { streamRef.current.getTracks().forEach((t) => t.stop()); streamRef.current = null; } setAnalyserNode(null); }, []); const startRecording = useCallback(async () => { setState("requesting"); setError(null); chunksRef.current = []; setAudioBlob(null); try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); streamRef.current = stream; // Set up Web Audio API for visualisation const audioContext = new AudioContext(); audioContextRef.current = audioContext; const source = audioContext.createMediaStreamSource(stream); const analyser = audioContext.createAnalyser(); analyser.fftSize = 256; source.connect(analyser); setAnalyserNode(analyser); const mediaRecorder = new MediaRecorder(stream, { mimeType: MediaRecorder.isTypeSupported("audio/webm;codecs=opus") ? "audio/webm;codecs=opus" : "audio/webm", }); mediaRecorderRef.current = mediaRecorder; mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) chunksRef.current.push(e.data); }; mediaRecorder.onstop = () => { const blob = new Blob(chunksRef.current, { type: "audio/webm" }); setAudioBlob(blob); cleanup(); }; mediaRecorder.start(250); setState("recording"); setSeconds(0); timerRef.current = setInterval(() => { setSeconds((prev) => { if (prev + 1 >= maxSeconds) { mediaRecorderRef.current?.stop(); if (timerRef.current) clearInterval(timerRef.current); setState("stopped"); return maxSeconds; } return prev + 1; }); }, 1000); } catch (err) { setError("Microphone access denied. Please allow microphone access and try again."); setState("error"); } }, [cleanup, maxSeconds]); const stopRecording = useCallback(() => { if (mediaRecorderRef.current && state === "recording") { mediaRecorderRef.current.stop(); if (timerRef.current) clearInterval(timerRef.current); setState("stopped"); } }, [state]); const reset = useCallback(() => { cleanup(); setState("idle"); setSeconds(0); setAudioBlob(null); setError(null); chunksRef.current = []; }, [cleanup]); useEffect(() => () => cleanup(), [cleanup]); return { state, seconds, audioBlob, analyserNode, startRecording, stopRecording, reset, error }; }