// Loudness figures and what they mean, with NO server imports -- the table is // a client component and needs the target constants at runtime (see // lib/note-types.ts for the break that rule exists to prevent). export type Loudness = { label: string; duration: number; /** BS.1770 gated integrated loudness, LUFS. Null for a silent track (-inf). */ integrated: number | null; /** EBU Tech 3342 loudness range, LU. */ lra: number | null; /** dBTP, from ebur128's own true-peak meter. Not a sample peak. */ truePeak: number | null; /** Loudest 400 ms and 3 s windows, when the frame log carried them. */ maxMomentary: number | null; maxShortTerm: number | null; /** Seconds the measurement took, so a slow file is visible as a slow file. */ tookMs: number; }; export type LoudnessTarget = { lufs: number; truePeak: number }; /** * YouTube normalises playback to about -14 LUFS and DOES NOT TURN QUIET * CONTENT UP -- it only attenuates what is louder. So -14 is a ceiling worth * hitting from below, and -1 dBTP is the headroom a lossy transcode needs to * avoid clipping on the way back out. * * Overridable per song in spec.json, because a deliverable bound for somewhere * else has a different target and hard-coding one would make the verdict lie. */ export const DEFAULT_TARGET: LoudnessTarget = { lufs: -14, truePeak: -1 }; export type LoudnessVerdict = { /** The sentence. This is the output; the numbers are the evidence. */ line: string; /** Only two things are ever flagged, and both have a consequence. */ flags: string[]; ok: boolean; }; /** * State the target, report the delta, flag only what has a consequence. * * Deliberately not a score. "-17.2 LUFS" means nothing on its own; "3.2 LU * under the target, and nothing will raise it" is the thing worth knowing. */ export function loudnessVerdict(l: Loudness, target: LoudnessTarget = DEFAULT_TARGET): LoudnessVerdict { const flags: string[] = []; if (l.integrated === null) { return { line: "silent — no gated loudness to report", flags: [], ok: false }; } const d = l.integrated - target.lufs; const line = Math.abs(d) < 0.5 ? `${l.integrated.toFixed(1)} LUFS — on target (${target.lufs})` : d < 0 ? `${l.integrated.toFixed(1)} LUFS — ${Math.abs(d).toFixed(1)} LU under ${target.lufs}; playback normalisation will not raise it, so it plays quiet next to everything else` : `${l.integrated.toFixed(1)} LUFS — ${d.toFixed(1)} LU over ${target.lufs}; playback will attenuate by the excess, so the loud mix buys nothing and costs headroom`; if (l.truePeak !== null && l.truePeak > target.truePeak) { flags.push(`true peak ${l.truePeak.toFixed(1)} dBTP is over ${target.truePeak} — it will clip on a lossy transcode`); } if (d > 1) flags.push(`${d.toFixed(1)} LU over target`); return { line, flags, ok: flags.length === 0 }; } /** The spread across a set — two cuts of one song differing by 2 LU is a bug. */ export function loudnessSpread(rows: { integrated: number | null }[]): number | null { const v = rows.map((r) => r.integrated).filter((n): n is number => n !== null); if (v.length < 2) return null; return +(Math.max(...v) - Math.min(...v)).toFixed(1); }