ishaq101 commited on
Commit
a5b2d49
·
1 Parent(s): 961e31b

fixing garbled sound

src/app/components/Main.tsx CHANGED
@@ -412,23 +412,43 @@ export default function Main() {
412
  player.init(sampleRate);
413
  const reader = stream.getReader();
414
  let startedNotified = false;
 
 
 
 
 
 
 
 
 
 
415
  while (true) {
416
  const { done, value } = await reader.read();
417
  if (done) break;
418
  if (value && value.byteLength > 0) {
419
  const pcm = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength) as ArrayBuffer;
420
  chunks.push(pcm);
421
- player.enqueue(
422
- pcm,
423
- !startedNotified ? () => { startedNotified = true; onStarted?.(); } : undefined,
424
- );
425
  }
426
  }
427
  // Play any remaining buffered audio that didn't reach the threshold (short responses)
428
- player.flush(!startedNotified ? () => { startedNotified = true; onStarted?.(); } : undefined);
429
  const totalBytes = chunks.reduce((acc, c) => acc + c.byteLength, 0);
430
  const durationMs = (totalBytes / 2 / sampleRate) * 1000;
431
- await new Promise<void>((resolve) => setTimeout(resolve, durationMs + 300));
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  player.stopImmediately();
433
  return { chunks, sampleRate };
434
  } catch {
 
412
  player.init(sampleRate);
413
  const reader = stream.getReader();
414
  let startedNotified = false;
415
+ // Track the exact moment the AudioContext actually starts playing so the
416
+ // stop timer is anchored to playback start, not stream-end.
417
+ let audioStartedAt: number | null = null;
418
+ const notifyStarted = () => {
419
+ if (!startedNotified) {
420
+ startedNotified = true;
421
+ audioStartedAt = Date.now();
422
+ onStarted?.();
423
+ }
424
+ };
425
  while (true) {
426
  const { done, value } = await reader.read();
427
  if (done) break;
428
  if (value && value.byteLength > 0) {
429
  const pcm = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength) as ArrayBuffer;
430
  chunks.push(pcm);
431
+ player.enqueue(pcm, notifyStarted);
 
 
 
432
  }
433
  }
434
  // Play any remaining buffered audio that didn't reach the threshold (short responses)
435
+ player.flush(notifyStarted);
436
  const totalBytes = chunks.reduce((acc, c) => acc + c.byteLength, 0);
437
  const durationMs = (totalBytes / 2 / sampleRate) * 1000;
438
+ // Wait until audio actually starts, then wait for the full playback duration.
439
+ // Polling in 10ms steps avoids a fixed large buffer while staying responsive.
440
+ await new Promise<void>((resolve) => {
441
+ const check = () => {
442
+ if (audioStartedAt !== null) {
443
+ const elapsed = Date.now() - audioStartedAt;
444
+ const remaining = Math.max(0, durationMs - elapsed) + 150;
445
+ setTimeout(resolve, remaining);
446
+ } else {
447
+ setTimeout(check, 10);
448
+ }
449
+ };
450
+ check();
451
+ });
452
  player.stopImmediately();
453
  return { chunks, sampleRate };
454
  } catch {
src/audio/AudioPlayer.ts CHANGED
@@ -8,6 +8,9 @@ export class AudioPlayer {
8
  private pendingBytes = 0;
9
  private bufferThresholdBytes = 6400; // 200ms at 16kHz default
10
  private pendingBuffers: AudioBuffer[] = [];
 
 
 
11
 
12
  init(sampleRate = DEFAULT_SAMPLE_RATE): void {
13
  if (this.context) {
@@ -22,12 +25,34 @@ export class AudioPlayer {
22
  this.resumed = false;
23
  this.pendingBytes = 0;
24
  this.pendingBuffers = [];
 
25
  }
26
 
27
  enqueue(rawPcm: ArrayBuffer, onStarted?: () => void): void {
28
  if (!this.context) return;
29
 
30
- const int16 = new Int16Array(rawPcm, 0, Math.floor(rawPcm.byteLength / 2));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  const float32 = new Float32Array(int16.length);
32
  for (let i = 0; i < int16.length; i++) {
33
  float32[i] = int16[i] / 32768;
@@ -105,6 +130,7 @@ export class AudioPlayer {
105
  this.resumed = false;
106
  this.pendingBytes = 0;
107
  this.pendingBuffers = [];
 
108
  }
109
  }
110
 
 
8
  private pendingBytes = 0;
9
  private bufferThresholdBytes = 6400; // 200ms at 16kHz default
10
  private pendingBuffers: AudioBuffer[] = [];
11
+ // Carries the leftover byte when a chunk has odd byte length, so PCM sample
12
+ // boundaries stay aligned across chunk splits from the HTTP stream.
13
+ private leftoverByte: number | null = null;
14
 
15
  init(sampleRate = DEFAULT_SAMPLE_RATE): void {
16
  if (this.context) {
 
25
  this.resumed = false;
26
  this.pendingBytes = 0;
27
  this.pendingBuffers = [];
28
+ this.leftoverByte = null;
29
  }
30
 
31
  enqueue(rawPcm: ArrayBuffer, onStarted?: () => void): void {
32
  if (!this.context) return;
33
 
34
+ // Prepend any leftover byte from the previous chunk so that Int16 sample
35
+ // boundaries are always aligned, regardless of how the HTTP stream splits.
36
+ let pcmBytes: Uint8Array;
37
+ if (this.leftoverByte !== null) {
38
+ const combined = new Uint8Array(1 + rawPcm.byteLength);
39
+ combined[0] = this.leftoverByte;
40
+ combined.set(new Uint8Array(rawPcm), 1);
41
+ pcmBytes = combined;
42
+ this.leftoverByte = null;
43
+ } else {
44
+ pcmBytes = new Uint8Array(rawPcm);
45
+ }
46
+
47
+ // If still odd, save the trailing byte for the next chunk.
48
+ if (pcmBytes.byteLength % 2 !== 0) {
49
+ this.leftoverByte = pcmBytes[pcmBytes.byteLength - 1];
50
+ pcmBytes = pcmBytes.slice(0, pcmBytes.byteLength - 1);
51
+ }
52
+
53
+ if (pcmBytes.byteLength === 0) return;
54
+
55
+ const int16 = new Int16Array(pcmBytes.buffer, pcmBytes.byteOffset, pcmBytes.byteLength / 2);
56
  const float32 = new Float32Array(int16.length);
57
  for (let i = 0; i < int16.length; i++) {
58
  float32[i] = int16[i] / 32768;
 
130
  this.resumed = false;
131
  this.pendingBytes = 0;
132
  this.pendingBuffers = [];
133
+ this.leftoverByte = null;
134
  }
135
  }
136