diff --git a/examples/client_browser.html b/examples/client_browser.html index ae66a11..508e31c 100644 --- a/examples/client_browser.html +++ b/examples/client_browser.html @@ -56,6 +56,7 @@
+
@@ -77,17 +78,52 @@ const nextSeq = () => ++seq; const ensureAudioContext = async () => { + const AudioContextCtor = window.AudioContext || window.webkitAudioContext; if (!audioCtx) { - audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 24000 }); + audioCtx = new AudioContextCtor({ sampleRate: 24000 }); + } + if (audioCtx.state === 'closed') { + audioCtx = new AudioContextCtor({ sampleRate: 24000 }); } if (audioCtx.state === 'suspended') { + log(`Resuming AudioContext (state=${audioCtx.state}) ...`); await audioCtx.resume(); + // Some browsers need a moment before currentTime starts advancing. + let attempts = 0; + while (audioCtx.state !== 'running' && attempts < 20) { + await new Promise((r) => setTimeout(r, 25)); + attempts++; + } + log(`AudioContext state after resume: ${audioCtx.state}`); } }; + const playTone = async (freq = 440, duration = 0.5, amplitude = 0.5) => { + await ensureAudioContext(); + const samplesCount = Math.ceil(24000 * duration); + const buffer = audioCtx.createBuffer(1, samplesCount, 24000); + const channel = buffer.getChannelData(0); + for (let i = 0; i < samplesCount; i++) { + const t = i / 24000; + channel[i] = amplitude * Math.sin(2 * Math.PI * freq * t) * (1 - t / duration); + } + const source = audioCtx.createBufferSource(); + source.buffer = buffer; + source.connect(audioCtx.destination); + const startTime = audioCtx.currentTime + 0.02; + source.start(startTime); + log(`Playing test tone at ${freq} Hz for ${duration}s`); + }; + const playPcm16 = async (base64Data) => { await ensureAudioContext(); + if (audioCtx.state !== 'running') { + throw new Error(`AudioContext not running (state=${audioCtx.state})`); + } const raw = atob(base64Data); + if (raw.length === 0) { + throw new Error('Empty audio data'); + } const samples = new Int16Array(raw.length / 2); const view = new DataView(samples.buffer); for (let i = 0; i < raw.length; i += 2) { @@ -124,6 +160,7 @@ await ensureAudioContext(); log('Connected'); $('connect').disabled = true; + $('testAudio').disabled = false; $('speak').disabled = false; $('stop').disabled = false; @@ -164,6 +201,7 @@ log('Disconnected'); ws = null; $('connect').disabled = false; + $('testAudio').disabled = true; $('speak').disabled = true; $('stop').disabled = true; }; @@ -174,6 +212,10 @@ } }; + $('testAudio').onclick = async () => { + await playTone(440, 0.6, 0.5); + }; + $('speak').onclick = async () => { if (!ws || ws.readyState !== WebSocket.OPEN) { log('Not connected');