feat(align): SMF parser, role guess, reference manifest

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Sonnet 5.5 committed 2026-10-30 06:27:08 +01:00
1 parent 67168e6103
commit e910036e67
8 files changed
+414 -1

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
import { parseArgs } from "node:util";
import { addSong, trackKeys } from "../ref/manifest.ts";
import { parseSmf } from "./smf.ts";
const USAGE = `usage: bun run align add <audio> <midi> --id <id> [--title <t>] [--capo "<track name>=9"]...`;
async function add(argv: string[]) {
const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { id: { type: "string" }, title: { type: "string" }, capo: { type: "string", multiple: true } } });
const [audio, midi] = positionals;
if (!audio || !midi || !values.id) throw new Error(USAGE);
const smf = parseSmf(new Uint8Array(await Bun.file(midi).arrayBuffer()));
const keys = new Set(trackKeys(smf.tracks));
const capo: Record<string, number> = {};
for (const c of values.capo ?? []) {
const i = c.lastIndexOf("=");
const k = c.slice(0, i), n = Number(c.slice(i + 1));
if (i < 0 || !Number.isInteger(n) || n < 0) throw new Error(`bad --capo ${JSON.stringify(c)}; expected "<track name>=<fret>"`);
if (!keys.has(k)) throw new Error(`--capo: no track named ${JSON.stringify(k)}; tracks: ${[...keys].map((x) => JSON.stringify(x)).join(", ")}`);
capo[k] = n;
}
const e = await addSong({ id: values.id, title: values.title ?? values.id, audio, midi, capo });
console.log(`added ${e.id} (${e.title})`);
for (const [k, r] of Object.entries(e.roles)) console.log(` ${JSON.stringify(k)} -> ${r ?? "unmapped"}`);
}
const [cmd, ...rest] = process.argv.slice(2);
try {
if (cmd === "add") await add(rest);
else throw new Error(USAGE);
} catch (err) {
console.error((err as Error).message);
process.exit(1);
}