v0.4.0 — Loudness normalisation
Every social platform normalises on upload — Instagram, TikTok and YouTube all target roughly −14 LUFS. A composition mixed by ear is re-levelled after publishing, usually downward and unevenly across clips mixed at different times.
Measuring first is the only way to control that rather than discover it.
let measured = Loudness.integrated(samples: samples, sampleRate: 48_000, channels: 2)
let track = AudioTrack(url: musicURL).normalized(from: measured, to: .social)Implemented per ITU-R BS.1770-4: K-weighting, 400 ms blocks at 75% overlap, absolute gate at −70 LUFS, relative gate 10 LU below what survives it.
Tested by properties, not reference values
"A 1 kHz sine at −20 dBFS measures −21.3 LUFS" would pin the code to one particular filter and break on any legitimate refinement. These hold for every correct implementation and fail for most incorrect ones:
- doubling amplitude → +6.02 dB
- tenfold → +20 dB
- length does not change loudness — it is a rate, not a total
- a silent half is gated out, not averaged in
- measure → gain → measure lands within 0.3 dB of target
Three limits, documented rather than hidden
A gain above 1.0 raises peaks as well as loudness and nothing here limits them, so a very quiet source pushed to −14 LUFS may clip. The caller may know their source has headroom, so this returns the number rather than deciding.
Silence returns a gain of 1.0. Multiplying silence by anything is still silence.
The K-weighting coefficients are specified at 48 kHz and applied directly — exact there, an approximation at 44.1 kHz. What most implementations do, but an approximation, and the source says so.
26 → 41 tests.