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