Spaces:
Runtime error
Runtime error
'use client'; | |
// AudioInput.tsx | |
import React, { useRef, useState } from "react"; | |
import { readAudio } from './audioUtils'; // Import the updated utility | |
interface AudioInputProps { | |
input: Blob | null; | |
setInput: (v: Blob | null) => void; | |
classify: (input: Float32Array) => void; // Still needs Float32Array | |
ready: boolean | null; | |
} | |
export const AudioInput = ({ input, setInput, classify, ready }: AudioInputProps) => { | |
const [recording, setRecording] = useState(false); | |
const [audioUrl, setAudioUrl] = useState<string | null>(null); | |
const mediaRecorderRef = useRef<MediaRecorder | null>(null); | |
const chunks = useRef<Blob[]>([]); | |
const fileInputRef = useRef<HTMLInputElement>(null); | |
const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => { | |
e.preventDefault(); | |
if (e.dataTransfer.files.length > 0) { | |
const file = e.dataTransfer.files[0]; | |
if (file.type.startsWith("audio/")) { | |
setInput(file); | |
// Revoke previous URL to free memory | |
if (audioUrl) URL.revokeObjectURL(audioUrl); | |
setAudioUrl(URL.createObjectURL(file)); | |
try { | |
const audioData = await readAudio(file); // Now decodes AND resamples to Float32Array PCM | |
classify(audioData); | |
} catch (error) { | |
console.error("Error reading or processing audio file:", error); | |
// Handle error, e.g., show a message to the user | |
} | |
} | |
} | |
}; | |
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { | |
if (e.target.files && e.target.files.length > 0) { | |
const file = e.target.files[0]; | |
if (file.type.startsWith("audio/")) { | |
setInput(file); | |
// Revoke previous URL to free memory | |
if (audioUrl) URL.revokeObjectURL(audioUrl); | |
setAudioUrl(URL.createObjectURL(file)); | |
try { | |
const audioData = await readAudio(file); // Now decodes AND resamples to Float32Array PCM | |
classify(audioData); | |
} catch (error) { | |
console.error("Error reading or processing audio file:", error); | |
// Handle error | |
} | |
} | |
} | |
}; | |
const startRecording = async () => { | |
try { | |
setRecording(true); | |
chunks.current = []; | |
// Ensure audioUrl is cleared for new recording | |
if (audioUrl) URL.revokeObjectURL(audioUrl); | |
setAudioUrl(null); | |
setInput(null); // Clear previous input blob | |
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
const mediaRecorder = new window.MediaRecorder(stream); | |
mediaRecorderRef.current = mediaRecorder; | |
mediaRecorder.ondataavailable = (e) => { | |
if (e.data.size > 0) { | |
chunks.current.push(e.data); | |
} | |
}; | |
mediaRecorder.onstop = async () => { | |
const blob = new Blob(chunks.current, { type: "audio/webm" }); // MediaRecorder outputs a Blob | |
setInput(blob); // Set the Blob input if needed elsewhere | |
// Revoke previous URL | |
if (audioUrl) URL.revokeObjectURL(audioUrl); | |
setAudioUrl(URL.createObjectURL(blob)); // Create URL for playback | |
try { | |
const audioData = await readAudio(blob); // Decode AND resample Blob to Float32Array PCM | |
classify(audioData); // Pass the Float32Array PCM data | |
} catch (error) { | |
console.error("Error reading or processing recorded audio:", error); | |
// Handle error | |
} finally { | |
// Always stop tracks after recording stops | |
stream.getTracks().forEach(track => track.stop()); | |
} | |
}; | |
mediaRecorder.start(); | |
} catch (error) { | |
console.error("Error starting recording:", error); | |
setRecording(false); // Ensure recording state is reset on error | |
// Handle error, e.g., show a message to the user that mic access failed | |
} | |
}; | |
const stopRecording = async () => { | |
if (!mediaRecorderRef.current) return; | |
// The actual classification and setting of input/audioUrl happens in mediaRecorder.onstop | |
mediaRecorderRef.current.stop(); | |
setRecording(false); // Set recording state to false immediately | |
}; | |
// Added error handling and URL cleanup | |
React.useEffect(() => { | |
// Cleanup object URLs when component unmounts or audioUrl changes | |
return () => { | |
if (audioUrl) URL.revokeObjectURL(audioUrl); | |
}; | |
}, [audioUrl]); | |
return ( | |
<div className="flex flex-col gap-4 h-full"> | |
<label className="block text-gray-600 mb-2 text-sm font-medium">Upload or record audio</label> | |
<div | |
className={`flex-1 flex flex-col items-center justify-center border-2 border-dashed rounded-lg p-6 bg-gray-50 transition | |
${ready === false ? 'border-gray-200 text-gray-400 cursor-not-allowed' : 'border-gray-300 cursor-pointer hover:border-blue-400'} | |
`} | |
onDrop={handleDrop} | |
onDragOver={e => e.preventDefault()} | |
onClick={() => ready !== false && fileInputRef.current?.click()} // Prevent click if not ready | |
style={{ minHeight: 120 }} | |
> | |
<input | |
ref={fileInputRef} | |
type="file" | |
accept="audio/*" | |
style={{ display: "none" }} | |
onChange={handleFileChange} | |
disabled={ready === false} | |
/> | |
<span className="text-gray-500 text-center"> | |
{ ready === false ? "Loading models..." : "Drag & drop audio file here or click to select" } | |
</span> | |
</div> | |
<div className="flex items-center gap-4"> | |
{!recording ? ( | |
<button | |
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 transition disabled:opacity-50 disabled:cursor-not-allowed" | |
onClick={startRecording} | |
disabled={ready === false} // Disable record button if not ready | |
> | |
Record | |
</button> | |
) : ( | |
<button | |
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition" | |
onClick={stopRecording} | |
> | |
Stop | |
</button> | |
)} | |
{/* Only show audio player if not recording and audioUrl exists */} | |
{!recording && audioUrl && ( | |
<audio controls src={audioUrl} className="ml-4 flex-1"> | |
Your browser does not support the audio element. | |
</audio> | |
)} | |
{ready === false && <span className="text-gray-600 ml-auto">Loading...</span>} {/* Optional loading indicator */} | |
</div> | |
</div> | |
); | |
}; |