Name108 commited on
Commit
46ec14f
·
verified ·
1 Parent(s): e383a89

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +348 -348
index.html CHANGED
@@ -1,348 +1,348 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Real-time Audio Stream</title>
7
- <!-- Tailwind CSS for styling -->
8
- <script src="https://cdn.tailwindcss.com"></script>
9
- <style>
10
- body {
11
- font-family: "Inter", sans-serif;
12
- background-color: #f3f4f6;
13
- }
14
- </style>
15
- </head>
16
- <body class="flex items-center justify-center min-h-screen">
17
- <div class="bg-white p-8 rounded-xl shadow-lg w-full max-w-2xl mx-4">
18
- <h1 class="text-3xl font-bold text-center mb-4 text-gray-800">Real-time Audio Stream</h1>
19
- <p class="text-center text-gray-600 mb-6">
20
- Capturing your microphone audio and sending it to a Python server. The server will echo the audio back with a 2-second delay.
21
- </p>
22
-
23
- <!-- Status Indicator -->
24
- <div id="status-container" class="flex items-center justify-center mb-6">
25
- <span id="status-dot" class="block h-3 w-3 rounded-full mr-2"></span>
26
- <span id="status-text" class="text-sm font-medium text-gray-700">Connecting...</span>
27
- </div>
28
-
29
- <!-- Control Buttons -->
30
- <div class="flex justify-center space-x-4">
31
- <button id="startButton" disabled class="bg-emerald-500 text-white font-semibold py-3 px-6 rounded-lg shadow-md hover:bg-emerald-600 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed">
32
- Start
33
- </button>
34
- <button id="stopButton" disabled class="bg-red-500 text-white font-semibold py-3 px-6 rounded-lg shadow-md hover:bg-red-600 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed">
35
- Stop
36
- </button>
37
- </div>
38
-
39
- <!-- Debugging Log -->
40
- <div class="mt-8">
41
- <h2 class="text-xl font-semibold mb-2 text-gray-800">Chunk Information</h2>
42
- <div id="chunk-log" class="bg-gray-100 p-4 rounded-lg overflow-y-scroll h-48 text-sm text-gray-700">
43
- <p id="initial-log-message">Waiting for audio chunks...</p>
44
- </div>
45
- <p id="chunk-count" class="text-right text-xs text-gray-500 mt-2">Chunks received: 0</p>
46
- </div>
47
-
48
- <!-- Message box for user feedback -->
49
- <div id="message-box" class="mt-6 text-sm text-center text-gray-500"></div>
50
- </div>
51
-
52
- <script>
53
- // --- Configuration ---
54
- // The WebSocket URL is now a single endpoint for all communication
55
- const WEBSOCKET_URL = (location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + 'localhost:7860/ws';
56
- const CHUNK_DURATION = 1; // seconds per chunk
57
-
58
- // --- DOM Elements ---
59
- const startButton = document.getElementById('startButton');
60
- const stopButton = document.getElementById('stopButton');
61
- const statusDot = document.getElementById('status-dot');
62
- const statusText = document.getElementById('status-text');
63
- const messageBox = document.getElementById('message-box');
64
- const chunkLog = document.getElementById('chunk-log');
65
- const initialLogMessage = document.getElementById('initial-log-message');
66
- const chunkCountDisplay = document.getElementById('chunk-count');
67
-
68
- // --- Global State Variables ---
69
- let audioContext;
70
- let micSource;
71
- let scriptNode;
72
- let gainNode;
73
- let accumulatedAudio = [];
74
- let chunkSamples;
75
- let websocket;
76
- let isRecording = false;
77
- let audioQueue = [];
78
- let isPlaying = false;
79
- let nextPlayTime = 0;
80
- let sendChunkCounter = 0;
81
- let receiveChunkCounter = 0;
82
- let expectedChunkNumber = 1;
83
-
84
- // --- Helper Functions ---
85
- function setStatus(dotColor, text) {
86
- statusDot.className = `block h-3 w-3 rounded-full mr-2 bg-${dotColor}-500`;
87
- statusText.textContent = text;
88
- }
89
-
90
- function showMessage(text, isError = false) {
91
- messageBox.textContent = text;
92
- messageBox.className = `mt-6 text-sm text-center ${isError ? 'text-red-500' : 'text-gray-500'}`;
93
- }
94
-
95
- function getCurrentTime() {
96
- const now = new Date();
97
- const hours = String(now.getHours()).padStart(2, '0');
98
- const minutes = String(now.getMinutes()).padStart(2, '0');
99
- const seconds = String(now.getSeconds()).padStart(2, '0');
100
- return `${hours}:${minutes}:${seconds}`;
101
- }
102
-
103
- function appendChunkLog(chunkNumber, loudness, isSent = false, timeToReceive = null, timeToPlay = null) {
104
- if (initialLogMessage) {
105
- initialLogMessage.remove();
106
- }
107
- const logEntry = document.createElement('p');
108
- if (isSent) {
109
- logEntry.textContent = `[${getCurrentTime()}] SENT chunk #${chunkNumber}`;
110
- logEntry.className = 'py-1 border-b border-gray-200 last:border-b-0 text-gray-500 italic';
111
- } else {
112
- let timeString = '';
113
- if (timeToReceive !== null) {
114
- timeString += ` | Receive time: ${timeToReceive.toFixed(2)}ms`;
115
- }
116
- if (timeToPlay !== null) {
117
- timeString += ` | Play time: ${timeToPlay.toFixed(2)}ms`;
118
- }
119
- logEntry.textContent = `[${getCurrentTime()}] RECEIVED chunk #${chunkNumber}: Loudness: ${loudness.toFixed(2)}${timeString}`;
120
- logEntry.className = 'py-1 border-b border-gray-200 last:border-b-0 font-bold text-gray-800';
121
-
122
- receiveChunkCounter++;
123
- chunkCountDisplay.textContent = `Chunks received: ${receiveChunkCounter}`;
124
- }
125
- chunkLog.appendChild(logEntry);
126
- chunkLog.scrollTop = chunkLog.scrollHeight;
127
- }
128
-
129
- // --- Web Audio Playback Logic ---
130
- function playbackLoop() {
131
- if (!isPlaying) return;
132
-
133
- console.log('PlaybackLoop called, audioQueue length:', audioQueue.length, 'expected:', expectedChunkNumber);
134
-
135
- // Sort queue by chunkNumber
136
- audioQueue.sort((a, b) => a.chunkNumber - b.chunkNumber);
137
-
138
- // Skip missing chunks
139
- while (audioQueue.length > 0 && audioQueue[0].chunkNumber < expectedChunkNumber) {
140
- audioQueue.shift();
141
- }
142
-
143
- let played = false;
144
- while (audioQueue.length > 0 && audioQueue[0].chunkNumber === expectedChunkNumber) {
145
- console.log('Processing chunk #', audioQueue[0].chunkNumber);
146
- played = true;
147
- const chunkData = audioQueue.shift();
148
- const { audioData, chunkNumber, loudness, receivedTime } = chunkData;
149
-
150
- const playbackStartTime = performance.now();
151
-
152
- try {
153
- const binaryString = window.atob(audioData);
154
- const len = binaryString.length;
155
- const bytes = new Uint8Array(len);
156
- for (let i = 0; i < len; i++) {
157
- bytes[i] = binaryString.charCodeAt(i);
158
- }
159
- const int16Arr = new Int16Array(bytes.buffer);
160
- const float32Arr = new Float32Array(int16Arr.length);
161
- for (let i = 0; i < int16Arr.length; i++) {
162
- float32Arr[i] = int16Arr[i] / 32768;
163
- }
164
- const audioBuffer = audioContext.createBuffer(
165
- 1,
166
- float32Arr.length,
167
- audioContext.sampleRate
168
- );
169
- audioBuffer.copyToChannel(float32Arr, 0);
170
-
171
- const playbackEndTime = performance.now();
172
- const timeToPlay = playbackEndTime - playbackStartTime;
173
-
174
- const timeToReceive = receivedTime - chunkData.workerPostTime;
175
- appendChunkLog(chunkNumber, loudness, false, timeToReceive, timeToPlay);
176
-
177
- const source = audioContext.createBufferSource();
178
- source.buffer = audioBuffer;
179
- source.connect(audioContext.destination);
180
-
181
- const currentTime = audioContext.currentTime;
182
- const delay = nextPlayTime - currentTime;
183
- source.start(currentTime + Math.max(0, delay));
184
-
185
- nextPlayTime = Math.max(nextPlayTime, currentTime) + audioBuffer.duration;
186
- expectedChunkNumber++;
187
-
188
- // Set onended only if no more to play immediately
189
- if (audioQueue.length === 0 || audioQueue[0].chunkNumber !== expectedChunkNumber) {
190
- source.onended = playbackLoop;
191
- }
192
- } catch (e) {
193
- console.error(`Error processing audio data for chunk #${chunkNumber}:`, e);
194
- }
195
- }
196
-
197
- if (!played) {
198
- setTimeout(playbackLoop, 10);
199
- }
200
- }
201
-
202
- // --- Main WebSocket and Audio Logic ---
203
- function connectWebSocket() {
204
- startButton.disabled = true;
205
- stopButton.disabled = true;
206
- setStatus('yellow', 'Connecting...');
207
- showMessage('Attempting to connect to the server...');
208
-
209
- try {
210
- websocket = new WebSocket(WEBSOCKET_URL);
211
- websocket.onopen = () => {
212
- console.log("WebSocket connection established.");
213
- setStatus('green', 'Connected');
214
- startButton.disabled = false;
215
- stopButton.disabled = true;
216
- showMessage('Successfully connected to the server. You can now start recording.');
217
- };
218
- websocket.onmessage = (event) => {
219
- console.log('WebSocket message received');
220
- const workerPostTime = performance.now();
221
- const chunkData = JSON.parse(event.data);
222
- const receivedTime = performance.now();
223
- chunkData.workerPostTime = workerPostTime;
224
- chunkData.receivedTime = receivedTime;
225
- audioQueue.push(chunkData);
226
- playbackLoop();
227
- };
228
- websocket.onclose = () => {
229
- console.log("WebSocket connection closed.");
230
- setStatus('red', 'Disconnected');
231
- startButton.disabled = true;
232
- stopButton.disabled = true;
233
- isRecording = false;
234
- isPlaying = false;
235
- audioQueue = [];
236
- };
237
- websocket.onerror = (error) => {
238
- console.error("WebSocket error:", error);
239
- setStatus('red', 'Error');
240
- showMessage('Could not connect to the server. Is the Python server running?', true);
241
- };
242
-
243
- } catch (e) {
244
- console.error("Failed to create WebSocket:", e);
245
- setStatus('red', 'Failed');
246
- showMessage('An error occurred. Check your server URLs.', true);
247
- startButton.disabled = true;
248
- stopButton.disabled = true;
249
- }
250
- }
251
-
252
- async function startRecording() {
253
- if (websocket && websocket.readyState === WebSocket.OPEN) {
254
- try {
255
- await audioContext.resume();
256
-
257
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
258
- micSource = audioContext.createMediaStreamSource(stream);
259
- scriptNode = audioContext.createScriptProcessor(4096, 1, 1);
260
- gainNode = audioContext.createGain();
261
- gainNode.gain.value = 0;
262
-
263
- micSource.connect(scriptNode);
264
- scriptNode.connect(gainNode);
265
- gainNode.connect(audioContext.destination);
266
-
267
- chunkSamples = Math.round(audioContext.sampleRate * CHUNK_DURATION);
268
- accumulatedAudio = [];
269
- scriptNode.onaudioprocess = (e) => {
270
- if (!isRecording) return;
271
- const data = e.inputBuffer.getChannelData(0);
272
- for (let i = 0; i < data.length; i++) {
273
- accumulatedAudio.push(data[i]);
274
- }
275
- while (accumulatedAudio.length >= chunkSamples) {
276
- const chunk = accumulatedAudio.splice(0, chunkSamples);
277
- const int16Arr = new Int16Array(chunk.length);
278
- for (let i = 0; i < chunk.length; i++) {
279
- int16Arr[i] = Math.max(-32768, Math.min(32767, chunk[i] * 32767));
280
- }
281
- const bytes = new Uint8Array(int16Arr.buffer);
282
- let binary = '';
283
- for (let i = 0; i < bytes.byteLength; i++) {
284
- binary += String.fromCharCode(bytes[i]);
285
- }
286
- const base64Data = btoa(binary);
287
-
288
- sendChunkCounter++;
289
- appendChunkLog(sendChunkCounter, null, true);
290
-
291
- const chunkToSend = {
292
- chunkNumber: sendChunkCounter,
293
- audioData: base64Data
294
- };
295
- if (websocket.readyState === WebSocket.OPEN) {
296
- websocket.send(JSON.stringify(chunkToSend));
297
- }
298
- }
299
- };
300
-
301
- console.log("Recording started.");
302
- startButton.disabled = true;
303
- stopButton.disabled = false;
304
- isRecording = true;
305
- showMessage("Recording... Please speak into your microphone.");
306
-
307
- if (!isPlaying) {
308
- isPlaying = true;
309
- nextPlayTime = audioContext.currentTime;
310
- playbackLoop();
311
- }
312
-
313
- } catch (err) {
314
- console.error("Failed to start recording:", err);
315
- showMessage('Failed to start recording. Check console for errors.', true);
316
- startButton.disabled = false;
317
- }
318
- } else {
319
- showMessage('Not connected to the server. Please wait or refresh.', true);
320
- }
321
- }
322
-
323
- function stopRecording() {
324
- if (isRecording) {
325
- isRecording = false;
326
- isPlaying = false;
327
- if (micSource) micSource.disconnect();
328
- if (scriptNode) scriptNode.disconnect();
329
- if (gainNode) gainNode.disconnect();
330
- const tracks = micSource?.mediaStream.getTracks();
331
- tracks?.forEach(track => track.stop());
332
- accumulatedAudio = [];
333
- console.log("Recording stopped.");
334
- startButton.disabled = false;
335
- stopButton.disabled = true;
336
- }
337
- }
338
-
339
- startButton.addEventListener('click', startRecording);
340
- stopButton.addEventListener('click', stopRecording);
341
-
342
- document.addEventListener('DOMContentLoaded', () => {
343
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
344
- connectWebSocket();
345
- });
346
- </script>
347
- </body>
348
- </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Real-time Audio Stream</title>
7
+ <!-- Tailwind CSS for styling -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <style>
10
+ body {
11
+ font-family: "Inter", sans-serif;
12
+ background-color: #f3f4f6;
13
+ }
14
+ </style>
15
+ </head>
16
+ <body class="flex items-center justify-center min-h-screen">
17
+ <div class="bg-white p-8 rounded-xl shadow-lg w-full max-w-2xl mx-4">
18
+ <h1 class="text-3xl font-bold text-center mb-4 text-gray-800">Real-time Audio Stream</h1>
19
+ <p class="text-center text-gray-600 mb-6">
20
+ Capturing your microphone audio and sending it to a Python server. The server will echo the audio back with a 2-second delay.
21
+ </p>
22
+
23
+ <!-- Status Indicator -->
24
+ <div id="status-container" class="flex items-center justify-center mb-6">
25
+ <span id="status-dot" class="block h-3 w-3 rounded-full mr-2"></span>
26
+ <span id="status-text" class="text-sm font-medium text-gray-700">Connecting...</span>
27
+ </div>
28
+
29
+ <!-- Control Buttons -->
30
+ <div class="flex justify-center space-x-4">
31
+ <button id="startButton" disabled class="bg-emerald-500 text-white font-semibold py-3 px-6 rounded-lg shadow-md hover:bg-emerald-600 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed">
32
+ Start
33
+ </button>
34
+ <button id="stopButton" disabled class="bg-red-500 text-white font-semibold py-3 px-6 rounded-lg shadow-md hover:bg-red-600 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed">
35
+ Stop
36
+ </button>
37
+ </div>
38
+
39
+ <!-- Debugging Log -->
40
+ <div class="mt-8">
41
+ <h2 class="text-xl font-semibold mb-2 text-gray-800">Chunk Information</h2>
42
+ <div id="chunk-log" class="bg-gray-100 p-4 rounded-lg overflow-y-scroll h-48 text-sm text-gray-700">
43
+ <p id="initial-log-message">Waiting for audio chunks...</p>
44
+ </div>
45
+ <p id="chunk-count" class="text-right text-xs text-gray-500 mt-2">Chunks received: 0</p>
46
+ </div>
47
+
48
+ <!-- Message box for user feedback -->
49
+ <div id="message-box" class="mt-6 text-sm text-center text-gray-500"></div>
50
+ </div>
51
+
52
+ <script>
53
+ // --- Configuration ---
54
+ // The WebSocket URL is now dynamic to work on any server (e.g., Hugging Face Spaces)
55
+ const WEBSOCKET_URL = (location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + location.host + '/ws';
56
+ const CHUNK_DURATION = 1; // seconds per chunk
57
+
58
+ // --- DOM Elements ---
59
+ const startButton = document.getElementById('startButton');
60
+ const stopButton = document.getElementById('stopButton');
61
+ const statusDot = document.getElementById('status-dot');
62
+ const statusText = document.getElementById('status-text');
63
+ const messageBox = document.getElementById('message-box');
64
+ const chunkLog = document.getElementById('chunk-log');
65
+ const initialLogMessage = document.getElementById('initial-log-message');
66
+ const chunkCountDisplay = document.getElementById('chunk-count');
67
+
68
+ // --- Global State Variables ---
69
+ let audioContext;
70
+ let micSource;
71
+ let scriptNode;
72
+ let gainNode;
73
+ let accumulatedAudio = [];
74
+ let chunkSamples;
75
+ let websocket;
76
+ let isRecording = false;
77
+ let audioQueue = [];
78
+ let isPlaying = false;
79
+ let nextPlayTime = 0;
80
+ let sendChunkCounter = 0;
81
+ let receiveChunkCounter = 0;
82
+ let expectedChunkNumber = 1;
83
+
84
+ // --- Helper Functions ---
85
+ function setStatus(dotColor, text) {
86
+ statusDot.className = `block h-3 w-3 rounded-full mr-2 bg-${dotColor}-500`;
87
+ statusText.textContent = text;
88
+ }
89
+
90
+ function showMessage(text, isError = false) {
91
+ messageBox.textContent = text;
92
+ messageBox.className = `mt-6 text-sm text-center ${isError ? 'text-red-500' : 'text-gray-500'}`;
93
+ }
94
+
95
+ function getCurrentTime() {
96
+ const now = new Date();
97
+ const hours = String(now.getHours()).padStart(2, '0');
98
+ const minutes = String(now.getMinutes()).padStart(2, '0');
99
+ const seconds = String(now.getSeconds()).padStart(2, '0');
100
+ return `${hours}:${minutes}:${seconds}`;
101
+ }
102
+
103
+ function appendChunkLog(chunkNumber, loudness, isSent = false, timeToReceive = null, timeToPlay = null) {
104
+ if (initialLogMessage) {
105
+ initialLogMessage.remove();
106
+ }
107
+ const logEntry = document.createElement('p');
108
+ if (isSent) {
109
+ logEntry.textContent = `[${getCurrentTime()}] SENT chunk #${chunkNumber}`;
110
+ logEntry.className = 'py-1 border-b border-gray-200 last:border-b-0 text-gray-500 italic';
111
+ } else {
112
+ let timeString = '';
113
+ if (timeToReceive !== null) {
114
+ timeString += ` | Receive time: ${timeToReceive.toFixed(2)}ms`;
115
+ }
116
+ if (timeToPlay !== null) {
117
+ timeString += ` | Play time: ${timeToPlay.toFixed(2)}ms`;
118
+ }
119
+ logEntry.textContent = `[${getCurrentTime()}] RECEIVED chunk #${chunkNumber}: Loudness: ${loudness.toFixed(2)}${timeString}`;
120
+ logEntry.className = 'py-1 border-b border-gray-200 last:border-b-0 font-bold text-gray-800';
121
+
122
+ receiveChunkCounter++;
123
+ chunkCountDisplay.textContent = `Chunks received: ${receiveChunkCounter}`;
124
+ }
125
+ chunkLog.appendChild(logEntry);
126
+ chunkLog.scrollTop = chunkLog.scrollHeight;
127
+ }
128
+
129
+ // --- Web Audio Playback Logic ---
130
+ function playbackLoop() {
131
+ if (!isPlaying) return;
132
+
133
+ console.log('PlaybackLoop called, audioQueue length:', audioQueue.length, 'expected:', expectedChunkNumber);
134
+
135
+ // Sort queue by chunkNumber
136
+ audioQueue.sort((a, b) => a.chunkNumber - b.chunkNumber);
137
+
138
+ // Skip missing chunks
139
+ while (audioQueue.length > 0 && audioQueue[0].chunkNumber < expectedChunkNumber) {
140
+ audioQueue.shift();
141
+ }
142
+
143
+ let played = false;
144
+ while (audioQueue.length > 0 && audioQueue[0].chunkNumber === expectedChunkNumber) {
145
+ console.log('Processing chunk #', audioQueue[0].chunkNumber);
146
+ played = true;
147
+ const chunkData = audioQueue.shift();
148
+ const { audioData, chunkNumber, loudness, receivedTime } = chunkData;
149
+
150
+ const playbackStartTime = performance.now();
151
+
152
+ try {
153
+ const binaryString = window.atob(audioData);
154
+ const len = binaryString.length;
155
+ const bytes = new Uint8Array(len);
156
+ for (let i = 0; i < len; i++) {
157
+ bytes[i] = binaryString.charCodeAt(i);
158
+ }
159
+ const int16Arr = new Int16Array(bytes.buffer);
160
+ const float32Arr = new Float32Array(int16Arr.length);
161
+ for (let i = 0; i < int16Arr.length; i++) {
162
+ float32Arr[i] = int16Arr[i] / 32768;
163
+ }
164
+ const audioBuffer = audioContext.createBuffer(
165
+ 1,
166
+ float32Arr.length,
167
+ audioContext.sampleRate
168
+ );
169
+ audioBuffer.copyToChannel(float32Arr, 0);
170
+
171
+ const playbackEndTime = performance.now();
172
+ const timeToPlay = playbackEndTime - playbackStartTime;
173
+
174
+ const timeToReceive = receivedTime - chunkData.workerPostTime;
175
+ appendChunkLog(chunkNumber, loudness, false, timeToReceive, timeToPlay);
176
+
177
+ const source = audioContext.createBufferSource();
178
+ source.buffer = audioBuffer;
179
+ source.connect(audioContext.destination);
180
+
181
+ const currentTime = audioContext.currentTime;
182
+ const delay = nextPlayTime - currentTime;
183
+ source.start(currentTime + Math.max(0, delay));
184
+
185
+ nextPlayTime = Math.max(nextPlayTime, currentTime) + audioBuffer.duration;
186
+ expectedChunkNumber++;
187
+
188
+ // Set onended only if no more to play immediately
189
+ if (audioQueue.length === 0 || audioQueue[0].chunkNumber !== expectedChunkNumber) {
190
+ source.onended = playbackLoop;
191
+ }
192
+ } catch (e) {
193
+ console.error(`Error processing audio data for chunk #${chunkNumber}:`, e);
194
+ }
195
+ }
196
+
197
+ if (!played) {
198
+ setTimeout(playbackLoop, 10);
199
+ }
200
+ }
201
+
202
+ // --- Main WebSocket and Audio Logic ---
203
+ function connectWebSocket() {
204
+ startButton.disabled = true;
205
+ stopButton.disabled = true;
206
+ setStatus('yellow', 'Connecting...');
207
+ showMessage('Attempting to connect to the server...');
208
+
209
+ try {
210
+ websocket = new WebSocket(WEBSOCKET_URL);
211
+ websocket.onopen = () => {
212
+ console.log("WebSocket connection established.");
213
+ setStatus('green', 'Connected');
214
+ startButton.disabled = false;
215
+ stopButton.disabled = true;
216
+ showMessage('Successfully connected to the server. You can now start recording.');
217
+ };
218
+ websocket.onmessage = (event) => {
219
+ console.log('WebSocket message received');
220
+ const workerPostTime = performance.now();
221
+ const chunkData = JSON.parse(event.data);
222
+ const receivedTime = performance.now();
223
+ chunkData.workerPostTime = workerPostTime;
224
+ chunkData.receivedTime = receivedTime;
225
+ audioQueue.push(chunkData);
226
+ playbackLoop();
227
+ };
228
+ websocket.onclose = () => {
229
+ console.log("WebSocket connection closed.");
230
+ setStatus('red', 'Disconnected');
231
+ startButton.disabled = true;
232
+ stopButton.disabled = true;
233
+ isRecording = false;
234
+ isPlaying = false;
235
+ audioQueue = [];
236
+ };
237
+ websocket.onerror = (error) => {
238
+ console.error("WebSocket error:", error);
239
+ setStatus('red', 'Error');
240
+ showMessage('Could not connect to the server. Is the Python server running?', true);
241
+ };
242
+
243
+ } catch (e) {
244
+ console.error("Failed to create WebSocket:", e);
245
+ setStatus('red', 'Failed');
246
+ showMessage('An error occurred. Check your server URLs.', true);
247
+ startButton.disabled = true;
248
+ stopButton.disabled = true;
249
+ }
250
+ }
251
+
252
+ async function startRecording() {
253
+ if (websocket && websocket.readyState === WebSocket.OPEN) {
254
+ try {
255
+ await audioContext.resume();
256
+
257
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
258
+ micSource = audioContext.createMediaStreamSource(stream);
259
+ scriptNode = audioContext.createScriptProcessor(4096, 1, 1);
260
+ gainNode = audioContext.createGain();
261
+ gainNode.gain.value = 0;
262
+
263
+ micSource.connect(scriptNode);
264
+ scriptNode.connect(gainNode);
265
+ gainNode.connect(audioContext.destination);
266
+
267
+ chunkSamples = Math.round(audioContext.sampleRate * CHUNK_DURATION);
268
+ accumulatedAudio = [];
269
+ scriptNode.onaudioprocess = (e) => {
270
+ if (!isRecording) return;
271
+ const data = e.inputBuffer.getChannelData(0);
272
+ for (let i = 0; i < data.length; i++) {
273
+ accumulatedAudio.push(data[i]);
274
+ }
275
+ while (accumulatedAudio.length >= chunkSamples) {
276
+ const chunk = accumulatedAudio.splice(0, chunkSamples);
277
+ const int16Arr = new Int16Array(chunk.length);
278
+ for (let i = 0; i < chunk.length; i++) {
279
+ int16Arr[i] = Math.max(-32768, Math.min(32767, chunk[i] * 32767));
280
+ }
281
+ const bytes = new Uint8Array(int16Arr.buffer);
282
+ let binary = '';
283
+ for (let i = 0; i < bytes.byteLength; i++) {
284
+ binary += String.fromCharCode(bytes[i]);
285
+ }
286
+ const base64Data = btoa(binary);
287
+
288
+ sendChunkCounter++;
289
+ appendChunkLog(sendChunkCounter, null, true);
290
+
291
+ const chunkToSend = {
292
+ chunkNumber: sendChunkCounter,
293
+ audioData: base64Data
294
+ };
295
+ if (websocket.readyState === WebSocket.OPEN) {
296
+ websocket.send(JSON.stringify(chunkToSend));
297
+ }
298
+ }
299
+ };
300
+
301
+ console.log("Recording started.");
302
+ startButton.disabled = true;
303
+ stopButton.disabled = false;
304
+ isRecording = true;
305
+ showMessage("Recording... Please speak into your microphone.");
306
+
307
+ if (!isPlaying) {
308
+ isPlaying = true;
309
+ nextPlayTime = audioContext.currentTime;
310
+ playbackLoop();
311
+ }
312
+
313
+ } catch (err) {
314
+ console.error("Failed to start recording:", err);
315
+ showMessage('Failed to start recording. Check console for errors.', true);
316
+ startButton.disabled = false;
317
+ }
318
+ } else {
319
+ showMessage('Not connected to the server. Please wait or refresh.', true);
320
+ }
321
+ }
322
+
323
+ function stopRecording() {
324
+ if (isRecording) {
325
+ isRecording = false;
326
+ isPlaying = false;
327
+ if (micSource) micSource.disconnect();
328
+ if (scriptNode) scriptNode.disconnect();
329
+ if (gainNode) gainNode.disconnect();
330
+ const tracks = micSource?.mediaStream.getTracks();
331
+ tracks?.forEach(track => track.stop());
332
+ accumulatedAudio = [];
333
+ console.log("Recording stopped.");
334
+ startButton.disabled = false;
335
+ stopButton.disabled = true;
336
+ }
337
+ }
338
+
339
+ startButton.addEventListener('click', startRecording);
340
+ stopButton.addEventListener('click', stopRecording);
341
+
342
+ document.addEventListener('DOMContentLoaded', () => {
343
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
344
+ connectWebSocket();
345
+ });
346
+ </script>
347
+ </body>
348
+ </html>