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+/);