update
Browse filesfixing garbled sound
- src/app/components/Main.tsx +26 -6
- src/audio/AudioPlayer.ts +27 -1
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(
|
| 429 |
const totalBytes = chunks.reduce((acc, c) => acc + c.byteLength, 0);
|
| 430 |
const durationMs = (totalBytes / 2 / sampleRate) * 1000;
|
| 431 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|