#!/usr/bin/env node // The one report every re-arrange has to produce: how far each voice's clips had to // be pitched, and IN WHICH DIRECTION. // // |shift| alone has hidden the fault twice in this project -- a p50 of 1.33 looked // healthy while 72% of a voice was being dragged DOWN into the bass's register. So // this prints signed counts beside the magnitudes, and the unique-clip count, which // is what says whether a voice was starved. // // node plan-shift.mjs [more.json ...] import { readFileSync } from "node:fs"; import path from "node:path"; import { SONG_DATA } from "./paths.mjs"; const q = (a, p) => (a.length ? a.slice().sort((x, y) => x - y)[Math.min(a.length - 1, Math.floor(p * a.length))] : 0); for (const file of process.argv.slice(2)) { const f = path.isAbsolute(file) ? file : path.join(SONG_DATA, file); const plan = JSON.parse(readFileSync(f, "utf8")); const end = Math.max(...plan.voices.flatMap((v) => v.plan.map((n) => n.slotStart + n.slotDur))); const start = Math.min(...plan.voices.filter((v) => v.plan.length).map((v) => v.plan[0].slotStart)); console.log(`\n${path.basename(f)} ${start.toFixed(2)} -> ${end.toFixed(2)}s (${(end - start).toFixed(2)}s)`); for (const v of plan.voices) { if (!v.plan.length) { console.log(` ${v.name.padEnd(7)} EMPTY`); continue; } const sh = v.plan.map((n) => n.shift ?? 0); const abs = sh.map(Math.abs); const uniq = new Set(v.plan.map((n) => `${n.video}@${(+n.srcStart).toFixed(2)}`)).size; const down = sh.filter((s) => s < -0.05).length, up = sh.filter((s) => s > 0.05).length; console.log(` ${v.name.padEnd(7)} ${String(v.plan.length).padStart(4)} notes, ${String(uniq).padStart(4)} unique ` + `|shift| p50 ${q(abs, 0.5).toFixed(2)} p90 ${q(abs, 0.9).toFixed(2)} max ${Math.max(...abs).toFixed(2)} ` + `(${down} down / ${up} up, worst down ${Math.min(...sh).toFixed(2)})`); } }