import { join, resolve } from "node:path"; import { mkdir } from "node:fs/promises"; import type { Role } from "../core/types.ts"; import { parseSmf } from "../align/smf.ts"; import { guessRole } from "../align/roles.ts"; export interface SongEntry { id: string; title: string; audio: string; midi: string; roles: Record; capo: Record; tuning?: Record; } const ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/; export function songDir(id: string, root = "reference"): string { if (!ID_RE.test(id)) throw new Error(`invalid song id: ${JSON.stringify(id)}`); return join(root, id); } export async function readManifest(root = "reference"): Promise { const f = Bun.file(join(root, "songs.json")); if (!(await f.exists())) return []; return (await f.json()) as SongEntry[]; } // Role-map key for a track: its name, or "track N" when unnamed; disambiguated on collision. export function trackKeys(tracks: { index: number; name: string }[]): string[] { const used = new Set(); return tracks.map((t) => { let k = t.name || `track ${t.index}`; if (used.has(k)) k = `${k} #${t.index}`; used.add(k); return k; }); } export async function addSong(e: Omit & { roles?: SongEntry["roles"] }, root = "reference"): Promise { const dir = songDir(e.id, root); const all = await readManifest(root); if (all.some((s) => s.id === e.id)) throw new Error(`duplicate song id: ${e.id}`); let roles = e.roles; if (!roles) { const tracks = parseSmf(new Uint8Array(await Bun.file(e.midi).arrayBuffer())).tracks; const keys = trackKeys(tracks); roles = Object.fromEntries(tracks.map((t, i) => [keys[i]!, guessRole(t)])); } const entry: SongEntry = { ...e, audio: resolve(e.audio), midi: resolve(e.midi), roles }; await mkdir(dir, { recursive: true }); await Bun.write(join(root, "songs.json"), JSON.stringify([...all, entry], null, 2) + "\n"); return entry; }