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