Files
ai-agent/reel/music/balance.mjs
Gabriel Vidal 945976fd2e feat(reel): an original score composed in code
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>
2026-10-11 04:50:47 +02:00

72 lines
2.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Mix-balancing aid: renders the raw mix with every bus kept apart, then
* prints each bus's loudness (LUFS, pre-master) per section and its octave
* band levels in a drop, a groove and the night. Read it before moving a
* fader in song.ts: in a drop the hook (lead) should sit on top, the kick
* and bass just under it.
*
* node music/balance.mjs
*/
import { build } from "esbuild";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const entry = `export * from "./song"; export { integratedLoudness } from "./master"; export { Biquad } from "./dsp"; export { at } from "../src/timeline";`;
const bundle = await build({ stdin: { contents: entry, resolveDir: join(root, "music"), loader: "ts" }, bundle: true, format: "esm", platform: "node", write: false });
const { renderScore, integratedLoudness, Biquad, at } = await import(`data:text/javascript;base64,${Buffer.from(bundle.outputFiles[0].text).toString("base64")}`);
const SR = 48000;
const r = renderScore({ raw: true, stems: true });
const stems = { ...r.stems, MIX: { L: r.L, R: r.R } };
const SPANS = [
["1–2", 1, 2],
["3–4", 3, 4],
["drop1", 5, 8],
["9–16", 9, 16],
["17–24", 17, 24],
["25–30", 25, 30],
["night", 31, 34],
["35–38", 35, 38],
["drop2", 39, 44],
["45–50", 45, 50],
["51–58", 51, 58],
["drop3", 59, 62],
["63–64", 63, 64],
];
const slice = (s, a, b) => [s.L.slice(Math.round(at(a) * SR), Math.round(at(b + 1) * SR)), s.R.slice(Math.round(at(a) * SR), Math.round(at(b + 1) * SR))];
const lufs = (s, a, b) => {
const v = integratedLoudness(...slice(s, a, b));
return Number.isFinite(v) ? v.toFixed(1) : "—";
};
console.log("LUFS per bus (raw, before the master)");
console.log("bus".padEnd(9) + SPANS.map(([n]) => n.padStart(8)).join(""));
for (const [name, s] of Object.entries(stems)) console.log(name.padEnd(9) + SPANS.map(([, a, b]) => lufs(s, a, b).padStart(8)).join(""));
const BANDS = [31.5, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];
for (const [label, a, b] of [
["drop 2 (39–44)", 39, 44],
["groove (9–16)", 9, 16],
["night (31–34)", 31, 34],
]) {
console.log(`\noctave bands, dB — ${label}`);
console.log("bus".padEnd(9) + BANDS.map((f) => String(f).padStart(7)).join(""));
for (const [name, s] of Object.entries(stems)) {
const [L, R] = slice(s, a, b);
const levels = BANDS.map((f) => {
let e = 0;
for (const x of [L, R]) {
const b1 = Biquad.bp(f, 1.41);
const b2 = Biquad.bp(f, 1.41);
for (let i = 0; i < x.length; i++) {
const y = b2.process(b1.process(x[i]));
e += y * y;
}
}
return 10 * Math.log10(e / (2 * L.length) + 1e-20);
});
if (Math.max(...levels) > -90) console.log(name.padEnd(9) + levels.map((v) => v.toFixed(1).padStart(7)).join(""));
}
}