const WORKLET_SRC = ` class PCMCapture extends AudioWorkletProcessor { process(inputs) { const input = inputs[0]; if (input && input[0]) { const f32 = input[0]; const i16 = new Int16Array(f32.length); for (let i = 0; i < f32.length; i++) { const s = Math.max(-1, Math.min(1, f32[i])); i16[i] = s < 0 ? s * 0x8000 : s * 0x7fff; } this.port.postMessage(i16.buffer, [i16.buffer]); } return true; } } registerProcessor('pcm-capture', PCMCapture); `; function makeCtx() { try { return new AudioContext({ sampleRate: 16000 }); } catch { return new AudioContext(); } } export async function startCapture(onChunk) { const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true }, }); const ctx = makeCtx(); const url = URL.createObjectURL(new Blob([WORKLET_SRC], { type: 'application/javascript' })); await ctx.audioWorklet.addModule(url); URL.revokeObjectURL(url); const source = ctx.createMediaStreamSource(stream); const node = new AudioWorkletNode(ctx, 'pcm-capture'); node.port.onmessage = (e) => onChunk(e.data); source.connect(node); // not connected to destination -> no local feedback return { stop() { node.port.onmessage = null; try { source.disconnect(); } catch { /* already closed */ } stream.getTracks().forEach((t) => t.stop()); ctx.close().catch(() => {}); }, }; } export class PttPlayer { constructor() { this.ctx = makeCtx(); this.nextStart = 0; } play(pcmBuffer) { const i16 = new Int16Array(pcmBuffer); if (!i16.length) return; const f32 = new Float32Array(i16.length); for (let i = 0; i < i16.length; i++) f32[i] = i16[i] / 0x8000; const buffer = this.ctx.createBuffer(1, f32.length, 16000); buffer.copyToChannel(f32, 0); const src = this.ctx.createBufferSource(); src.buffer = buffer; src.connect(this.ctx.destination); const now = this.ctx.currentTime; if (this.nextStart < now) this.nextStart = now + 0.03; src.start(this.nextStart); this.nextStart += buffer.duration; } reset() { this.nextStart = 0; } } export function sosBeep(times = 3) { let ctx; try { ctx = new AudioContext(); } catch { return; } const t0 = ctx.currentTime; for (let i = 0; i < times; i++) { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'square'; osc.frequency.value = 880; const s = t0 + i * 0.35; gain.gain.setValueAtTime(0.0001, s); gain.gain.exponentialRampToValueAtTime(0.2, s + 0.02); gain.gain.exponentialRampToValueAtTime(0.0001, s + 0.3); osc.connect(gain).connect(ctx.destination); osc.start(s); osc.stop(s + 0.32); } setTimeout(() => ctx.close().catch(() => {}), times * 350 + 500); }