/** * Replay a recorded float32 mono buffer through the app's EXACT live decode * path: a Web Audio BiquadFilter highpass is emulated here, then blocks are * fed to StreamingOnsetDetector (lib/detector.js) just like mic.js does, and * onsets are decoded with wire.js. This validates the real live path against * real robot-clap audio (no browser, no robot). * * node tools/replay_streaming.mjs /tmp/robot_clap.f32 --sr 48000 --unit 150 \ * --expect SOS --highpass 2000 --factor 4 */ import { readFileSync } from "node:fs"; import { StreamingOnsetDetector, highpass } from "../lib/detector.js"; import { decodeOnsets } from "../lib/wire.js"; import { makeTiming } from "../lib/timing.js"; const args = process.argv.slice(2); const path = args[0]; const opt = (k, d) => { const i = args.indexOf(`--${k}`); return i >= 0 ? args[i + 1] : d; }; const sr = parseInt(opt("sr", "48000"), 10); const unit = parseInt(opt("unit", "150"), 10); const expect = (opt("expect", "") || "").toUpperCase(); const hp = parseFloat(opt("highpass", "2000")); const factor = parseFloat(opt("factor", "4")); const refractory = parseFloat(opt("refractory", "120")); const buf = readFileSync(path); const samples = new Float32Array(buf.buffer, buf.byteOffset, Math.floor(buf.byteLength / 4)); // The app applies the highpass via a Web Audio BiquadFilterNode upstream of // the ScriptProcessor. Emulate that by pre-filtering, then stream blocks. const filtered = highpass(samples, sr, hp); const onsets = []; const det = new StreamingOnsetDetector({ sampleRate: sr, thresholdFactor: factor, refractoryMs: refractory, onOnset: (t) => onsets.push(t), }); const BLOCK = 2048; for (let i = 0; i < filtered.length; i += BLOCK) { const block = filtered.subarray(i, Math.min(i + BLOCK, filtered.length)); det.process(block, (i / sr) * 1000); } const t = makeTiming(unit); const { morse, text } = decodeOnsets(onsets, t); console.log(`samples=${samples.length} (${(samples.length / sr).toFixed(1)}s)`); console.log(`onsets detected (live streaming path): ${onsets.length}`); console.log(`onset times (ms): ${onsets.map((x) => Math.round(x)).join(", ")}`); // inter-onset intervals — reveals rebounds (tiny gaps) vs real spacing console.log(`IOIs (ms): ${onsets.slice(1).map((x, i) => Math.round(x - onsets[i])).join(", ")}`); console.log(`morse: ${morse}`); console.log(`decoded: ${JSON.stringify(text)}`); if (expect) { const ok = text === expect; console.log(`expected ${JSON.stringify(expect)} -> ${ok ? "OK" : "MISMATCH"}`); process.exit(ok ? 0 : 2); }