Files
STSheets/src/ref/manifest.ts
T

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;
}