Spaces:
Sleeping
Sleeping
| import { useState, useRef, useCallback } from 'react'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Card, CardContent } from '@/components/ui/card'; | |
| import { Mic, MicOff, Play, Pause, Download, Trash2 } from 'lucide-react'; | |
| import { useToast } from '@/hooks/use-toast'; | |
| import AudioWaveform from './AudioWaveform'; | |
| import MicrophoneScene from '../three/MicrophoneScene'; | |
| interface AudioRecorderProps { | |
| onRecordingComplete?: (audioBlob: Blob, audioUrl: string) => void; | |
| className?: string; | |
| } | |
| export default function AudioRecorder({ onRecordingComplete, className = "" }: AudioRecorderProps) { | |
| const [isRecording, setIsRecording] = useState(false); | |
| const [isPlaying, setIsPlaying] = useState(false); | |
| const [audioUrl, setAudioUrl] = useState<string>(''); | |
| const [recordingTime, setRecordingTime] = useState(0); | |
| const mediaRecorderRef = useRef<MediaRecorder | null>(null); | |
| const audioRef = useRef<HTMLAudioElement | null>(null); | |
| const chunksRef = useRef<Blob[]>([]); | |
| const intervalRef = useRef<NodeJS.Timeout | null>(null); | |
| const { toast } = useToast(); | |
| const startRecording = useCallback(async () => { | |
| try { | |
| const stream = await navigator.mediaDevices.getUserMedia({ | |
| audio: { | |
| echoCancellation: true, | |
| noiseSuppression: true, | |
| sampleRate: 44100 | |
| } | |
| }); | |
| const mediaRecorder = new MediaRecorder(stream, { | |
| mimeType: 'audio/webm;codecs=opus' | |
| }); | |
| mediaRecorderRef.current = mediaRecorder; | |
| chunksRef.current = []; | |
| mediaRecorder.ondataavailable = (event) => { | |
| if (event.data.size > 0) { | |
| chunksRef.current.push(event.data); | |
| } | |
| }; | |
| mediaRecorder.onstop = () => { | |
| const audioBlob = new Blob(chunksRef.current, { type: 'audio/webm' }); | |
| const url = URL.createObjectURL(audioBlob); | |
| setAudioUrl(url); | |
| onRecordingComplete?.(audioBlob, url); | |
| // Stop all tracks | |
| stream.getTracks().forEach(track => track.stop()); | |
| }; | |
| mediaRecorder.start(); | |
| setIsRecording(true); | |
| setRecordingTime(0); | |
| intervalRef.current = setInterval(() => { | |
| setRecordingTime(prev => prev + 1); | |
| }, 1000); | |
| toast({ | |
| title: "Recording started", | |
| description: "Speak clearly into your microphone" | |
| }); | |
| } catch (error) { | |
| console.error('Error starting recording:', error); | |
| toast({ | |
| title: "Recording failed", | |
| description: "Could not access microphone. Please check permissions.", | |
| variant: "destructive" | |
| }); | |
| } | |
| }, [onRecordingComplete, toast]); | |
| const stopRecording = useCallback(() => { | |
| if (mediaRecorderRef.current && isRecording) { | |
| mediaRecorderRef.current.stop(); | |
| setIsRecording(false); | |
| if (intervalRef.current) { | |
| clearInterval(intervalRef.current); | |
| intervalRef.current = null; | |
| } | |
| toast({ | |
| title: "Recording complete", | |
| description: `Recorded ${recordingTime} seconds of audio` | |
| }); | |
| } | |
| }, [isRecording, recordingTime, toast]); | |
| const playAudio = useCallback(() => { | |
| if (audioUrl && audioRef.current) { | |
| if (isPlaying) { | |
| audioRef.current.pause(); | |
| setIsPlaying(false); | |
| } else { | |
| audioRef.current.play(); | |
| setIsPlaying(true); | |
| } | |
| } | |
| }, [audioUrl, isPlaying]); | |
| const downloadAudio = useCallback(() => { | |
| if (audioUrl) { | |
| const a = document.createElement('a'); | |
| a.href = audioUrl; | |
| a.download = `voice-sample-${Date.now()}.webm`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| } | |
| }, [audioUrl]); | |
| const clearRecording = useCallback(() => { | |
| if (audioUrl) { | |
| URL.revokeObjectURL(audioUrl); | |
| setAudioUrl(''); | |
| } | |
| setIsPlaying(false); | |
| setRecordingTime(0); | |
| }, [audioUrl]); | |
| const formatTime = (seconds: number) => { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = seconds % 60; | |
| return `${mins}:${secs.toString().padStart(2, '0')}`; | |
| }; | |
| return ( | |
| <Card className={`glass-effect ${className}`}> | |
| <CardContent className="p-6"> | |
| <div className="flex flex-col items-center space-y-6"> | |
| {/* 3D Microphone */} | |
| <div className="w-48 h-48 rounded-xl overflow-hidden"> | |
| <MicrophoneScene isRecording={isRecording} /> | |
| </div> | |
| {/* Waveform Visualization */} | |
| <div className="w-full max-w-md h-16 flex items-center justify-center"> | |
| <AudioWaveform | |
| isRecording={isRecording} | |
| isPlaying={isPlaying} | |
| bars={25} | |
| /> | |
| </div> | |
| {/* Recording Time */} | |
| {(isRecording || recordingTime > 0) && ( | |
| <div className="text-center"> | |
| <div className={`text-2xl font-mono ${isRecording ? 'text-primary' : 'text-foreground'}`}> | |
| {formatTime(recordingTime)} | |
| </div> | |
| {isRecording && ( | |
| <div className="text-sm text-muted-foreground mt-1">Recording...</div> | |
| )} | |
| </div> | |
| )} | |
| {/* Control Buttons */} | |
| <div className="flex items-center space-x-4"> | |
| {!isRecording ? ( | |
| <Button | |
| onClick={startRecording} | |
| size="lg" | |
| className="bg-primary hover:bg-primary/90 glow-primary recording-pulse" | |
| > | |
| <Mic className="w-5 h-5 mr-2" /> | |
| Start Recording | |
| </Button> | |
| ) : ( | |
| <Button | |
| onClick={stopRecording} | |
| size="lg" | |
| variant="destructive" | |
| > | |
| <MicOff className="w-5 h-5 mr-2" /> | |
| Stop Recording | |
| </Button> | |
| )} | |
| {audioUrl && !isRecording && ( | |
| <> | |
| <Button | |
| onClick={playAudio} | |
| variant="secondary" | |
| size="lg" | |
| > | |
| {isPlaying ? ( | |
| <Pause className="w-5 h-5 mr-2" /> | |
| ) : ( | |
| <Play className="w-5 h-5 mr-2" /> | |
| )} | |
| {isPlaying ? 'Pause' : 'Play'} | |
| </Button> | |
| <Button | |
| onClick={downloadAudio} | |
| variant="outline" | |
| size="sm" | |
| > | |
| <Download className="w-4 h-4" /> | |
| </Button> | |
| <Button | |
| onClick={clearRecording} | |
| variant="outline" | |
| size="sm" | |
| > | |
| <Trash2 className="w-4 h-4" /> | |
| </Button> | |
| </> | |
| )} | |
| </div> | |
| {/* Hidden audio element for playback */} | |
| {audioUrl && ( | |
| <audio | |
| ref={audioRef} | |
| src={audioUrl} | |
| onEnded={() => setIsPlaying(false)} | |
| onPause={() => setIsPlaying(false)} | |
| hidden | |
| /> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ); | |
| } |