Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
51 lines
2.0 KiB
TypeScript
51 lines
2.0 KiB
TypeScript
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<string, Role | null>; capo: Record<string, number>; tuning?: Record<string, number[]>;
|
|
}
|
|
|
|
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<SongEntry[]> {
|
|
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<string>();
|
|
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<SongEntry, "roles"> & { roles?: SongEntry["roles"] }, root = "reference"): Promise<SongEntry> {
|
|
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;
|
|
}
|