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');