AJ50's picture
Initial voice cloning backend with all dependencies
5008b66
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>
);
}