yasserrmd commited on
Commit
ca9b479
·
verified ·
1 Parent(s): 39d90db

Create conv.html

Browse files
Files changed (1) hide show
  1. static/conv.html +686 -0
static/conv.html ADDED
@@ -0,0 +1,686 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Sound Chat Interface</title>
7
+ <!-- Bootstrap CSS -->
8
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
9
+ <!-- Bootstrap Icons -->
10
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
11
+ <style>
12
+ :root {
13
+ --primary: #5046e5;
14
+ --primary-dark: #3730a3;
15
+ --accent: #06b6d4;
16
+ --accent-light: #22d3ee;
17
+ --bg-color: #f8fafc;
18
+ --card-bg: #ffffff;
19
+ --dark-text: #0f172a;
20
+ --light-text: #f8fafc;
21
+ --message-bg-user: #7c3aed;
22
+ --message-bg-assistant: #e2e8f0;
23
+ --border-radius: 16px;
24
+ }
25
+
26
+ body {
27
+ background-color: var(--bg-color);
28
+ font-family: 'Inter', 'Segoe UI', sans-serif;
29
+ color: var(--dark-text);
30
+ min-height: 100vh;
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: center;
34
+ }
35
+
36
+ .app-container {
37
+ max-width: 900px;
38
+ margin: 20px auto;
39
+ background: var(--card-bg);
40
+ border-radius: var(--border-radius);
41
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05), 0 0 1px rgba(0, 0, 0, 0.1);
42
+ overflow: hidden;
43
+ display: grid;
44
+ grid-template-rows: auto 1fr auto;
45
+ height: 85vh;
46
+ width: 100%;
47
+ }
48
+
49
+ .app-header {
50
+ padding: 24px;
51
+ background: linear-gradient(135deg, var(--primary), var(--primary-dark));
52
+ color: var(--light-text);
53
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 15px;
57
+ }
58
+
59
+ .app-header h1 {
60
+ font-size: 1.5rem;
61
+ font-weight: 600;
62
+ margin: 0;
63
+ }
64
+
65
+ .app-title-icon {
66
+ font-size: 1.8rem;
67
+ display: flex;
68
+ align-items: center;
69
+ animation: pulse 2s infinite alternate;
70
+ }
71
+
72
+ .conversation-area {
73
+ padding: 20px;
74
+ overflow-y: auto;
75
+ display: flex;
76
+ flex-direction: column;
77
+ gap: 16px;
78
+ background-color: var(--bg-color);
79
+ height: 100%;
80
+ }
81
+
82
+ .message-group {
83
+ display: flex;
84
+ flex-direction: column;
85
+ gap: 8px;
86
+ max-width: 85%;
87
+ }
88
+
89
+ .message-group.user {
90
+ align-self: flex-end;
91
+ }
92
+
93
+ .message {
94
+ padding: 16px;
95
+ border-radius: 18px;
96
+ position: relative;
97
+ line-height: 1.5;
98
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
99
+ animation: fadeIn 0.3s ease-out;
100
+ }
101
+
102
+ .message.user {
103
+ background-color: var(--message-bg-user);
104
+ color: var(--light-text);
105
+ border-top-right-radius: 4px;
106
+ }
107
+
108
+ .message.assistant {
109
+ background-color: var(--message-bg-assistant);
110
+ color: var(--dark-text);
111
+ border-top-left-radius: 4px;
112
+ }
113
+
114
+ .message-avatar {
115
+ width: 32px;
116
+ height: 32px;
117
+ border-radius: 50%;
118
+ display: flex;
119
+ align-items: center;
120
+ justify-content: center;
121
+ font-size: 14px;
122
+ margin-bottom: 6px;
123
+ }
124
+
125
+ .user .message-avatar {
126
+ background-color: var(--primary-dark);
127
+ color: var(--light-text);
128
+ align-self: flex-end;
129
+ }
130
+
131
+ .assistant .message-avatar {
132
+ background-color: var(--accent);
133
+ color: var(--light-text);
134
+ }
135
+
136
+ .controls-area {
137
+ padding: 24px;
138
+ background-color: white;
139
+ border-top: 1px solid rgba(0, 0, 0, 0.05);
140
+ display: flex;
141
+ justify-content: center;
142
+ align-items: center;
143
+ }
144
+
145
+ .listen-container {
146
+ display: flex;
147
+ flex-direction: column;
148
+ align-items: center;
149
+ }
150
+
151
+ .listen-ball {
152
+ width: 100px;
153
+ height: 100px;
154
+ border-radius: 50%;
155
+ background: linear-gradient(135deg, var(--primary), var(--primary-dark));
156
+ color: white;
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: center;
160
+ cursor: pointer;
161
+ transition: all 0.3s ease;
162
+ box-shadow: 0 4px 16px rgba(80, 70, 229, 0.4);
163
+ position: relative;
164
+ overflow: hidden;
165
+ }
166
+
167
+ .listen-ball.listening {
168
+ background: linear-gradient(135deg, var(--accent), var(--accent-light));
169
+ animation: pulse 1.5s infinite;
170
+ }
171
+
172
+ .listen-ball.processing {
173
+ background: linear-gradient(135deg, #8b5cf6, #7c3aed);
174
+ animation: none;
175
+ }
176
+
177
+ .listen-ball i {
178
+ font-size: 2.5rem;
179
+ }
180
+
181
+ .sound-wave {
182
+ position: absolute;
183
+ top: 0;
184
+ left: 0;
185
+ width: 100%;
186
+ height: 100%;
187
+ border-radius: 50%;
188
+ opacity: 0;
189
+ }
190
+
191
+ .listening .sound-wave {
192
+ border: 2px solid rgba(255, 255, 255, 0.5);
193
+ animation: wave 2s infinite;
194
+ }
195
+
196
+ .status-badge {
197
+ background-color: rgba(0, 0, 0, 0.04);
198
+ border-radius: 50px;
199
+ padding: 8px 16px;
200
+ font-size: 0.9rem;
201
+ color: var(--dark-text);
202
+ margin-top: 16px;
203
+ display: inline-flex;
204
+ align-items: center;
205
+ gap: 8px;
206
+ font-weight: 500;
207
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
208
+ }
209
+
210
+ .status-badge i {
211
+ font-size: 1rem;
212
+ }
213
+
214
+ .audio-controls {
215
+ display: none;
216
+ }
217
+
218
+ .empty-state {
219
+ display: flex;
220
+ flex-direction: column;
221
+ align-items: center;
222
+ justify-content: center;
223
+ height: 100%;
224
+ color: #94a3b8;
225
+ text-align: center;
226
+ padding: 20px;
227
+ }
228
+
229
+ .empty-state i {
230
+ font-size: 3.5rem;
231
+ margin-bottom: 20px;
232
+ color: #cbd5e1;
233
+ }
234
+
235
+ .empty-state h3 {
236
+ font-size: 1.3rem;
237
+ margin-bottom: 10px;
238
+ color: #64748b;
239
+ }
240
+
241
+ .time-stamp {
242
+ font-size: 0.75rem;
243
+ margin-top: 4px;
244
+ opacity: 0.7;
245
+ align-self: flex-end;
246
+ }
247
+
248
+ @keyframes pulse {
249
+ 0% { transform: scale(1); }
250
+ 50% { transform: scale(1.05); }
251
+ 100% { transform: scale(1); }
252
+ }
253
+
254
+ @keyframes wave {
255
+ 0% { transform: scale(1); opacity: 0.7; }
256
+ 100% { transform: scale(1.5); opacity: 0; }
257
+ }
258
+
259
+ @keyframes fadeIn {
260
+ from { opacity: 0; transform: translateY(10px); }
261
+ to { opacity: 1; transform: translateY(0); }
262
+ }
263
+
264
+ @media (max-width: 768px) {
265
+ .app-container {
266
+ margin: 0;
267
+ height: 100vh;
268
+ border-radius: 0;
269
+ }
270
+
271
+ .message-group {
272
+ max-width: 90%;
273
+ }
274
+
275
+ .listen-ball {
276
+ width: 80px;
277
+ height: 80px;
278
+ }
279
+
280
+ .listen-ball i {
281
+ font-size: 2rem;
282
+ }
283
+ }
284
+ </style>
285
+ </head>
286
+ <body>
287
+ <div class="container-fluid p-0">
288
+ <div class="app-container">
289
+ <div class="app-header">
290
+ <div class="app-title-icon">
291
+ <i class="bi bi-soundwave"></i>
292
+ </div>
293
+ <h1>Sound Chat</h1>
294
+ </div>
295
+
296
+ <div class="conversation-area" id="conversationArea">
297
+ <div class="empty-state" id="emptyState">
298
+ <i class="bi bi-ear"></i>
299
+ <h3>No messages yet</h3>
300
+ <p>Tap the sound button below to start listening and chatting.</p>
301
+ </div>
302
+ <!-- Messages will be added here dynamically -->
303
+ </div>
304
+
305
+ <div class="controls-area">
306
+ <div class="listen-container">
307
+ <div class="listen-ball" id="listenBall">
308
+ <div class="sound-wave"></div>
309
+ <div class="sound-wave" style="animation-delay: 0.5s"></div>
310
+ <div class="sound-wave" style="animation-delay: 1s"></div>
311
+ <i class="bi bi-soundwave"></i>
312
+ </div>
313
+ <div class="status-badge" id="statusBadge">
314
+ <i class="bi bi-info-circle"></i>
315
+ <span id="statusMessage">Tap to listen for sound</span>
316
+ </div>
317
+ </div>
318
+
319
+ <div class="audio-controls">
320
+ <audio id="audioPlayer" controls></audio>
321
+ </div>
322
+ </div>
323
+ </div>
324
+ </div>
325
+
326
+ <!-- Bootstrap JS Bundle -->
327
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
328
+ <script>
329
+ // DOM Elements
330
+ const listenBall = document.getElementById("listenBall");
331
+ const statusMessage = document.getElementById("statusMessage");
332
+ const audioPlayer = document.getElementById("audioPlayer");
333
+ const conversationArea = document.getElementById("conversationArea");
334
+ const emptyState = document.getElementById("emptyState");
335
+ const statusBadge = document.getElementById("statusBadge");
336
+
337
+ // Global variables
338
+ let mediaRecorder;
339
+ let audioChunks = [];
340
+ let audioStream;
341
+ let chatHistory = [];
342
+ let isListening = false;
343
+ let isAutoListening = false;
344
+ let silenceDetectionInterval;
345
+ let lastAudioLevel = 0;
346
+ let currentUserGroup = null;
347
+ let currentAssistantGroup = null;
348
+
349
+ // Functions
350
+ function updateStatus(message, icon = "bi-info-circle") {
351
+ statusMessage.textContent = message;
352
+ statusBadge.querySelector("i").className = `bi ${icon}`;
353
+ }
354
+
355
+ function addMessageToChat(content, sender) {
356
+ // Hide empty state if it's visible
357
+ if (!emptyState.classList.contains("d-none")) {
358
+ emptyState.classList.add("d-none");
359
+ }
360
+
361
+ const now = new Date();
362
+ const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
363
+
364
+ // Create new message group or use existing one based on sender
365
+ let messageGroup;
366
+
367
+ if (sender === 'user') {
368
+ if (!currentUserGroup || (currentAssistantGroup && currentAssistantGroup.classList.contains("assistant"))) {
369
+ currentUserGroup = document.createElement("div");
370
+ currentUserGroup.className = "message-group user";
371
+
372
+ const avatar = document.createElement("div");
373
+ avatar.className = "message-avatar";
374
+ avatar.innerHTML = "<i class='bi bi-person'></i>";
375
+ currentUserGroup.appendChild(avatar);
376
+
377
+ conversationArea.appendChild(currentUserGroup);
378
+ }
379
+ messageGroup = currentUserGroup;
380
+ currentAssistantGroup = null;
381
+ } else {
382
+ if (!currentAssistantGroup || (currentUserGroup && currentUserGroup.classList.contains("user"))) {
383
+ currentAssistantGroup = document.createElement("div");
384
+ currentAssistantGroup.className = "message-group assistant";
385
+
386
+ const avatar = document.createElement("div");
387
+ avatar.className = "message-avatar";
388
+ avatar.innerHTML = "<i class='bi bi-robot'></i>";
389
+ currentAssistantGroup.appendChild(avatar);
390
+
391
+ conversationArea.appendChild(currentAssistantGroup);
392
+ }
393
+ messageGroup = currentAssistantGroup;
394
+ currentUserGroup = null;
395
+ }
396
+
397
+ // Create message element
398
+ const messageDiv = document.createElement("div");
399
+ messageDiv.className = `message ${sender}`;
400
+ messageDiv.textContent = content;
401
+
402
+ const timestamp = document.createElement("div");
403
+ timestamp.className = "time-stamp";
404
+ timestamp.textContent = timeString;
405
+
406
+ messageGroup.appendChild(messageDiv);
407
+ messageGroup.appendChild(timestamp);
408
+
409
+ // Scroll to bottom
410
+ conversationArea.scrollTop = conversationArea.scrollHeight;
411
+
412
+ // Add to chat history
413
+ chatHistory.push({
414
+ role: sender === 'user' ? 'user' : 'assistant',
415
+ content: content
416
+ });
417
+ }
418
+
419
+ function startAnalyzingAudioLevels() {
420
+ if (!audioStream) return;
421
+
422
+ const audioContext = new AudioContext();
423
+ const source = audioContext.createMediaStreamSource(audioStream);
424
+ const analyzer = audioContext.createAnalyser();
425
+ analyzer.fftSize = 256;
426
+ source.connect(analyzer);
427
+
428
+ const bufferLength = analyzer.frequencyBinCount;
429
+ const dataArray = new Uint8Array(bufferLength);
430
+
431
+ let silenceCounter = 0;
432
+ const SILENCE_THRESHOLD = 10;
433
+ const MAX_SILENCE_COUNT = 20; // About 2 seconds of silence
434
+
435
+ clearInterval(silenceDetectionInterval);
436
+ silenceDetectionInterval = setInterval(() => {
437
+ analyzer.getByteFrequencyData(dataArray);
438
+
439
+ // Calculate average audio level
440
+ let sum = 0;
441
+ for (let i = 0; i < bufferLength; i++) {
442
+ sum += dataArray[i];
443
+ }
444
+ const avg = sum / bufferLength;
445
+ lastAudioLevel = avg;
446
+
447
+ // Detect significant sound and silence
448
+ if (avg > SILENCE_THRESHOLD) {
449
+ silenceCounter = 0;
450
+ if (!isListening && isAutoListening) {
451
+ startListening();
452
+ }
453
+ } else {
454
+ silenceCounter++;
455
+ if (silenceCounter >= MAX_SILENCE_COUNT && isListening) {
456
+ stopListening();
457
+ }
458
+ }
459
+ }, 100);
460
+ }
461
+
462
+ async function startListening() {
463
+ if (isListening) return;
464
+
465
+ try {
466
+ // Stop any existing stream tracks
467
+ if (audioStream) {
468
+ audioStream.getTracks().forEach(track => track.stop());
469
+ }
470
+
471
+ audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
472
+ mediaRecorder = new MediaRecorder(audioStream, { mimeType: "audio/webm" });
473
+
474
+ mediaRecorder.ondataavailable = event => audioChunks.push(event.data);
475
+
476
+ mediaRecorder.onstop = async () => {
477
+ if (audioChunks.length === 0) {
478
+ updateState("idle");
479
+ return;
480
+ }
481
+
482
+ updateState("processing");
483
+
484
+ try {
485
+ const audioBlob = new Blob(audioChunks, { type: "audio/webm" });
486
+ const wavBlob = await convertWebMToWav(audioBlob);
487
+
488
+ // Create form data with the audio and chat history
489
+ const formData = new FormData();
490
+ formData.append("file", wavBlob, "recording.wav");
491
+ formData.append("chat_history", JSON.stringify(chatHistory));
492
+
493
+ // Send to the continuous-chat endpoint using root-relative path
494
+ const response = await fetch("/continuous-chat/", {
495
+ method: "POST",
496
+ body: formData
497
+ });
498
+
499
+ if (response.ok) {
500
+ const userMessage = response.headers.get("X-User-Message") || "No user message";
501
+ const llmResponse = response.headers.get("X-LLM-Response") || "No response";
502
+
503
+ // Add messages to chat
504
+ addMessageToChat(userMessage, 'user');
505
+ addMessageToChat(llmResponse, 'assistant');
506
+
507
+ // Get audio response and play it
508
+ const audioData = await response.blob();
509
+ audioPlayer.src = URL.createObjectURL(audioData);
510
+ audioPlayer.play();
511
+
512
+ updateState("idle");
513
+ updateStatus("Ready for next sound input", "bi-broadcast");
514
+
515
+ // If in auto mode, start listening again
516
+ if (isAutoListening) {
517
+ setTimeout(() => {
518
+ startAnalyzingAudioLevels();
519
+ }, 1000);
520
+ }
521
+ } else {
522
+ updateState("idle");
523
+ updateStatus("Error processing audio", "bi-exclamation-triangle");
524
+ }
525
+ } catch (error) {
526
+ console.error("Error:", error);
527
+ updateState("idle");
528
+ updateStatus("Error processing audio", "bi-exclamation-triangle");
529
+ }
530
+ };
531
+
532
+ audioChunks = [];
533
+ mediaRecorder.start();
534
+
535
+ updateState("listening");
536
+ updateStatus("Listening...", "bi-ear");
537
+
538
+ // Set max recording duration (8 seconds)
539
+ setTimeout(() => {
540
+ if (mediaRecorder && mediaRecorder.state === "recording") {
541
+ stopListening();
542
+ }
543
+ }, 8000);
544
+
545
+ isListening = true;
546
+ } catch (error) {
547
+ console.error("Error accessing microphone:", error);
548
+ updateStatus("Microphone access denied", "bi-mic-mute");
549
+ updateState("idle");
550
+ }
551
+ }
552
+
553
+ function stopListening() {
554
+ if (!isListening) return;
555
+
556
+ if (mediaRecorder && mediaRecorder.state === "recording") {
557
+ mediaRecorder.stop();
558
+ }
559
+
560
+ isListening = false;
561
+ }
562
+
563
+ function updateState(state) {
564
+ listenBall.classList.remove("listening", "processing");
565
+
566
+ if (state === "listening") {
567
+ listenBall.classList.add("listening");
568
+ listenBall.innerHTML = `
569
+ <div class="sound-wave"></div>
570
+ <div class="sound-wave" style="animation-delay: 0.5s"></div>
571
+ <div class="sound-wave" style="animation-delay: 1s"></div>
572
+ <i class="bi bi-soundwave"></i>
573
+ `;
574
+ } else if (state === "processing") {
575
+ listenBall.classList.add("processing");
576
+ listenBall.innerHTML = `<i class="bi bi-arrow-repeat"></i>`;
577
+ } else {
578
+ listenBall.innerHTML = `<i class="bi bi-soundwave"></i>`;
579
+ }
580
+ }
581
+
582
+ function toggleContinuousListening() {
583
+ isAutoListening = !isAutoListening;
584
+
585
+ if (isAutoListening) {
586
+ updateStatus("Auto-listening mode active", "bi-broadcast");
587
+ startAnalyzingAudioLevels();
588
+ } else {
589
+ updateStatus("Tap to listen", "bi-info-circle");
590
+ clearInterval(silenceDetectionInterval);
591
+ }
592
+ }
593
+
594
+ async function convertWebMToWav(blob) {
595
+ return new Promise((resolve, reject) => {
596
+ try {
597
+ const reader = new FileReader();
598
+ reader.onload = function () {
599
+ const audioContext = new AudioContext();
600
+ audioContext.decodeAudioData(reader.result)
601
+ .then(buffer => {
602
+ const wavBuffer = audioBufferToWav(buffer);
603
+ resolve(new Blob([wavBuffer], { type: "audio/wav" }));
604
+ })
605
+ .catch(error => {
606
+ console.error("Error decoding audio data:", error);
607
+ reject(error);
608
+ });
609
+ };
610
+ reader.readAsArrayBuffer(blob);
611
+ } catch (error) {
612
+ console.error("Error in convertWebMToWav:", error);
613
+ reject(error);
614
+ }
615
+ });
616
+ }
617
+
618
+ function audioBufferToWav(buffer) {
619
+ let numOfChan = buffer.numberOfChannels,
620
+ length = buffer.length * numOfChan * 2 + 44,
621
+ bufferArray = new ArrayBuffer(length),
622
+ view = new DataView(bufferArray),
623
+ channels = [],
624
+ sampleRate = buffer.sampleRate,
625
+ offset = 0,
626
+ pos = 0;
627
+ setUint32(0x46464952); // "RIFF"
628
+ setUint32(length - 8);
629
+ setUint32(0x45564157); // "WAVE"
630
+ setUint32(0x20746d66); // "fmt " chunk
631
+ setUint32(16); // length = 16
632
+ setUint16(1); // PCM (uncompressed)
633
+ setUint16(numOfChan);
634
+ setUint32(sampleRate);
635
+ setUint32(sampleRate * 2 * numOfChan);
636
+ setUint16(numOfChan * 2);
637
+ setUint16(16); // bits per sample
638
+ setUint32(0x61746164); // "data" chunk
639
+ setUint32(length - pos - 4);
640
+ for (let i = 0; i < buffer.numberOfChannels; i++)
641
+ channels.push(buffer.getChannelData(i));
642
+ while (pos < length) {
643
+ for (let i = 0; i < numOfChan; i++) {
644
+ let sample = Math.max(-1, Math.min(1, channels[i][offset]));
645
+ sample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
646
+ setUint16(sample);
647
+ }
648
+ offset++;
649
+ }
650
+ function setUint16(data) {
651
+ view.setUint16(pos, data, true);
652
+ pos += 2;
653
+ }
654
+ function setUint32(data) {
655
+ view.setUint32(pos, data, true);
656
+ pos += 4;
657
+ }
658
+ return bufferArray;
659
+ }
660
+
661
+ // Event Listeners
662
+ listenBall.addEventListener("click", () => {
663
+ if (isListening) {
664
+ stopListening();
665
+ } else if (isAutoListening) {
666
+ toggleContinuousListening();
667
+ } else {
668
+ startListening();
669
+ }
670
+ });
671
+
672
+ listenBall.addEventListener("dblclick", toggleContinuousListening);
673
+
674
+ audioPlayer.addEventListener("ended", () => {
675
+ if (isAutoListening) {
676
+ setTimeout(() => {
677
+ startAnalyzingAudioLevels();
678
+ }, 500);
679
+ }
680
+ });
681
+
682
+ // Initialize
683
+ updateStatus("Tap to listen", "bi-info-circle");
684
+ </script>
685
+ </body>
686
+ </html>