File size: 2,834 Bytes
b5200e2
 
 
 
67f6d1b
b5200e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67f6d1b
b5200e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
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);
}