morse-code / tools /replay_streaming.mjs
RemiFabre
tools: robot-clap hardware test + live-detector replay harness
2b1805b
Raw
History Blame Contribute Delete
2.58 kB
/**
* 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);
}