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> | |
| ); | |
| }; |