// Print a high-band / low-band energy trace so events can be READ off the audio. // node env-dump.mjs [lo] [hi] [hopMs] import { readFileSync } from "node:fs"; function readWav(file) { const b = readFileSync(file); let p = 12, fmt = null, data = null; while (p + 8 <= b.length) { const id = b.toString("latin1", p, p + 4), sz = b.readUInt32LE(p + 4), body = p + 8; if (id === "fmt ") fmt = { ch: b.readUInt16LE(body + 2), sr: b.readUInt32LE(body + 4), bits: b.readUInt16LE(body + 14) }; if (id === "data") { data = b.subarray(body, body + sz); break; } p = body + sz + (sz & 1); } const by = fmt.bits / 8, n = Math.floor(data.length / by / fmt.ch), x = new Float32Array(n); for (let i = 0; i < n; i += 1) { let s = 0; for (let c = 0; c < fmt.ch; c += 1) s += data.readInt16LE((i * fmt.ch + c) * by) / 32768; x[i] = s / fmt.ch; } return { x, sr: fmt.sr }; } function fft(re, im) { const N = re.length; for (let i = 1, j = 0; i < N; i += 1) { let bit = N >> 1; for (; j & bit; bit >>= 1) j ^= bit; j ^= bit; if (i < j) { [re[i], re[j]] = [re[j], re[i]]; [im[i], im[j]] = [im[j], im[i]]; } } for (let len = 2; len <= N; len <<= 1) { const ang = -2 * Math.PI / len; for (let i = 0; i < N; i += len) for (let k = 0; k < len / 2; k += 1) { const wr = Math.cos(ang * k), wi = Math.sin(ang * k), ur = re[i + k], ui = im[i + k]; const vr = re[i + k + len / 2] * wr - im[i + k + len / 2] * wi, vi = re[i + k + len / 2] * wi + im[i + k + len / 2] * wr; re[i + k] = ur + vr; im[i + k] = ui + vi; re[i + k + len / 2] = ur - vr; im[i + k + len / 2] = ui - vi; } } } const [f, startArg, loArg, hiArg, hopArg] = process.argv.slice(2); const start = Number(startArg ?? 0), lo = Number(loArg ?? 2000), hi = Number(hiArg ?? 9000), hop = Number(hopArg ?? 20) / 1000; const { x, sr } = readWav(f); const N = 1024, HOP = Math.round(hop * sr); const bin = sr / N; const kLo = [Math.max(1, Math.floor(lo / bin)), Math.min(N / 2 - 1, Math.ceil(hi / bin))]; const rows = []; for (let off = 0; off + N <= x.length; off += HOP) { const re = new Float64Array(N), im = new Float64Array(N); for (let i = 0; i < N; i += 1) re[i] = x[off + i] * (0.5 - 0.5 * Math.cos(2 * Math.PI * i / (N - 1))); fft(re, im); let hiE = 0, loE = 0, best = 0, bestK = 0; for (let k = 1; k < N / 2; k += 1) { const m = re[k] * re[k] + im[k] * im[k]; if (k >= kLo[0] && k <= kLo[1]) hiE += m; else if (k * bin < lo) loE += m; if (m > best) { best = m; bestK = k; } } rows.push({ t: start + off / sr, hi: hiE, lo: loE, peakHz: bestK * bin, peakMag: best }); } const mx = Math.max(...rows.map((r) => r.hi)); for (const r of rows) { const db = 10 * Math.log10(r.hi / mx + 1e-12); const bar = "#".repeat(Math.max(0, Math.round((db + 60) / 2))); console.log(`${r.t.toFixed(3)} hi ${db.toFixed(1).padStart(6)}dB ratio ${(r.hi / (r.lo + 1e-12)).toFixed(2).padStart(7)} peak ${r.peakHz.toFixed(0).padStart(5)}Hz ${bar}`); }