#!/usr/bin/env node // Hold the FINAL note of a cut longer, before arranging. // // A tune cut at `TIME_LIMIT` stops on whatever note happens to be last, at whatever // length the sequencer wrote it -- 0.218s for Mario RPG's lead, 0.106s for its bass. // That is a stop, not an ending. // // The extension has to happen in the LEAD file rather than in the finished plan, // because the arranger sizes its pick to the note: lengthening a slot afterwards // leaves the chosen clip too short and the extra slot is simply silence. Asking for a // long note up front makes it choose a clip that can hold it. // // node hold-last.mjs // // TL is in the lead file's own units (raw MIDI seconds), the same units TIME_LIMIT // filters on -- so "the last note of the cut" is the last note with t < TL, which is // not the last note of the file. import { readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; import { SONG_DATA } from "./paths.mjs"; const [IN, TLs, EXTRAs, OUT] = process.argv.slice(2); if (!OUT) throw new Error("usage: hold-last.mjs "); const TL = Number(TLs), EXTRA = Number(EXTRAs); const abs = (f) => (path.isAbsolute(f) ? f : path.join(SONG_DATA, f)); const notes = JSON.parse(readFileSync(abs(IN), "utf8")); const kept = notes.filter((n) => n.t < TL); if (!kept.length) throw new Error(`no notes before TL=${TL} in ${IN}`); const lastT = Math.max(...kept.map((n) => n.t)); // A final CHORD is several notes at the same onset; all of them are held, or the // remaining one sticks out as a lone voice over the rest. let n = 0; for (const note of notes) { if (Math.abs(note.t - lastT) < 1e-6) { note.dur = (note.dur ?? 0) + EXTRA; n += 1; } } writeFileSync(abs(OUT), JSON.stringify(notes)); console.log(`${path.basename(IN)}: ${kept.length} notes before TL=${TL}; held ${n} note(s) at ` + `t=${lastT.toFixed(3)} by +${EXTRA}s -> ${path.basename(OUT)}`);