useAudioRecorder.ts
1 import { useState, useRef, useCallback } from 'react' 2 3 export function useAudioRecorder(sampleRate: number = 16000) { 4 const [isRecording, setIsRecording] = useState(false) 5 const [duration, setDuration] = useState(0) 6 const mediaRecorderRef = useRef<MediaRecorder | null>(null) 7 const audioChunksRef = useRef<Blob[]>([]) 8 const startTimeRef = useRef<number>(0) 9 const durationIntervalRef = useRef<NodeJS.Timeout | null>(null) 10 11 const startRecording = useCallback(async () => { 12 try { 13 const stream = await navigator.mediaDevices.getUserMedia({ 14 audio: { 15 sampleRate, 16 channelCount: 1, 17 echoCancellation: true, 18 noiseSuppression: true, 19 } 20 }) 21 22 const mediaRecorder = new MediaRecorder(stream, { 23 mimeType: 'audio/webm' 24 }) 25 26 audioChunksRef.current = [] 27 28 mediaRecorder.ondataavailable = (event) => { 29 if (event.data.size > 0) { 30 audioChunksRef.current.push(event.data) 31 } 32 } 33 34 mediaRecorderRef.current = mediaRecorder 35 mediaRecorder.start() 36 setIsRecording(true) 37 startTimeRef.current = Date.now() 38 39 // Update duration every 100ms 40 durationIntervalRef.current = setInterval(() => { 41 setDuration(Math.floor((Date.now() - startTimeRef.current) / 1000)) 42 }, 100) 43 44 } catch (error) { 45 console.error('Failed to start recording:', error) 46 throw error 47 } 48 }, [sampleRate]) 49 50 const stopRecording = useCallback(async (): Promise<{ audioData: Uint8Array; duration: number }> => { 51 return new Promise((resolve, reject) => { 52 if (!mediaRecorderRef.current) { 53 reject(new Error('No recording in progress')) 54 return 55 } 56 57 if (durationIntervalRef.current) { 58 clearInterval(durationIntervalRef.current) 59 durationIntervalRef.current = null 60 } 61 62 const finalDuration = Math.floor((Date.now() - startTimeRef.current) / 1000) 63 64 mediaRecorderRef.current.onstop = async () => { 65 try { 66 const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' }) 67 68 // Convert to raw PCM data for processing 69 const arrayBuffer = await audioBlob.arrayBuffer() 70 const audioContext = new AudioContext({ sampleRate }) 71 const audioBuffer = await audioContext.decodeAudioData(arrayBuffer) 72 73 // Get PCM data from first channel 74 const pcmData = audioBuffer.getChannelData(0) 75 76 // Convert float32 array to int16 array 77 const int16Array = new Int16Array(pcmData.length) 78 for (let i = 0; i < pcmData.length; i++) { 79 const s = Math.max(-1, Math.min(1, pcmData[i])) 80 int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF 81 } 82 83 // Convert to Uint8Array 84 const uint8Array = new Uint8Array(int16Array.buffer) 85 86 // Stop all tracks 87 mediaRecorderRef.current?.stream.getTracks().forEach(track => track.stop()) 88 89 setIsRecording(false) 90 setDuration(0) 91 resolve({ audioData: uint8Array, duration: finalDuration }) 92 } catch (error) { 93 reject(error) 94 } 95 } 96 97 mediaRecorderRef.current.stop() 98 }) 99 }, [sampleRate]) 100 101 return { 102 isRecording, 103 duration, 104 startRecording, 105 stopRecording 106 } 107 }