#!/usr/bin/env node // bake.mjs — generate c/score.h for the maytrax C engine (americomputadora // pattern). Reads kit.json (the freesound one-shot roster), shouts.json // (jeffrey-pvc + apple-say drop shouts) and the hellsine church-bell, and // emits a header of sample-path tables the C renderer loads at startup. // // Two root references are baked per pitched role because the original JS // used both: // • detected f0 — smpNote() path (sub / reese / pad). Computed in C at // load (port of detectF0), so only `pitched` is needed. // • nominal root — rateFor() path (lead / choir / strings / animal / wolf // / bass). The kit.json `root` string → midi, baked here. // // usage: node c/bake.mjs import { readFileSync, writeFileSync, existsSync } from "node:fs"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = dirname(HERE); // pop/maytrax const POP = dirname(ROOT); // pop const BPM = 160; const SR = 48000; const NN = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 }; const noteToMidi = (s) => { const m = /^([A-G])([#b]?)(-?\d)$/.exec(s || "C4"); if (!m) return 60; return NN[m[1]] + (m[2] === "#" ? 1 : m[2] === "b" ? -1 : 0) + (parseInt(m[3], 10) + 1) * 12; }; const kit = JSON.parse(readFileSync(resolve(ROOT, "kit.json"), "utf8")); const roles = Object.keys(kit); // shouts ------------------------------------------------------------------ const keyOf = { wake: "wake_up", real: "it_s_real", hold: "hold_on", let: "let_go", rabbit: "follow_the_white_rabbit", now: "now", }; let shouts = {}; const shPath = resolve(ROOT, "shouts.json"); if (existsSync(shPath)) shouts = JSON.parse(readFileSync(shPath, "utf8")); const shoutRows = Object.entries(keyOf).map(([k, slug]) => { const e = shouts[slug]; const j = e?.path && existsSync(e.path) ? e.path : ""; const a = e?.path ? e.path.replace(/\.wav$/, "-say-sung.wav") : ""; return { k, j, a: a && existsSync(a) ? a : "" }; }); const bell = resolve(POP, "hellsine/samples/church-bell.wav"); // emit -------------------------------------------------------------------- const esc = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); const L = []; L.push("// score.h — generated by c/bake.mjs. do not hand-edit."); L.push(`#define SCORE_BPM ${BPM}`); L.push(`#define SCORE_SR ${SR}`); L.push(""); L.push(`#define N_ROLES ${roles.length}`); L.push(`static const char *ROLE_NAME[N_ROLES] = {`); L.push(" " + roles.map((r) => `"${r}"`).join(", ")); L.push("};"); L.push(`static const char *ROLE_PATH[N_ROLES] = {`); for (const r of roles) L.push(` "${esc(kit[r].path)}",`); L.push("};"); L.push(`static const int ROLE_PITCHED[N_ROLES] = {`); L.push(" " + roles.map((r) => (kit[r].pitched ? 1 : 0)).join(", ")); L.push("};"); L.push(`// nominal root midi from kit.json (rateFor path); -1 = unpitched`); L.push(`static const int ROLE_NOMINAL[N_ROLES] = {`); L.push(" " + roles.map((r) => (kit[r].pitched ? noteToMidi(kit[r].root || "C4") : -1)).join(", ")); L.push("};"); L.push(""); L.push(`static const char *BELL_PATH = "${esc(bell)}";`); L.push(""); L.push(`#define N_SHOUTS ${shoutRows.length}`); L.push(`static const char *SHOUT_KEY[N_SHOUTS] = { ${shoutRows.map((s) => `"${s.k}"`).join(", ")} };`); L.push(`static const char *SHOUT_J[N_SHOUTS] = {`); for (const s of shoutRows) L.push(` "${esc(s.j)}",`); L.push("};"); L.push(`static const char *SHOUT_A[N_SHOUTS] = {`); for (const s of shoutRows) L.push(` "${esc(s.a)}",`); L.push("};"); L.push(""); writeFileSync(resolve(HERE, "score.h"), L.join("\n") + "\n"); console.log(`✓ score.h · ${roles.length} roles · ${shoutRows.filter((s) => s.j).length}/6 jeffrey + ${shoutRows.filter((s) => s.a).length}/6 apple shouts · bell ${existsSync(bell) ? "✓" : "✗"}`);