Spaces:
Runtime error
Runtime error
File size: 6,765 Bytes
3baa9da |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
'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>
);
}; |