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