File size: 2,308 Bytes
c0ddd13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
const TARGET_SAMPLE_RATE = 16000;
const CHUNK_SAMPLES = 3200; // 100ms at 16kHz

export class AudioRecorder {
  private context: AudioContext | null = null;
  private stream: MediaStream | null = null;
  private workletNode: AudioWorkletNode | null = null;
  private accumulator: Float32Array = new Float32Array(0);
  micLevel = 0;

  async start(onChunk: (pcm: ArrayBuffer) => void): Promise<void> {
    this.stream = await navigator.mediaDevices.getUserMedia({
      audio: {
        channelCount: 1,
        echoCancellation: true,
        noiseSuppression: true,
      },
    });

    this.context = new AudioContext({ sampleRate: TARGET_SAMPLE_RATE });
    await this.context.resume();

    await this.context.audioWorklet.addModule(
      new URL("./RecorderWorkletProcessor.js", import.meta.url).href
    );

    const source = this.context.createMediaStreamSource(this.stream);
    this.workletNode = new AudioWorkletNode(this.context, "recorder-processor");

    this.workletNode.port.onmessage = (e: MessageEvent<Float32Array>) => {
      const input = e.data;
      const combined = new Float32Array(this.accumulator.length + input.length);
      combined.set(this.accumulator);
      combined.set(input, this.accumulator.length);
      this.accumulator = combined;

      while (this.accumulator.length >= CHUNK_SAMPLES) {
        const chunk = this.accumulator.slice(0, CHUNK_SAMPLES);
        this.accumulator = this.accumulator.slice(CHUNK_SAMPLES);

        // RMS for barge-in detection
        let sumSq = 0;
        for (let i = 0; i < chunk.length; i++) sumSq += chunk[i] * chunk[i];
        this.micLevel = Math.sqrt(sumSq / chunk.length) * 32767;

        // Convert float32 → int16 PCM
        const int16 = new Int16Array(chunk.length);
        for (let i = 0; i < chunk.length; i++) {
          const s = Math.max(-1, Math.min(1, chunk[i]));
          int16[i] = s < 0 ? s * 32768 : s * 32767;
        }

        onChunk(int16.buffer);
      }
    };

    source.connect(this.workletNode);
  }

  stop(): void {
    this.workletNode?.disconnect();
    this.workletNode = null;
    this.stream?.getTracks().forEach((t) => t.stop());
    this.stream = null;
    this.context?.close();
    this.context = null;
    this.accumulator = new Float32Array(0);
    this.micLevel = 0;
  }
}