Name108 commited on
Commit
0543c56
·
verified ·
1 Parent(s): 191435f

Upload 2 files

Browse files
Files changed (2) hide show
  1. EchoAudio.py +125 -0
  2. client.html +388 -0
EchoAudio.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import websockets
3
+ from collections import deque
4
+ import time
5
+ import json
6
+ import base64
7
+ import uuid
8
+ import numpy as np
9
+ import sys
10
+
11
+ # --- Configuration ---
12
+ RECEIVE_HOST = "localhost"
13
+ RECEIVE_PORT = 8765
14
+
15
+ SEND_HOST = "localhost"
16
+ SEND_PORT = 8766
17
+
18
+ # The target delay in seconds.
19
+ TARGET_DELAY_SECONDS = 2
20
+ # The client sends a chunk of data every 1 second (1000ms).
21
+ CHUNK_SEND_INTERVAL = 1.0
22
+
23
+ # --- Server Logic ---
24
+ async def receive_from_client_handler(websocket, internal_audio_queue):
25
+ """
26
+ Handles a single WebSocket connection for receiving audio from the client.
27
+ This server's only job is to receive and process data, then put it in a queue.
28
+ """
29
+ print(f"[{time.strftime('%H:%M:%S')}] Receive server: New client connected from {websocket.remote_address}")
30
+ try:
31
+ async for message in websocket:
32
+ print(f"[{time.strftime('%H:%M:%S')}] Receive server: Message received")
33
+ # The client sends a JSON string, so we need to parse it.
34
+ message_object = json.loads(message)
35
+
36
+ # Decode the base64 audio data back to a binary array
37
+ audio_data_base64 = message_object['audioData']
38
+ audio_bytes = base64.b64decode(audio_data_base64)
39
+
40
+ # Truncate to even length for int16 processing
41
+ audio_bytes = audio_bytes[:(len(audio_bytes) // 2) * 2]
42
+
43
+ # Calculate loudness (RMS) of the audio chunk
44
+ audio_samples = np.frombuffer(audio_bytes, dtype=np.int16)
45
+ rms = np.sqrt(np.mean(np.square(audio_samples.astype(np.float64)))) if audio_samples.size > 0 else 0
46
+ rms = float(rms)
47
+
48
+ # Add loudness and a new ID to the message object to send back
49
+ message_object['id'] = str(uuid.uuid4())
50
+ message_object['loudness'] = rms
51
+
52
+ await internal_audio_queue.put(message_object)
53
+ print(f"[{time.strftime('%H:%M:%S')}] Receive server: Received chunk #{message_object['chunkNumber']} with ID {message_object['id']}. Queue size: {internal_audio_queue.qsize()}")
54
+ except websockets.exceptions.ConnectionClosed as e:
55
+ print(f"[{time.strftime('%H:%M:%S')}] Receive server: Connection closed with code {e.code}")
56
+ except asyncio.CancelledError:
57
+ print(f"[{time.strftime('%H:%M:%S')}] Receive server: Task cancelled.")
58
+ except Exception as e:
59
+ print(f"[{time.strftime('%H:%M:%S')}] Receive server: Error: {e}")
60
+ finally:
61
+ print(f"[{time.strftime('%H:%M:%S')}] Receive server: Client disconnected.")
62
+
63
+ async def send_to_client_handler(websocket, internal_audio_queue):
64
+ """
65
+ Handles a single WebSocket connection for sending delayed audio to the client.
66
+ This server's only job is to get data from the internal queue and send it.
67
+ """
68
+ print(f"[{time.strftime('%H:%M:%S')}] Send server: New client connected from {websocket.remote_address}")
69
+ internal_audio_buffer = deque()
70
+
71
+ try:
72
+ print(f"[{time.strftime('%H:%M:%S')}] Send server: Waiting for buffer to fill...")
73
+ # Fill the initial buffer. This blocks until enough chunks are available.
74
+ while len(internal_audio_buffer) < (TARGET_DELAY_SECONDS / CHUNK_SEND_INTERVAL):
75
+ chunk = await internal_audio_queue.get()
76
+ internal_audio_buffer.append(chunk)
77
+ print(f"[{time.strftime('%H:%M:%S')}] Send server: Filling buffer... Current size: {len(internal_audio_buffer)}")
78
+
79
+ print(f"[{time.strftime('%H:%M:%S')}] Send server: Buffer filled. Starting to echo audio back.")
80
+
81
+ # Main loop to continuously process and send data
82
+ while True:
83
+ # Get the next chunk from the queue. This will block until a new chunk is available.
84
+ new_chunk = await internal_audio_queue.get()
85
+
86
+ # Put the new chunk into the buffer and pop the oldest one
87
+ internal_audio_buffer.append(new_chunk)
88
+ chunk_to_send = internal_audio_buffer.popleft()
89
+
90
+ await websocket.send(json.dumps(chunk_to_send))
91
+ print(f"[{time.strftime('%H:%M:%S')}] Send server: Sent chunk #{chunk_to_send['chunkNumber']} with ID {chunk_to_send['id']}. Buffer size: {len(internal_audio_buffer)}")
92
+
93
+ except websockets.exceptions.ConnectionClosed as e:
94
+ print(f"[{time.strftime('%H:%M:%S')}] Send server: Connection closed with code {e.code}")
95
+ except asyncio.CancelledError:
96
+ print(f"[{time.strftime('%H:%M:%S')}] Send server: Task cancelled.")
97
+ finally:
98
+ print(f"[{time.strftime('%H:%M:%S')}] Send server: Client disconnected.")
99
+
100
+ # --- Main Server Startup ---
101
+ async def main():
102
+ """
103
+ Starts both WebSocket servers concurrently and keeps them running.
104
+ """
105
+ print(f"[{time.strftime('%H:%M:%S')}] Starting servers...")
106
+
107
+ # Create the internal queue for communication between servers
108
+ internal_audio_queue = asyncio.Queue()
109
+
110
+ receive_server = await websockets.serve(lambda ws: receive_from_client_handler(ws, internal_audio_queue), RECEIVE_HOST, RECEIVE_PORT)
111
+ print(f"[{time.strftime('%H:%M:%S')}] Receive server started on ws://{RECEIVE_HOST}:{RECEIVE_PORT}")
112
+
113
+ send_server = await websockets.serve(lambda ws: send_to_client_handler(ws, internal_audio_queue), SEND_HOST, SEND_PORT)
114
+ print(f"[{time.strftime('%H:%M:%S')}] Send server started on ws://{SEND_HOST}:{SEND_PORT}")
115
+
116
+ await asyncio.gather(receive_server.wait_closed(), send_server.wait_closed())
117
+
118
+ if __name__ == "__main__":
119
+ if "win" in sys.platform:
120
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
121
+
122
+ try:
123
+ asyncio.run(main())
124
+ except KeyboardInterrupt:
125
+ print("\nServer shutting down gracefully...")
client.html ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-send" class="block h-3 w-3 rounded-full mr-2"></span>
26
+ <span id="status-text-send" class="text-sm font-medium text-gray-700 mr-4">Connecting Send...</span>
27
+ <span id="status-dot-receive" class="block h-3 w-3 rounded-full mr-2"></span>
28
+ <span id="status-text-receive" class="text-sm font-medium text-gray-700">Connecting Receive...</span>
29
+ </div>
30
+
31
+ <!-- Control Buttons -->
32
+ <div class="flex justify-center space-x-4">
33
+ <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">
34
+ Start
35
+ </button>
36
+ <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">
37
+ Stop
38
+ </button>
39
+ </div>
40
+
41
+ <!-- Debugging Log -->
42
+ <div class="mt-8">
43
+ <h2 class="text-xl font-semibold mb-2 text-gray-800">Chunk Information</h2>
44
+ <div id="chunk-log" class="bg-gray-100 p-4 rounded-lg overflow-y-scroll h-48 text-sm text-gray-700">
45
+ <p id="initial-log-message">Waiting for audio chunks...</p>
46
+ </div>
47
+ <p id="chunk-count" class="text-right text-xs text-gray-500 mt-2">Chunks received: 0</p>
48
+ </div>
49
+
50
+ <!-- Message box for user feedback -->
51
+ <div id="message-box" class="mt-6 text-sm text-center text-gray-500"></div>
52
+ </div>
53
+
54
+ <script>
55
+ // --- Configuration ---
56
+ const SEND_WEBSOCKET_URL = "ws://localhost:8765";
57
+ const RECEIVE_WEBSOCKET_URL = "ws://localhost:8766";
58
+ const CHUNK_DURATION = 1; // seconds per chunk
59
+
60
+ // --- DOM Elements ---
61
+ const startButton = document.getElementById('startButton');
62
+ const stopButton = document.getElementById('stopButton');
63
+ const statusDotSend = document.getElementById('status-dot-send');
64
+ const statusTextSend = document.getElementById('status-text-send');
65
+ const statusDotReceive = document.getElementById('status-dot-receive');
66
+ const statusTextReceive = document.getElementById('status-text-receive');
67
+ const messageBox = document.getElementById('message-box');
68
+ const chunkLog = document.getElementById('chunk-log');
69
+ const initialLogMessage = document.getElementById('initial-log-message');
70
+ const chunkCountDisplay = document.getElementById('chunk-count');
71
+
72
+ // --- Global State Variables ---
73
+ let audioContext;
74
+ let micSource;
75
+ let scriptNode;
76
+ let gainNode;
77
+ let accumulatedAudio = [];
78
+ let chunkSamples;
79
+ let sendWebSocket;
80
+ let receiveWebSocket;
81
+ let isRecording = false;
82
+ let audioQueue = [];
83
+ let isPlaying = false;
84
+ let nextPlayTime = 0;
85
+ let sendChunkCounter = 0;
86
+ let receiveChunkCounter = 0;
87
+ let expectedChunkNumber = 1;
88
+
89
+ // --- Helper Functions ---
90
+ function setStatus(connection, dotColor, text) {
91
+ if (connection === 'send') {
92
+ statusDotSend.className = `block h-3 w-3 rounded-full mr-2 bg-${dotColor}-500`;
93
+ statusTextSend.textContent = text;
94
+ } else if (connection === 'receive') {
95
+ statusDotReceive.className = `block h-3 w-3 rounded-full mr-2 bg-${dotColor}-500`;
96
+ statusTextReceive.textContent = text;
97
+ }
98
+ }
99
+
100
+ function showMessage(text, isError = false) {
101
+ messageBox.textContent = text;
102
+ messageBox.className = `mt-6 text-sm text-center ${isError ? 'text-red-500' : 'text-gray-500'}`;
103
+ }
104
+
105
+ function getCurrentTime() {
106
+ const now = new Date();
107
+ const hours = String(now.getHours()).padStart(2, '0');
108
+ const minutes = String(now.getMinutes()).padStart(2, '0');
109
+ const seconds = String(now.getSeconds()).padStart(2, '0');
110
+ return `${hours}:${minutes}:${seconds}`;
111
+ }
112
+
113
+ function appendChunkLog(chunkNumber, id, loudness, isSent = false, timeToReceive = null, timeToPlay = null) {
114
+ if (initialLogMessage) {
115
+ initialLogMessage.remove();
116
+ }
117
+ const logEntry = document.createElement('p');
118
+ if (isSent) {
119
+ logEntry.textContent = `[${getCurrentTime()}] SENT chunk #${chunkNumber}`;
120
+ logEntry.className = 'py-1 border-b border-gray-200 last:border-b-0 text-gray-500 italic';
121
+ } else {
122
+ let timeString = '';
123
+ if (timeToReceive !== null) {
124
+ timeString += ` | Receive time: ${timeToReceive.toFixed(2)}ms`;
125
+ }
126
+ if (timeToPlay !== null) {
127
+ timeString += ` | Play time: ${timeToPlay.toFixed(2)}ms`;
128
+ }
129
+ logEntry.textContent = `[${getCurrentTime()}] RECEIVED chunk #${chunkNumber}: Loudness: ${loudness.toFixed(2)}${timeString}`;
130
+ logEntry.className = 'py-1 border-b border-gray-200 last:border-b-0 font-bold text-gray-800';
131
+
132
+ receiveChunkCounter++;
133
+ chunkCountDisplay.textContent = `Chunks received: ${receiveChunkCounter}`;
134
+ }
135
+ chunkLog.appendChild(logEntry);
136
+ chunkLog.scrollTop = chunkLog.scrollHeight;
137
+ }
138
+
139
+ // --- Web Audio Playback Logic ---
140
+ function playbackLoop() {
141
+ if (!isPlaying) return;
142
+
143
+ console.log('PlaybackLoop called, audioQueue length:', audioQueue.length, 'expected:', expectedChunkNumber);
144
+
145
+ // Sort queue by chunkNumber
146
+ audioQueue.sort((a, b) => a.chunkNumber - b.chunkNumber);
147
+
148
+ // Skip missing chunks
149
+ while (audioQueue.length > 0 && audioQueue[0].chunkNumber < expectedChunkNumber) {
150
+ audioQueue.shift();
151
+ }
152
+
153
+ let played = false;
154
+ while (audioQueue.length > 0 && audioQueue[0].chunkNumber === expectedChunkNumber) {
155
+ console.log('Processing chunk #', audioQueue[0].chunkNumber);
156
+ played = true;
157
+ const chunkData = audioQueue.shift();
158
+ const { audioData, chunkNumber, loudness, receivedTime, id } = chunkData;
159
+
160
+ const playbackStartTime = performance.now();
161
+
162
+ try {
163
+ const binaryString = window.atob(audioData);
164
+ const len = binaryString.length;
165
+ const bytes = new Uint8Array(len);
166
+ for (let i = 0; i < len; i++) {
167
+ bytes[i] = binaryString.charCodeAt(i);
168
+ }
169
+ const int16Arr = new Int16Array(bytes.buffer);
170
+ const float32Arr = new Float32Array(int16Arr.length);
171
+ for (let i = 0; i < int16Arr.length; i++) {
172
+ float32Arr[i] = int16Arr[i] / 32768;
173
+ }
174
+ const audioBuffer = audioContext.createBuffer(
175
+ 1,
176
+ float32Arr.length,
177
+ audioContext.sampleRate
178
+ );
179
+ audioBuffer.copyToChannel(float32Arr, 0);
180
+
181
+ const playbackEndTime = performance.now();
182
+ const timeToPlay = playbackEndTime - playbackStartTime;
183
+
184
+ const timeToReceive = receivedTime - chunkData.workerPostTime;
185
+ appendChunkLog(chunkNumber, id, loudness, false, timeToReceive, timeToPlay);
186
+
187
+ const source = audioContext.createBufferSource();
188
+ source.buffer = audioBuffer;
189
+ source.connect(audioContext.destination);
190
+
191
+ const currentTime = audioContext.currentTime;
192
+ const delay = nextPlayTime - currentTime;
193
+ source.start(currentTime + Math.max(0, delay));
194
+
195
+ nextPlayTime = Math.max(nextPlayTime, currentTime) + audioBuffer.duration;
196
+ expectedChunkNumber++;
197
+
198
+ // Set onended only if no more to play immediately
199
+ if (audioQueue.length === 0 || audioQueue[0].chunkNumber !== expectedChunkNumber) {
200
+ source.onended = playbackLoop;
201
+ }
202
+ } catch (e) {
203
+ console.error(`Error processing audio data for chunk #${chunkNumber}:`, e);
204
+ }
205
+ }
206
+
207
+ if (!played) {
208
+ setTimeout(playbackLoop, 10);
209
+ }
210
+ }
211
+
212
+ // --- Main WebSocket and Audio Logic ---
213
+ function connectWebSockets() {
214
+ startButton.disabled = true;
215
+ stopButton.disabled = true;
216
+ setStatus('send', 'yellow', 'Connecting Send...');
217
+ setStatus('receive', 'yellow', 'Connecting Receive...');
218
+ showMessage('Attempting to connect to both servers...');
219
+
220
+ try {
221
+ // Connect the send websocket
222
+ sendWebSocket = new WebSocket(SEND_WEBSOCKET_URL);
223
+ sendWebSocket.onopen = () => {
224
+ console.log("Send WebSocket connection established.");
225
+ setStatus('send', 'green', 'Send Connected');
226
+ if (receiveWebSocket && receiveWebSocket.readyState === WebSocket.OPEN) {
227
+ startButton.disabled = false;
228
+ stopButton.disabled = true;
229
+ showMessage('Successfully connected to both servers. You can now start recording.');
230
+ }
231
+ };
232
+ sendWebSocket.onclose = () => {
233
+ console.log("Send WebSocket connection closed.");
234
+ setStatus('send', 'red', 'Send Disconnected');
235
+ startButton.disabled = true;
236
+ stopButton.disabled = true;
237
+ isRecording = false;
238
+ };
239
+ sendWebSocket.onerror = (error) => {
240
+ console.error("Send WebSocket error:", error);
241
+ setStatus('send', 'red', 'Send Error');
242
+ showMessage('Could not connect to the send server. Is the Python server running?', true);
243
+ };
244
+
245
+ // Connect the receive websocket
246
+ receiveWebSocket = new WebSocket(RECEIVE_WEBSOCKET_URL);
247
+ receiveWebSocket.onopen = () => {
248
+ console.log("Receive WebSocket connection established.");
249
+ setStatus('receive', 'green', 'Receive Connected');
250
+ if (sendWebSocket && sendWebSocket.readyState === WebSocket.OPEN) {
251
+ startButton.disabled = false;
252
+ stopButton.disabled = true;
253
+ showMessage('Successfully connected to both servers. You can now start recording.');
254
+ }
255
+ };
256
+ receiveWebSocket.onmessage = (event) => {
257
+ console.log('Receive WebSocket message received');
258
+ const workerPostTime = performance.now();
259
+ const chunkData = JSON.parse(event.data);
260
+ const receivedTime = performance.now();
261
+ chunkData.workerPostTime = workerPostTime;
262
+ chunkData.receivedTime = receivedTime;
263
+ audioQueue.push(chunkData);
264
+ playbackLoop();
265
+ };
266
+ receiveWebSocket.onclose = () => {
267
+ console.log("Receive WebSocket connection closed.");
268
+ setStatus('receive', 'red', 'Receive Disconnected');
269
+ startButton.disabled = true;
270
+ stopButton.disabled = true;
271
+ isRecording = false;
272
+ isPlaying = false;
273
+ audioQueue = [];
274
+ };
275
+ receiveWebSocket.onerror = (error) => {
276
+ console.error("Receive WebSocket error.");
277
+ setStatus('receive', 'red', 'Receive Error');
278
+ showMessage('Could not connect to the receive server. Is the Python server running?', true);
279
+ };
280
+
281
+ } catch (e) {
282
+ console.error("Failed to create WebSockets:", e);
283
+ setStatus('send', 'red', 'Send Failed');
284
+ setStatus('receive', 'red', 'Receive Failed');
285
+ showMessage('An error occurred. Check your server URLs.', true);
286
+ startButton.disabled = true;
287
+ stopButton.disabled = true;
288
+ }
289
+ }
290
+
291
+ async function startRecording() {
292
+ if (sendWebSocket && sendWebSocket.readyState === WebSocket.OPEN) {
293
+ try {
294
+ await audioContext.resume();
295
+
296
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
297
+ micSource = audioContext.createMediaStreamSource(stream);
298
+ scriptNode = audioContext.createScriptProcessor(4096, 1, 1);
299
+ gainNode = audioContext.createGain();
300
+ gainNode.gain.value = 0;
301
+
302
+ micSource.connect(scriptNode);
303
+ scriptNode.connect(gainNode);
304
+ gainNode.connect(audioContext.destination);
305
+
306
+ chunkSamples = Math.round(audioContext.sampleRate * CHUNK_DURATION);
307
+ accumulatedAudio = [];
308
+ scriptNode.onaudioprocess = (e) => {
309
+ if (!isRecording) return;
310
+ const data = e.inputBuffer.getChannelData(0);
311
+ for (let i = 0; i < data.length; i++) {
312
+ accumulatedAudio.push(data[i]);
313
+ }
314
+ while (accumulatedAudio.length >= chunkSamples) {
315
+ const chunk = accumulatedAudio.splice(0, chunkSamples);
316
+ const int16Arr = new Int16Array(chunk.length);
317
+ for (let i = 0; i < chunk.length; i++) {
318
+ int16Arr[i] = Math.max(-32768, Math.min(32767, chunk[i] * 32767));
319
+ }
320
+ const bytes = new Uint8Array(int16Arr.buffer);
321
+ let binary = '';
322
+ for (let i = 0; i < bytes.byteLength; i++) {
323
+ binary += String.fromCharCode(bytes[i]);
324
+ }
325
+ const base64Data = btoa(binary);
326
+
327
+ sendChunkCounter++;
328
+ appendChunkLog(sendChunkCounter, null, null, true);
329
+
330
+ const chunkToSend = {
331
+ chunkNumber: sendChunkCounter,
332
+ audioData: base64Data
333
+ };
334
+ if (sendWebSocket.readyState === WebSocket.OPEN) {
335
+ sendWebSocket.send(JSON.stringify(chunkToSend));
336
+ }
337
+ }
338
+ };
339
+
340
+ console.log("Recording started.");
341
+ startButton.disabled = true;
342
+ stopButton.disabled = false;
343
+ isRecording = true;
344
+ showMessage("Recording... Please speak into your microphone.");
345
+
346
+ // Start the playback loop
347
+ if (!isPlaying) {
348
+ isPlaying = true;
349
+ nextPlayTime = audioContext.currentTime;
350
+ playbackLoop();
351
+ }
352
+
353
+ } catch (err) {
354
+ console.error("Failed to start recording:", err);
355
+ showMessage('Failed to start recording. Check console for errors.', true);
356
+ startButton.disabled = false;
357
+ }
358
+ } else {
359
+ showMessage('Not connected to the send server. Please wait or refresh.', true);
360
+ }
361
+ }
362
+
363
+ function stopRecording() {
364
+ if (isRecording) {
365
+ isRecording = false;
366
+ isPlaying = false;
367
+ if (micSource) micSource.disconnect();
368
+ if (scriptNode) scriptNode.disconnect();
369
+ if (gainNode) gainNode.disconnect();
370
+ const tracks = micSource?.mediaStream.getTracks();
371
+ tracks?.forEach(track => track.stop());
372
+ accumulatedAudio = [];
373
+ console.log("Recording stopped.");
374
+ startButton.disabled = false;
375
+ stopButton.disabled = true;
376
+ }
377
+ }
378
+
379
+ startButton.addEventListener('click', startRecording);
380
+ stopButton.addEventListener('click', stopRecording);
381
+
382
+ document.addEventListener('DOMContentLoaded', () => {
383
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
384
+ connectWebSockets();
385
+ });
386
+ </script>
387
+ </body>
388
+ </html>