dispatch-console / web /src /audio.js
secutorpro's picture
audio capture/playback/alarm
67f6d1b verified
Raw History Blame Contribute Delete
2.83 kB
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);
}