Expand description
ITU-R BS.1770-style loudness measurement and normalization (mono).
Why LUFS and not plain RMS: preference data is poisoned by loudness — louder reliably wins A/B tests — so candidates must be matched on perceived loudness before audition and feature extraction. K-weighting (a high-shelf boost above ~1.7 kHz plus a ~38 Hz highpass) approximates the ear’s sensitivity, and 400 ms gated blocks keep silence and release tails from dragging the measurement down.
The filter coefficients are derived parametrically (RBJ bilinear transform) from the BS.1770 analog prototype — the same approach pyloudnorm uses — so any sample rate works, matching the spec’s published 48 kHz coefficients at 48 kHz.
§Loudness is a target, not a promise: the peak wins
Matching integrated loudness says nothing about the peak. Crest factor
varies by tens of dB across this grammar — a pad and a pluck at the same
LUFS are nowhere near the same peak — so normalizing to a target level
sends percussive patches well over full scale. Measured over 150 vetted
prior draws before PEAK_CEILING existed: 15 % of renders peaked above
1.0 and 8 % above 1.25 (which is where the app’s master.gain = 0.8
clips), with a worst case of 4.06 — 12 dB over.
That is not a cosmetic defect. Preference data is elicited on this exact
buffer, so a clipped audition collects a vote about clipping rather than
about the patch — which is precisely the confound loudness normalization
exists to remove, one stage later and silent. The live voice was never
exposed to it (auracle_wasm::live’s master limiter has always held a 0.98
ceiling); the offline path took the volt divisor and not the limiter.
The fix is a smaller gain, not a limiter. normalize_to gives up
whatever makeup it has to for the peak to clear PEAK_CEILING, and
reports how much in NormReport::peak_reduction_db. A limiter would hold
the loudness target but reshape the waveform, which moves crest,
flatness_mean and flux_mean as well as the RMS pair, and would need a
second copy of itself inside
render_playback kept in lockstep
forever. A scalar keeps that replay bit-identical by construction and
cannot change timbre at all.
What it costs, stated plainly: the ~15 % of patches that hit the ceiling
sit below the loudness target, so they audition quieter than the rest.
Loudness matching degrades exactly where crest is highest. That is the
right trade — quieter is a smaller bias on a preference judgment than
clipped — but it is a trade, and peak_reduction_db is on the record so a
surface can say “pulled down 3.2 dB so it would not clip” instead of
pretending the patch was simply quiet.
Structs§
- Norm
Report - Result of loudness normalization.
Constants§
- MAX_
GAIN_ DB - Maximum boost applied during normalization — a very quiet patch is a vet problem, not something to amplify by 60 dB.
- PEAK_
CEILING - Peak ceiling of the normalized buffer, in the nominal ±1.0 float domain.
Functions§
- integrated_
lufs - Gated integrated loudness in LUFS.
Nonewhen no block clears the −70 LUFS absolute gate (i.e. the signal is effectively silent). - normalize_
to - Normalize
samplesin place toward the target integrated loudness, never exceedingPEAK_CEILING.