KingNish's picture
Upload 29 files
3baa9da verified
'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>
);
};