Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
/**
* 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;
}