'use client'; import React, { useState, useRef, useEffect } from 'react'; import { Mic, Square, Save, Play, SkipForward, SkipBack, Star, Volume2 } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { toast } from 'sonner'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { cn } from '@/lib/utils'; interface AudioRecorderProps { speakerId: string; datasetName: string; text: string; fontStyle: string; index: number; onSaved: () => void; onNext: () => void; onPrev: () => void; onSkip: () => void; hasPrev: boolean; hasNext: boolean; autoAdvance: boolean; autoSave: boolean; silenceThreshold: number; } const EMOTIONS = [ { id: 'neutral', label: 'Neutral', color: 'bg-gray-500' }, { id: 'happy', label: 'Happy', color: 'bg-yellow-500' }, { id: 'sad', label: 'Sad', color: 'bg-blue-500' }, { id: 'angry', label: 'Angry', color: 'bg-red-500' }, { id: 'surprised', label: 'Surprised', color: 'bg-purple-500' }, { id: 'whisper', label: 'Whisper', color: 'bg-indigo-500' }, { id: 'excited', label: 'Excited', color: 'bg-orange-500' }, ]; export default function AudioRecorder({ speakerId, datasetName, text, fontStyle, index, onSaved, onNext, onPrev, onSkip, hasPrev, hasNext, autoAdvance, autoSave, silenceThreshold }: AudioRecorderProps) { const [isRecording, setIsRecording] = useState(false); const [audioBlob, setAudioBlob] = useState(null); const [audioUrl, setAudioUrl] = useState(null); const [isSaving, setIsSaving] = useState(false); const [isSilent, setIsSilent] = useState(false); const [emotion, setEmotion] = useState('neutral'); const [rating, setRating] = useState(3); const [duration, setDuration] = useState(0); const [audioLevel, setAudioLevel] = useState(0); const mediaRecorderRef = useRef(null); const chunksRef = useRef([]); const canvasRef = useRef(null); const audioContextRef = useRef(null); const analyserRef = useRef(null); const sourceRef = useRef(null); const animationFrameRef = useRef(null); const startTimeRef = useRef(0); useEffect(() => { return () => { if (audioUrl) URL.revokeObjectURL(audioUrl); if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current); if (audioContextRef.current) audioContextRef.current.close(); }; }, [audioUrl]); const startRecording = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); mediaRecorderRef.current = new MediaRecorder(stream); chunksRef.current = []; startTimeRef.current = Date.now(); mediaRecorderRef.current.ondataavailable = (e) => { if (e.data.size > 0) chunksRef.current.push(e.data); }; mediaRecorderRef.current.onstop = () => { const blob = new Blob(chunksRef.current, { type: 'audio/wav' }); setAudioBlob(blob); setAudioUrl(URL.createObjectURL(blob)); setDuration((Date.now() - startTimeRef.current) / 1000); stopVisualizer(); if (autoSave) { saveRecording(blob); } }; mediaRecorderRef.current.start(); setIsRecording(true); startVisualizer(stream); toast.info('Recording started'); } catch (err) { console.error('Error accessing microphone:', err); toast.error('Could not access microphone'); } }; const stopRecording = () => { if (mediaRecorderRef.current && isRecording) { mediaRecorderRef.current.stop(); setIsRecording(false); mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop()); toast.info('Recording stopped'); } }; const startVisualizer = (stream: MediaStream) => { if (!canvasRef.current) return; // @ts-ignore audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)(); analyserRef.current = audioContextRef.current.createAnalyser(); sourceRef.current = audioContextRef.current.createMediaStreamSource(stream); sourceRef.current.connect(analyserRef.current); analyserRef.current.fftSize = 256; const bufferLength = analyserRef.current.frequencyBinCount; const dataArray = new Uint8Array(bufferLength); const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) return; const draw = () => { if (!analyserRef.current) return; animationFrameRef.current = requestAnimationFrame(draw); analyserRef.current.getByteTimeDomainData(dataArray); // Calculate RMS for level meter let sum = 0; for (let i = 0; i < bufferLength; i++) { const x = (dataArray[i] - 128) / 128.0; sum += x * x; } const rms = Math.sqrt(sum / bufferLength); const db = 20 * Math.log10(rms); // Normalize db to 0-1 range (approx -60db to 0db) const level = Math.max(0, (db + 60) / 60); setAudioLevel(level); // Silence detection const isCurrentlySilent = level < (silenceThreshold / 100); setIsSilent(isCurrentlySilent); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.lineWidth = 2; ctx.strokeStyle = isCurrentlySilent ? 'rgb(239, 68, 68)' : 'rgb(139, 92, 246)'; ctx.beginPath(); const sliceWidth = canvas.width * 1.0 / bufferLength; let x = 0; for (let i = 0; i < bufferLength; i++) { const v = dataArray[i] / 128.0; const y = v * canvas.height / 2; if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } x += sliceWidth; } ctx.lineTo(canvas.width, canvas.height / 2); ctx.stroke(); }; draw(); }; const stopVisualizer = () => { if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current); setAudioLevel(0); }; const saveRecording = async (blobToSave: Blob | null = audioBlob) => { if (!blobToSave || !speakerId || !datasetName) { toast.error('Missing recording or metadata'); return; } setIsSaving(true); const formData = new FormData(); formData.append('audio', blobToSave); formData.append('metadata', JSON.stringify({ speaker_id: speakerId, dataset_name: datasetName, text: text, font_style: fontStyle, index: index, emotion: emotion, rating: rating, duration: duration, timestamp: new Date().toISOString() })); try { const res = await fetch('/api/save-recording', { method: 'POST', body: formData }); if (res.ok) { onSaved(); setAudioBlob(null); setAudioUrl(null); toast.success('Recording saved successfully'); if (autoAdvance) { onNext(); } } else { toast.error('Failed to save recording'); } } catch (err) { console.error('Error saving:', err); toast.error('Error saving recording'); } finally { setIsSaving(false); } }; return ( {/* Meter Bar */}
{isRecording && isSilent && ( SILENCE DETECTED )}
{!isRecording ? ( ) : ( )}
{audioUrl && (
{EMOTIONS.map((e) => ( ))}
{[1, 2, 3, 4, 5].map((star) => ( ))}
)}
); }