#!/usr/bin/env node // Extract the top-voice (melody) line from the parsed MIDI and quantise it. import { readFileSync, writeFileSync } from "node:fs"; const d = JSON.parse(readFileSync(process.argv[2], "utf8")); const track = d.tracks.filter((t) => t.count).sort((a, b) => b.count - a.count)[0]; const notes = track.notes; // Group by onset (performance rubato means "simultaneous" notes differ slightly). const groups = []; for (const n of notes) { const g = groups[groups.length - 1]; if (g && Math.abs(n.beat - g.beat) < 0.12) { g.notes.push(n); g.beat = Math.min(g.beat, n.beat); } else groups.push({ beat: n.beat, t: n.t, notes: [n] }); } // Melody = highest note of each onset group, but only where that note is in the // lead register. Below that it is accompaniment sounding alone. const MIN_MIDI = 69; // A4 const lead = []; for (const g of groups) { const top = g.notes.reduce((a, b) => (b.midi > a.midi ? b : a)); if (top.midi >= MIN_MIDI) lead.push({ ...top, groupBeat: g.beat }); } const stats = (arr) => { const s = [...arr].sort((a, b) => a - b); const q = (p) => s[Math.max(0, Math.min(s.length - 1, Math.floor(p * (s.length - 1))))]; return { min: s[0], p10: q(0.1), p50: q(0.5), p90: q(0.9), max: s[s.length - 1] }; }; const iois = []; for (let i = 1; i < lead.length; i += 1) iois.push(+(lead[i].t - lead[i - 1].t).toFixed(3)); console.log(`track "${track.name}" total notes ${track.count}`); console.log(`onset groups ${groups.length}; lead notes (>=A4) ${lead.length}`); console.log(`piece length ${(notes.reduce((m, n) => Math.max(m, n.t + n.dur), 0)).toFixed(1)}s`); console.log("lead IOI (s):", JSON.stringify(stats(iois))); console.log("lead dur (s):", JSON.stringify(stats(lead.map((n) => n.dur)))); console.log("lead beats :", JSON.stringify(stats(lead.map((n) => n.beats)))); const pcs = {}; for (const n of lead) pcs[n.name.replace(/\d/, "")] = (pcs[n.name.replace(/\d/, "")] ?? 0) + 1; console.log("pitch classes:", JSON.stringify(pcs)); console.log("\nfirst 48 lead notes (name@sec dur):"); console.log(lead.slice(0, 48).map((n) => `${n.name}@${n.t.toFixed(2)}(${n.dur.toFixed(2)})`).join(" ")); writeFileSync(process.argv[3] ?? "lead.json", JSON.stringify(lead, null, 1));