/** * Loudness range (LRA, in LU) per EBU Tech 3342. * * Inputs are short-term loudness samples — values of K-weighted, channel- * summed loudness measured over 3 s windows stepped by 1 s. The algorithm * applies an absolute gate at −70 LUFS, then a relative gate 20 LU below the * gated mean (wider than the integrated-loudness relative gate of 10 LU — * this is intentional in Tech 3342), and returns the 95th-minus-10th * percentile spread of the surviving distribution. */ import { ABSOLUTE_GATE_LUFS, energyToLufs, LUFS_SILENCE_FLOOR, } from "./lufs.js"; /** EBU Tech 3342 relative-gate offset. */ export const LRA_RELATIVE_GATE_OFFSET_LU = -20; export interface LoudnessRangeResult { /** LRA in LU — 95th percentile minus 10th percentile. */ readonly lra: number; /** Lower percentile (10th). */ readonly low: number; /** Upper percentile (95th). */ readonly high: number; /** Number of short-term blocks that survived both gates. */ readonly gatedCount: number; } /** * Compute loudness range from a flat array of short-term LUFS values. If the * input contains fewer than one full short-term window (i.e. is empty), the * caller should pass `[]` and the function reports `lra: 0` so consumers do * not have to special-case very short documents. */ export function loudnessRange( shortTermLufs: readonly number[] ): LoudnessRangeResult { if (shortTermLufs.length === 0) return { lra: 0, low: LUFS_SILENCE_FLOOR, high: LUFS_SILENCE_FLOOR, gatedCount: 0, }; // Absolute gate. const aboveAbs: number[] = []; for (const v of shortTermLufs) if (v > ABSOLUTE_GATE_LUFS) aboveAbs.push(v); if (aboveAbs.length === 0) return { lra: 0, low: LUFS_SILENCE_FLOOR, high: LUFS_SILENCE_FLOOR, gatedCount: 0, }; // Ungated mean of absolute-gate survivors (mean of linear power → LUFS). let powerSum = 0; for (const v of aboveAbs) powerSum += 10 ** ((v + 0.691) / 10); const meanPower = powerSum / aboveAbs.length; const meanLufs = energyToLufs(meanPower); const relativeGate = meanLufs + LRA_RELATIVE_GATE_OFFSET_LU; const gated: number[] = []; for (const v of aboveAbs) if (v > relativeGate) gated.push(v); // gated.length is always ≥ 1 here: at least the loudest survivor of the // absolute gate must exceed (mean − 20 LU). The branch is asserted by // construction; no explicit fallback path is needed. gated.sort((a, b) => a - b); const low = percentile(gated, 0.1); const high = percentile(gated, 0.95); return { lra: high - low, low, high, gatedCount: gated.length }; } /** * Linear-interpolated percentile of a sorted ascending array. EBU Tech 3342 * specifies linear interpolation between adjacent order statistics. */ export function percentile(sortedAsc: readonly number[], q: number): number { const n = sortedAsc.length; if (n === 0) return LUFS_SILENCE_FLOOR; if (n === 1) return sortedAsc[0] as number; const idx = q * (n - 1); const lo = Math.floor(idx); const hi = Math.ceil(idx); const frac = idx - lo; const a = sortedAsc[lo] as number; const b = sortedAsc[hi] as number; return a + (b - a) * frac; }