diff --git a/examples/client_browser.html b/examples/client_browser.html
index 508e31c..e24cb4e 100644
--- a/examples/client_browser.html
+++ b/examples/client_browser.html
@@ -115,7 +115,7 @@
log(`Playing test tone at ${freq} Hz for ${duration}s`);
};
- const playPcm16 = async (base64Data) => {
+ const playPcm16 = async (base64Data, seq) => {
await ensureAudioContext();
if (audioCtx.state !== 'running') {
throw new Error(`AudioContext not running (state=${audioCtx.state})`);
@@ -124,7 +124,11 @@
if (raw.length === 0) {
throw new Error('Empty audio data');
}
- const samples = new Int16Array(raw.length / 2);
+ if (raw.length % 2 !== 0) {
+ throw new Error(`Odd raw audio length: ${raw.length}`);
+ }
+ const sampleCount = raw.length / 2;
+ const samples = new Int16Array(sampleCount);
const view = new DataView(samples.buffer);
for (let i = 0; i < raw.length; i += 2) {
// little-endian PCM16
@@ -132,9 +136,9 @@
}
// Convert to float32 AudioBuffer
- const buffer = audioCtx.createBuffer(1, samples.length, 24000);
+ const buffer = audioCtx.createBuffer(1, sampleCount, 24000);
const channel = buffer.getChannelData(0);
- for (let i = 0; i < samples.length; i++) {
+ for (let i = 0; i < sampleCount; i++) {
channel[i] = samples[i] / 32768.0;
}
@@ -146,8 +150,14 @@
if (nextStartTime < now) {
nextStartTime = now;
}
- source.start(nextStartTime);
+ const startAt = nextStartTime;
+ source.start(startAt);
nextStartTime += buffer.duration;
+ log(`audio queued seq=${seq} raw=${raw.length} samples=${sampleCount} duration=${buffer.duration.toFixed(2)}s startAt=${startAt.toFixed(3)} ctxState=${audioCtx.state}`);
+
+ source.onended = () => {
+ log(`audio ended seq=${seq}`);
+ };
};
$('connect').onclick = async () => {
@@ -180,12 +190,21 @@
};
ws.onmessage = (event) => {
- const msg = JSON.parse(event.data);
+ let msg;
+ try {
+ msg = JSON.parse(event.data);
+ } catch (err) {
+ log(`malformed message: ${err.message}`);
+ return;
+ }
if (msg.type === 'audio') {
(async () => {
try {
- await playPcm16(msg.data);
- log(`audio seq=${msg.seq} len=${(msg.data.length * 3 / 4 / 2 / 24000).toFixed(2)}s`);
+ log(`received audio seq=${msg.seq} base64=${String(msg.data).length} format=${msg.format} sr=${msg.sample_rate} ch=${msg.channels}`);
+ if (!msg.data || String(msg.data).trim() === '') {
+ throw new Error('Server sent empty audio data');
+ }
+ await playPcm16(msg.data, msg.seq);
} catch (err) {
log(`audio playback error: ${err.message}`);
}
@@ -194,6 +213,8 @@
log(`status ${msg.event} seq=${msg.seq}`);
} else if (msg.type === 'error') {
log(`error: ${msg.message}`);
+ } else {
+ log(`unknown message type=${msg.type}`);
}
};
@@ -222,7 +243,8 @@
return;
}
await ensureAudioContext();
- nextStartTime = 0;
+ nextStartTime = audioCtx.currentTime;
+ log(`Speak start, ctxState=${audioCtx.state}, nextStartTime=${nextStartTime.toFixed(3)}`);
const text = $('text').value.trim();
const words = text.split(/\s+/);