feat: /testcall /testecho /testmic — local audio diagnostics

- /testcall: plays 3 tones (440/880/660Hz) to test speaker
- /testecho: mic → speaker loopback with 100ms delay (/stopecho to end)
- /testmic: records 3 seconds, plays back (tests both mic + speaker)

No relay or peer needed — pure local browser audio.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-30 16:38:26 +04:00
parent 5ae87be316
commit 7c248442c2

View File

@@ -2068,6 +2068,97 @@ async function doSend() {
return;
}
if (text === '/call') { startCall(); return; }
if (text === '/testcall' || text === '/testtone') {
addSys('Audio test: playing 440Hz tone for 3 seconds...');
try {
const ctx = new AudioContext({ sampleRate: 48000 });
const osc = ctx.createOscillator();
osc.type = 'sine';
osc.frequency.value = 440;
osc.connect(ctx.destination);
osc.start();
setTimeout(() => {
osc.frequency.value = 880;
addSys('Audio test: 880Hz...');
}, 1000);
setTimeout(() => {
osc.frequency.value = 660;
addSys('Audio test: 660Hz...');
}, 2000);
setTimeout(() => {
osc.stop();
ctx.close();
addSys('Audio test: done. If you heard 3 tones, speaker works.');
}, 3000);
} catch(e) { addSys('Audio test failed: ' + e.message); }
return;
}
if (text === '/testecho') {
addSys('Echo test: speak into mic, you should hear yourself...');
addSys('Type /stopecho to stop.');
try {
const ctx = new AudioContext({ sampleRate: 48000 });
const stream = await navigator.mediaDevices.getUserMedia({
audio: { sampleRate: 48000, channelCount: 1, echoCancellation: false, noiseSuppression: false }
});
const source = ctx.createMediaStreamSource(stream);
// Direct mic → speaker loopback (with small delay to avoid feedback)
const delay = ctx.createDelay(0.15);
delay.delayTime.value = 0.1;
source.connect(delay);
delay.connect(ctx.destination);
addSys('Echo active \u2014 mic \u2192 speaker (100ms delay)');
window._echoTest = { ctx, stream, source, delay };
} catch(e) { addSys('Echo test failed: ' + e.message); }
return;
}
if (text === '/stopecho') {
if (window._echoTest) {
window._echoTest.stream.getTracks().forEach(t => t.stop());
window._echoTest.source.disconnect();
window._echoTest.delay.disconnect();
window._echoTest.ctx.close();
window._echoTest = null;
addSys('Echo test stopped.');
} else { addSys('No echo test running.'); }
return;
}
if (text === '/testmic') {
addSys('Mic test: recording 3 seconds, then playback...');
try {
const ctx = new AudioContext({ sampleRate: 48000 });
const stream = await navigator.mediaDevices.getUserMedia({
audio: { sampleRate: 48000, channelCount: 1, echoCancellation: true, noiseSuppression: true }
});
const source = ctx.createMediaStreamSource(stream);
const processor = ctx.createScriptProcessor(4096, 1, 1);
const recorded = [];
processor.onaudioprocess = (e) => {
recorded.push(new Float32Array(e.inputBuffer.getChannelData(0)));
};
source.connect(processor);
processor.connect(ctx.destination);
addSys('Recording... speak now');
setTimeout(() => {
processor.disconnect();
source.disconnect();
stream.getTracks().forEach(t => t.stop());
// Concatenate and play back
const total = recorded.reduce((s, a) => s + a.length, 0);
const buffer = ctx.createBuffer(1, total, 48000);
const channel = buffer.getChannelData(0);
let offset = 0;
for (const chunk of recorded) { channel.set(chunk, offset); offset += chunk.length; }
addSys('Playing back ' + (total / 48000).toFixed(1) + 's of audio...');
const playSource = ctx.createBufferSource();
playSource.buffer = buffer;
playSource.connect(ctx.destination);
playSource.start();
playSource.onended = () => { ctx.close(); addSys('Mic test done. If you heard yourself, mic + speaker work.'); };
}, 3000);
} catch(e) { addSys('Mic test failed: ' + e.message); }
return;
}
if (text === '/hangup' || text === '/end') { hangupCall(); return; }
if (text === '/accept') { acceptCall(); return; }
if (text === '/reject') { rejectCall(); return; }