Skip to content

v0.4.0 — Loudness normalisation

Choose a tag to compare

@SteliyanH SteliyanH released this 27 Aug 11:35
· 19 commits to main since this release
a66f37f

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.