Spaces:
Sleeping
Sleeping
Fix garbled audio
Browse files1. Buffer 200ms → 500ms — lebih banyak data di-buffer sebelum playback dimulai, mengurangi dampak network jitter dan memberikan Web Audio API lebih banyak headroom untuk render audio tanpa glitch
2. flush() setelah stream selesai — fix bug di mana audio pendek (total < 500ms) tidak pernah diputar karena threshold tidak pernah tercapai; flush() memaksa semua pending buffers di-schedule setelah stream berakhir
- src/app/components/Main.tsx +2 -0
- src/audio/AudioPlayer.ts +23 -1
src/app/components/Main.tsx
CHANGED
|
@@ -414,6 +414,8 @@ export default function Main() {
|
|
| 414 |
);
|
| 415 |
}
|
| 416 |
}
|
|
|
|
|
|
|
| 417 |
const totalBytes = chunks.reduce((acc, c) => acc + c.byteLength, 0);
|
| 418 |
const durationMs = (totalBytes / 2 / sampleRate) * 1000;
|
| 419 |
await new Promise<void>((resolve) => setTimeout(resolve, durationMs + 300));
|
|
|
|
| 414 |
);
|
| 415 |
}
|
| 416 |
}
|
| 417 |
+
// Play any remaining buffered audio that didn't reach the threshold (short responses)
|
| 418 |
+
player.flush(!startedNotified ? () => { startedNotified = true; onStarted?.(); } : undefined);
|
| 419 |
const totalBytes = chunks.reduce((acc, c) => acc + c.byteLength, 0);
|
| 420 |
const durationMs = (totalBytes / 2 / sampleRate) * 1000;
|
| 421 |
await new Promise<void>((resolve) => setTimeout(resolve, durationMs + 300));
|
src/audio/AudioPlayer.ts
CHANGED
|
@@ -16,7 +16,7 @@ export class AudioPlayer {
|
|
| 16 |
this.context = null;
|
| 17 |
}
|
| 18 |
this.context = new AudioContext({ sampleRate });
|
| 19 |
-
this.bufferThresholdBytes = Math.floor(sampleRate * 0.
|
| 20 |
this.nextPlayTime = 0;
|
| 21 |
this.started = false;
|
| 22 |
this.resumed = false;
|
|
@@ -70,6 +70,28 @@ export class AudioPlayer {
|
|
| 70 |
}
|
| 71 |
}
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
drain(): void {
|
| 74 |
// Let queued buffers play out — nothing to do, the AudioContext schedule handles it
|
| 75 |
}
|
|
|
|
| 16 |
this.context = null;
|
| 17 |
}
|
| 18 |
this.context = new AudioContext({ sampleRate });
|
| 19 |
+
this.bufferThresholdBytes = Math.floor(sampleRate * 0.5) * 2; // 500ms
|
| 20 |
this.nextPlayTime = 0;
|
| 21 |
this.started = false;
|
| 22 |
this.resumed = false;
|
|
|
|
| 70 |
}
|
| 71 |
}
|
| 72 |
|
| 73 |
+
// Call after the stream ends to play any buffered audio that hasn't started yet
|
| 74 |
+
// (handles responses shorter than the buffer threshold)
|
| 75 |
+
flush(onStarted?: () => void): void {
|
| 76 |
+
if (!this.context || this.started || this.pendingBuffers.length === 0) return;
|
| 77 |
+
this.started = true;
|
| 78 |
+
void this.context.resume().then(() => {
|
| 79 |
+
if (!this.context) return;
|
| 80 |
+
this.resumed = true;
|
| 81 |
+
this.nextPlayTime = this.context.currentTime;
|
| 82 |
+
onStarted?.();
|
| 83 |
+
const toSchedule = this.pendingBuffers.splice(0);
|
| 84 |
+
this.pendingBuffers = [];
|
| 85 |
+
for (const buf of toSchedule) {
|
| 86 |
+
const source = this.context.createBufferSource();
|
| 87 |
+
source.buffer = buf;
|
| 88 |
+
source.connect(this.context.destination);
|
| 89 |
+
source.start(this.nextPlayTime);
|
| 90 |
+
this.nextPlayTime += buf.duration;
|
| 91 |
+
}
|
| 92 |
+
});
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
drain(): void {
|
| 96 |
// Let queued buffers play out — nothing to do, the AudioContext schedule handles it
|
| 97 |
}
|