File size: 3,738 Bytes
843a4b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/**
 * Live microphone capture β†’ onset stream.
 *
 * Web Audio graph:
 *   getUserMedia β†’ MediaStreamSource β†’ BiquadFilter(highpass) β†’
 *   ScriptProcessor β†’ (muted gain β†’ destination, just to keep it pumping)
 *
 * The ScriptProcessor receives already-high-passed blocks and feeds them to
 * the StreamingOnsetDetector (lib/detector.js). Each detected onset is
 * reported with a session-relative timestamp (ms) suitable for wire.js
 * decoding. A level callback drives the visualizer.
 *
 * Browser audio "enhancements" (echo cancellation, noise suppression, auto
 * gain) are explicitly DISABLED β€” they treat sharp clicks as noise and would
 * gut exactly the signal we need.
 */
import { StreamingOnsetDetector } from "./detector.js";

export class Mic {
    /**
     * @param {object} o
     * @param {(timeMs:number, strength:number)=>void} o.onOnset
     * @param {(level:number, wave:Float32Array)=>void} [o.onLevel]
     * @param {number} [o.highpassHz]
     * @param {object} [o.detector] extra StreamingOnsetDetector options
     */
    constructor(o) {
        this.opts = o;
        this.highpassHz = o.highpassHz ?? 2000;
        this.ctx = null;
        this.stream = null;
        this._nodes = [];
        this._detector = null;
        this._elapsedMs = 0;
        this.running = false;
    }

    async start() {
        const AC = window.AudioContext || window.webkitAudioContext;
        this.stream = await navigator.mediaDevices.getUserMedia({
            audio: {
                echoCancellation: false,
                noiseSuppression: false,
                autoGainControl: false,
                channelCount: 1,
            },
        });
        this.ctx = new AC();
        if (this.ctx.state === "suspended") await this.ctx.resume();
        const sr = this.ctx.sampleRate;

        const src = this.ctx.createMediaStreamSource(this.stream);
        const hp = this.ctx.createBiquadFilter();
        hp.type = "highpass";
        hp.frequency.value = this.highpassHz;
        hp.Q.value = Math.SQRT1_2;

        const bufferSize = 2048;
        const proc = this.ctx.createScriptProcessor(bufferSize, 1, 1);
        const mute = this.ctx.createGain();
        mute.gain.value = 0;

        this._detector = new StreamingOnsetDetector({
            sampleRate: sr,
            onOnset: (timeMs, strength) => this.opts.onOnset?.(timeMs, strength),
            ...(this.opts.detector || {}),
        });
        this._elapsedMs = 0;
        this.running = true;

        proc.onaudioprocess = (e) => {
            if (!this.running) return;
            const block = e.inputBuffer.getChannelData(0);
            const base = this._elapsedMs;
            this._detector.process(block, base);
            this._elapsedMs += (block.length / sr) * 1000;
            if (this.opts.onLevel) {
                let sum = 0;
                for (let i = 0; i < block.length; i++) sum += block[i] * block[i];
                this.opts.onLevel(Math.sqrt(sum / block.length), block);
            }
        };

        src.connect(hp);
        hp.connect(proc);
        proc.connect(mute);
        mute.connect(this.ctx.destination);
        this._nodes = [src, hp, proc, mute];
    }

    /** Current session clock (ms since start) β€” handy to time out a message. */
    now() {
        return this._elapsedMs;
    }

    async stop() {
        this.running = false;
        this._nodes.forEach((n) => { try { n.disconnect(); } catch { /* */ } });
        this._nodes = [];
        if (this.stream) {
            this.stream.getTracks().forEach((t) => t.stop());
            this.stream = null;
        }
        if (this.ctx) { try { await this.ctx.close(); } catch { /* */ } this.ctx = null; }
    }
}