/ apps / web / src / hooks / useAudioRecorder.ts
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  }