music/ renders public/audio/score.wav (24-bit 48 kHz, exactly 120 s) with plain TypeScript DSP in Node (`npm run music`): D major on the 王道進行 "royal road" progression (G△9 – A9 – F#m7 – Bm7), a two-bar hook in the yo scale over the three drops, sidechained supersaws, a pluck arpeggio, FM bells on the notification pings and glass chimes for the cron jobs. Every drum hit is scheduled from src/score.ts, so the kicks sit within 0.2 ms of the grid the picture pulses on. check-music.mjs re-measures loudness (−15 LUFS, −1.3 dBTP), per-chapter levels and kick alignment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
61 lines
2.4 KiB
JavaScript
61 lines
2.4 KiB
JavaScript
/**
|
|
* Renders the score to public/audio/score.wav (48 kHz, stereo, 24-bit,
|
|
* exactly 5 760 000 frames = 120 s; bar 1 beat 1 is sample 0).
|
|
*
|
|
* music/song.ts and everything it imports are plain TypeScript DSP: esbuild
|
|
* bundles them into one ES module, which is imported here and run in Node.
|
|
* The render is deterministic: two runs give the same bytes.
|
|
*
|
|
* npm run music → public/audio/score.wav
|
|
* node music/render-music.mjs --raw --out mix.wav → the unmastered mix, 32-bit float
|
|
* node music/render-music.mjs --solo bass,pad --raw --out bass.wav → some buses alone
|
|
*
|
|
* Buses: drums fx bells pad stab night ring arp bass lead counter.
|
|
*/
|
|
import { build } from "esbuild";
|
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const args = process.argv.slice(2);
|
|
const option = (name) => {
|
|
const i = args.indexOf(`--${name}`);
|
|
return i >= 0 ? args[i + 1] : undefined;
|
|
};
|
|
const raw = args.includes("--raw");
|
|
const solo = option("solo")?.split(",");
|
|
const out = resolve(option("out") ?? join(root, "public/audio/score.wav"));
|
|
|
|
const started = performance.now();
|
|
const bundle = await build({
|
|
entryPoints: [join(root, "music/song.ts")],
|
|
bundle: true,
|
|
format: "esm",
|
|
platform: "node",
|
|
target: "node20",
|
|
write: false,
|
|
logLevel: "warning",
|
|
});
|
|
const song = await import(`data:text/javascript;base64,${Buffer.from(bundle.outputFiles[0].text).toString("base64")}`);
|
|
const { wav } = song;
|
|
|
|
const result = song.renderScore({ raw, solo });
|
|
const bytes = wav(result.L, result.R, raw);
|
|
mkdirSync(dirname(out), { recursive: true });
|
|
writeFileSync(out, bytes);
|
|
|
|
const f = (x) => x.toFixed(2);
|
|
const timing = Object.entries(result.seconds)
|
|
.map(([k, v]) => `${k} ${v.toFixed(1)} s`)
|
|
.join(" · ");
|
|
console.log(`[score] ${result.L.length} frames @ 48000 Hz → ${out} (${(bytes.length / 1e6).toFixed(1)} MB, ${raw ? "32-bit float, unmastered" : "24-bit"})`);
|
|
console.log(`[score] ${timing}`);
|
|
const m = result.master;
|
|
if (m) {
|
|
console.log(
|
|
`[score] ${f(m.lufs)} LUFS · true peak ${f(m.truePeak)} dBTP · sample peak ${f(m.samplePeak)} dBFS · gain ${f(m.gainDb)} dB · limiter ≤ ${f(m.maxLimitDb)} dB · glue ≤ ${f(m.maxGlueDb)} dB`,
|
|
);
|
|
}
|
|
console.log(`[score] done in ${((performance.now() - started) / 1000).toFixed(1)} s`);
|