import { useCallback, useRef, useState } from "react"; import { encodeWav, resampleTo16kMono, TARGET_SAMPLE_RATE } from "../utils/wavEncoder"; export type RecorderStatus = "idle" | "recording" | "recorded" | "error"; export function useAudioRecorder() { const [status, setStatus] = useState("idle"); const [seconds, setSeconds] = useState(0); const [error, setError] = useState(null); const [audioUrl, setAudioUrl] = useState(null); const [audioFile, setAudioFile] = useState(null); const streamRef = useRef(null); const contextRef = useRef(null); const processorRef = useRef(null); const chunksRef = useRef([]); const timerRef = useRef(null); const recordingRef = useRef(false); const cleanupStream = useCallback(() => { streamRef.current?.getTracks().forEach((track) => track.stop()); streamRef.current = null; processorRef.current?.disconnect(); processorRef.current = null; if (contextRef.current?.state !== "closed") { void contextRef.current?.close(); } contextRef.current = null; }, []); const reset = useCallback(() => { if (timerRef.current) { window.clearInterval(timerRef.current); timerRef.current = null; } recordingRef.current = false; cleanupStream(); chunksRef.current = []; if (audioUrl) URL.revokeObjectURL(audioUrl); setAudioUrl(null); setAudioFile(null); setSeconds(0); setError(null); setStatus("idle"); }, [audioUrl, cleanupStream]); const start = useCallback(async () => { reset(); if (!navigator.mediaDevices?.getUserMedia) { setError("المتصفح لا يدعم التسجيل الصوتي."); setStatus("error"); return; } try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); streamRef.current = stream; const context = new AudioContext(); contextRef.current = context; const source = context.createMediaStreamSource(stream); const processor = context.createScriptProcessor(4096, 1, 1); processorRef.current = processor; chunksRef.current = []; recordingRef.current = true; processor.onaudioprocess = (event) => { if (!recordingRef.current) return; const channel = event.inputBuffer.getChannelData(0); chunksRef.current.push(new Float32Array(channel)); }; source.connect(processor); processor.connect(context.destination); setStatus("recording"); setSeconds(0); timerRef.current = window.setInterval(() => { setSeconds((s) => s + 1); }, 1000); } catch { setError("تعذّر الوصول للميكروفون. تأكّد من منح الإذن."); setStatus("error"); cleanupStream(); } }, [cleanupStream, reset]); const stop = useCallback(async () => { recordingRef.current = false; if (timerRef.current) { window.clearInterval(timerRef.current); timerRef.current = null; } const context = contextRef.current; const chunks = chunksRef.current; cleanupStream(); if (!context || chunks.length === 0) { setError("لم يُسجَّل أي صوت."); setStatus("error"); return; } try { const sampleRate = context.sampleRate; const totalLength = chunks.reduce((sum, c) => sum + c.length, 0); const merged = new Float32Array(totalLength); let offset = 0; for (const chunk of chunks) { merged.set(chunk, offset); offset += chunk.length; } const audioBuffer = context.createBuffer(1, merged.length, sampleRate); audioBuffer.copyToChannel(merged, 0); const samples = await resampleTo16kMono(audioBuffer); const wavBlob = encodeWav(samples, TARGET_SAMPLE_RATE); const url = URL.createObjectURL(wavBlob); const file = new File([wavBlob], "recording.wav", { type: "audio/wav" }); setAudioUrl(url); setAudioFile(file); setStatus("recorded"); } catch { setError("تعذّر تحضير التسجيل الصوتي."); setStatus("error"); } }, [cleanupStream]); return { status, seconds, error, audioUrl, audioFile, start, stop, reset, }; } export function formatTime(totalSeconds: number): string { const m = Math.floor(totalSeconds / 60) .toString() .padStart(2, "0"); const s = (totalSeconds % 60).toString().padStart(2, "0"); return `${m}:${s}`; }