Spaces:
Running
Running
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; }
}
}
|