morse-code / lib /mic.js
RemiFabre
feat: initial Reachy Mini Morse Code app
843a4b2
Raw
History Blame Contribute Delete
3.74 kB
/**
* 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; }
}
}