Real-time

Loudness (LUFS)

loudness
frequency-domainlow-latencypolyphonicperceptual loudness

An envelope detector traces the loudness contour of a waveform — the slow outline riding over the fast carrier inside it. Every graph on this page is drawn by the method's real algorithm, and the sliders at the top drive all of them at once.

The whole method, live

Loudness (LUFS)
loudnesspolyphonic
Loudness (LUFS)
Window32 samp (0.7 ms)

Score card

Causality
low-latency
Signal model
polyphonic
Reads
perceptual loudness
Latency
½ window
Cost
filter + mean-sq
Domain
frequency

Scored qualitatively.

This method outputs a normalized contour (onset strength, per-band or perceptual loudness), not an amplitude in the units of the true envelope — so an amplitude error number would be meaningless. Its strength is the spectral axis: read the gallery below.

How it works

How loud it actually sounds, not how big the samples are. Pass the signal through the ITU-R BS.1770 K-weighting filter — a coarse model of the ear's frequency response — then take a short-time mean-square. This is the loudness measure used across streaming and broadcast; it weights mid and high energy more than the lows, so bass-heavy material doesn't read as louder than it sounds, and the contour tracks perceived loudness through a busy mix.

The window sets momentary versus short-term. (The absolute frequencies here are illustrative, but the filter-then-mean-square detector is the real one.)

Key terms

K-weighting
The ITU-R BS.1770 pre-filter — a coarse model of the ear's frequency response. A high-shelf then a high-pass that de-emphasizes low frequencies and lifts the upper-mids, so the signal is weighted the way the ear weighs loudness before any power is measured.
Mean-square / loudness (LUFS)
The short-time average power of the K-weighted signal — square each sample, average over a sliding window. Reported in LUFS, the loudness unit used across streaming and broadcast to set program level.
Momentary vs short-term
The window length. A short window gives momentary loudness that follows fast level changes; a longer window gives short-term loudness, a steadier read of how loud a passage sits.

Building the envelope, step by step

Each step adds one idea and shows a graph with only that principle applied — drawn by the real algorithm on a polyphonic mix, working up to the finished loudness contour.

  1. Step 1The raw mix

    Start with the polyphonic input — several voices at once. Raw sample height is not loudness: a heavy bass note can dwarf a bright lead on the meter while sounding quieter to the ear.

  2. Step 2K-weighting

    Run the signal through the K-weighting filter, a high-shelf then a high-pass. It de-emphasizes low end and lifts the upper-mids, reshaping the waveform toward what the ear actually weighs as loud.

  3. Step 3Short-time mean-square

    Take a centered mean-square of the K-weighted signal over a sliding window and square-root it. The result is a smooth perceptual loudness contour that hugs the swell of the mix.

The code

Six readable forms of the exact algorithm that draws the curves above — C, JS and Python ports, an optimized C, a fixed-coefficient version, and a user-controlled one whose parameters match the sliders.

#include <math.h>

/* One Direct-Form-I biquad: y = b0*x + b1*x1 + b2*x2 - a1*y1 - a2*y2. */
static void biquad(const double *x, double *y, int n,
                   const double b[3], const double a[3]) {
    double x1 = 0, x2 = 0, y1 = 0, y2 = 0;
    for (int i = 0; i < n; i++) {
        double xn = x[i];
        double yn = b[0]*xn + b[1]*x1 + b[2]*x2 - a[1]*y1 - a[2]*y2;
        x2 = x1; x1 = xn;
        y2 = y1; y1 = yn;
        y[i] = yn;
    }
}

/* ITU-R BS.1770 K-weighting: a high-shelf then a high-pass. */
static void k_weight(const double *x, double *y, double *tmp, int n) {
    /* stage 1: high-shelf  */
    static const double b1[3] = { 1.53512485958697, -2.69169618940638, 1.19839281085285 };
    static const double a1[3] = { 1.0, -1.69065929318241, 0.73248077421585 };
    /* stage 2: high-pass   */
    static const double b2[3] = { 1.0, -2.0, 1.0 };
    static const double a2[3] = { 1.0, -1.99004745483398, 0.99007225036621 };
    biquad(x, tmp, n, b1, a1);
    biquad(tmp, y, n, b2, a2);
}

/* Loudness envelope: K-weight, then a centered sliding mean-square of width W
   samples, square-rooted, then peak-normalized to the loudest point. */
void loud_envelope(const double *x, double *env, int n, int W) {
    double *kw = malloc(n * sizeof(double));
    double *tmp = malloc(n * sizeof(double));
    k_weight(x, kw, tmp, n);

    int half = W >> 1;
    double peak = 0.0;
    for (int i = 0; i < n; i++) {
        int lo = i - half;       if (lo < 0) lo = 0;
        int hi = i + half + 1;   if (hi > n) hi = n;
        double sum = 0.0;
        for (int j = lo; j < hi; j++) sum += kw[j] * kw[j];
        env[i] = sqrt(sum / (hi - lo));
        if (env[i] > peak) peak = env[i];
    }
    if (peak > 0.0) for (int i = 0; i < n; i++) env[i] /= peak;

    free(kw); free(tmp);
}