PR #25202 opened by nmoinvaz
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25202
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25202.patch

`loudnorm` measures at 192 kHz, and every 100 ms `ebur128_calc_gating_block()` 
re-summed 400 ms of squared samples, while the short-term loudness re-summed 3 
s, so each sample was squared and summed about 50 times.

1. Keep the energy of each completed 100 ms block, so a window of whole blocks 
sums 4 or 30 values. Windows that do not end on a block boundary keep the 
current loop.
2. Hold the K-weighting filter state and coefficients in locals, since every 
store to `audio_data` could alias them. This commit is bit-exact.

Adding the block sums in a different order changes double results in the last 
bits. Over 52 s of stereo the float64 output differs from master by at most 
2.8e-14 relative, and 4 of 19.9M float32 samples differ by 1 ulp. The s16 
output and the measured I, TP, LRA and threshold are identical, also for 5.1, 
mono, 44.1 kHz and linear mode.

No FATE test covers `loudnorm`, and the `ebur128` filter has its own 
implementation in `f_ebur128.c`, which this does not touch.

| 52 s stereo, 48 kHz | master | this PR | time |
|---|---|---|---|
| `-af loudnorm` | 1.92 s | 0.72 s | -61% |

Measured on an i7-1185G7 with GCC 16.



>From b857eebac354f938b06d5a14c0dfffb25c012629 Mon Sep 17 00:00:00 2001
From: Nathan Moinvaziri <[email protected]>
Date: Sun, 18 Oct 2026 12:33:50 -0700
Subject: [PATCH 1/2] avfilter/ebur128: sum the energy of each 100 ms block
 once

Every 100 ms the gating block summed 400 ms of squared samples and the
short-term loudness summed 3 s, at the 192 kHz loudnorm runs at. Keeping
the energy of each 100 ms block makes loudnorm about 58% faster, though
reordering the sums can change double results in the last bits.
---
 libavfilter/ebur128.c | 47 ++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 46 insertions(+), 1 deletion(-)

diff --git a/libavfilter/ebur128.c b/libavfilter/ebur128.c
index 062099e206..60ff555683 100644
--- a/libavfilter/ebur128.c
+++ b/libavfilter/ebur128.c
@@ -97,6 +97,10 @@ struct FFEBUR128StateInternal {
     unsigned long window;
     /** Data pointer array for interleaved data */
     void **data_ptrs;
+    /** Energy of each 100ms block of audio_data, per channel. */
+    double *block_energy;
+    /** Frame of audio_data up to which block_energy is current. */
+    size_t block_energy_frame;
 };
 
 static AVOnce histogram_init = AV_ONCE_INIT;
@@ -257,6 +261,11 @@ FFEBUR128State *ff_ebur128_init(unsigned int channels,
         (double *) av_calloc(st->d->audio_data_frames,
                              st->channels * sizeof(*st->d->audio_data));
     CHECK_ERROR(!st->d->audio_data, 0, free_sample_peak)
+    st->d->block_energy =
+        av_calloc(st->d->audio_data_frames / st->d->samples_in_100ms,
+                  st->channels * sizeof(*st->d->block_energy));
+    CHECK_ERROR(!st->d->block_energy, 0, free_audio_data)
+    st->d->block_energy_frame = 0;
 
     ebur128_init_filter(st);
 
@@ -288,6 +297,7 @@ free_short_term_block_energy_histogram:
 free_block_energy_histogram:
     av_free(st->d->block_energy_histogram);
 free_audio_data:
+    av_free(st->d->block_energy);
     av_free(st->d->audio_data);
 free_sample_peak:
     av_free(st->d->sample_peak);
@@ -306,6 +316,7 @@ void ff_ebur128_destroy(FFEBUR128State ** st)
     av_free((*st)->d->block_energy_histogram);
     av_free((*st)->d->short_term_block_energy_histogram);
     av_free((*st)->d->audio_data);
+    av_free((*st)->d->block_energy);
     av_free((*st)->d->channel_map);
     av_free((*st)->d->sample_peak);
     av_free((*st)->d->data_ptrs);
@@ -388,6 +399,28 @@ static size_t find_histogram_index(double energy)
     return index_min;
 }
 
+/* Compute the energy of the 100ms blocks that filled up since the last call. 
*/
+static void ebur128_update_block_energy(FFEBUR128State *st)
+{
+    struct FFEBUR128StateInternal *d = st->d;
+    const size_t end = d->audio_data_index / st->channels % 
d->audio_data_frames;
+
+    while (d->block_energy_frame != end) {
+        const size_t start = d->block_energy_frame;
+        double *energy = d->block_energy + start / d->samples_in_100ms * 
st->channels;
+
+        for (size_t c = 0; c < st->channels; c++) {
+            double sum = 0.0;
+            if (d->channel_map[c] == FF_EBUR128_UNUSED)
+                continue;
+            for (size_t i = start; i < start + d->samples_in_100ms; i++)
+                sum += d->audio_data[i * st->channels + c] * d->audio_data[i * 
st->channels + c];
+            energy[c] = sum;
+        }
+        d->block_energy_frame = (start + d->samples_in_100ms) % 
d->audio_data_frames;
+    }
+}
+
 static void ebur128_calc_gating_block(FFEBUR128State * st,
                                       size_t frames_per_block,
                                       double *optional_output)
@@ -395,11 +428,22 @@ static void ebur128_calc_gating_block(FFEBUR128State * st,
     size_t i, c;
     double sum = 0.0;
     double channel_sum;
+    const size_t nb_blocks = st->d->audio_data_frames / 
st->d->samples_in_100ms;
+    const size_t end = st->d->audio_data_index / st->channels % 
st->d->audio_data_frames;
+    /* a window of whole blocks up to the last completed one sums their 
energies */
+    const int whole_blocks = !(frames_per_block % st->d->samples_in_100ms) &&
+                             end == st->d->block_energy_frame;
     for (c = 0; c < st->channels; ++c) {
         if (st->d->channel_map[c] == FF_EBUR128_UNUSED)
             continue;
         channel_sum = 0.0;
-        if (st->d->audio_data_index < frames_per_block * st->channels) {
+        if (whole_blocks) {
+            size_t b = end / st->d->samples_in_100ms;
+            for (i = 0; i < frames_per_block / st->d->samples_in_100ms; i++) {
+                b = (b ? b : nb_blocks) - 1;
+                channel_sum += st->d->block_energy[b * st->channels + c];
+            }
+        } else if (st->d->audio_data_index < frames_per_block * st->channels) {
             for (i = 0; i < st->d->audio_data_index / st->channels; ++i) {
                 channel_sum += st->d->audio_data[i * st->channels + c] *
                     st->d->audio_data[i * st->channels + c];
@@ -467,6 +511,7 @@ static void 
ebur128_add_frames_planar_##type(FFEBUR128State* st, const type** sr
             src_index += st->d->needed_frames * stride;                        
        \
             frames -= st->d->needed_frames;                                    
        \
             st->d->audio_data_index += st->d->needed_frames * st->channels;    
        \
+            ebur128_update_block_energy(st);                                   
        \
             /* calculate the new gating block */                               
        \
             if ((st->mode & FF_EBUR128_MODE_I) == FF_EBUR128_MODE_I) {         
        \
                 ebur128_calc_gating_block(st, st->d->samples_in_100ms * 4, 
NULL);      \
-- 
2.54.0


>From 8107106b95d2e42b2693ad1860937147197fa3df Mon Sep 17 00:00:00 2001
From: Nathan Moinvaziri <[email protected]>
Date: Sun, 18 Oct 2026 12:36:06 -0700
Subject: [PATCH 2/2] avfilter/ebur128: keep the K-weighting filter state in
 locals

Each output store could alias the filter coefficients and state, so the
compiler reloaded and stored them for every sample. Holding them in
locals keeps the output identical and makes loudnorm another 7% faster.
---
 libavfilter/ebur128.c | 36 ++++++++++++++++++------------------
 1 file changed, 18 insertions(+), 18 deletions(-)

diff --git a/libavfilter/ebur128.c b/libavfilter/ebur128.c
index 60ff555683..0786ac7030 100644
--- a/libavfilter/ebur128.c
+++ b/libavfilter/ebur128.c
@@ -351,27 +351,27 @@ static void ebur128_filter_##type(FFEBUR128State* st, 
const type** srcs,
         int ci = st->d->channel_map[c] - 1;                                    
    \
         if (ci < 0) continue;                                                  
    \
         else if (ci == FF_EBUR128_DUAL_MONO - 1) ci = 0; /*dual mono */        
    \
+        /* stores to audio_data could alias the filter, so keep it in locals 
*/    \
+        const double a1 = st->d->a[1], a2 = st->d->a[2], a3 = st->d->a[3], a4 
= st->d->a[4]; \
+        const double b0 = st->d->b[0], b1 = st->d->b[1], b2 = st->d->b[2];     
    \
+        const double b3 = st->d->b[3], b4 = st->d->b[4];                       
    \
+        double v0 = st->d->v[ci][0], v1 = st->d->v[ci][1], v2 = 
st->d->v[ci][2];   \
+        double v3 = st->d->v[ci][3], v4 = st->d->v[ci][4];                     
    \
         for (i = 0; i < frames; ++i) {                                         
    \
-            st->d->v[ci][0] = (double) (srcs[c][src_index + i * stride] / 
scaling_factor) \
-                         - st->d->a[1] * st->d->v[ci][1]                       
    \
-                         - st->d->a[2] * st->d->v[ci][2]                       
    \
-                         - st->d->a[3] * st->d->v[ci][3]                       
    \
-                         - st->d->a[4] * st->d->v[ci][4];                      
    \
+            v0 = (double) (srcs[c][src_index + i * stride] / scaling_factor)   
    \
+                 - a1 * v1 - a2 * v2 - a3 * v3 - a4 * v4;                      
    \
             audio_data[i * st->channels + c] =                                 
    \
-                           st->d->b[0] * st->d->v[ci][0]                       
    \
-                         + st->d->b[1] * st->d->v[ci][1]                       
    \
-                         + st->d->b[2] * st->d->v[ci][2]                       
    \
-                         + st->d->b[3] * st->d->v[ci][3]                       
    \
-                         + st->d->b[4] * st->d->v[ci][4];                      
    \
-            st->d->v[ci][4] = st->d->v[ci][3];                                 
    \
-            st->d->v[ci][3] = st->d->v[ci][2];                                 
    \
-            st->d->v[ci][2] = st->d->v[ci][1];                                 
    \
-            st->d->v[ci][1] = st->d->v[ci][0];                                 
    \
+                   b0 * v0 + b1 * v1 + b2 * v2 + b3 * v3 + b4 * v4;            
    \
+            v4 = v3;                                                           
    \
+            v3 = v2;                                                           
    \
+            v2 = v1;                                                           
    \
+            v1 = v0;                                                           
    \
         }                                                                      
    \
-        st->d->v[ci][4] = fabs(st->d->v[ci][4]) < DBL_MIN ? 0.0 : 
st->d->v[ci][4]; \
-        st->d->v[ci][3] = fabs(st->d->v[ci][3]) < DBL_MIN ? 0.0 : 
st->d->v[ci][3]; \
-        st->d->v[ci][2] = fabs(st->d->v[ci][2]) < DBL_MIN ? 0.0 : 
st->d->v[ci][2]; \
-        st->d->v[ci][1] = fabs(st->d->v[ci][1]) < DBL_MIN ? 0.0 : 
st->d->v[ci][1]; \
+        st->d->v[ci][0] = v0;                                                  
    \
+        st->d->v[ci][4] = fabs(v4) < DBL_MIN ? 0.0 : v4;                       
    \
+        st->d->v[ci][3] = fabs(v3) < DBL_MIN ? 0.0 : v3;                       
    \
+        st->d->v[ci][2] = fabs(v2) < DBL_MIN ? 0.0 : v2;                       
    \
+        st->d->v[ci][1] = fabs(v1) < DBL_MIN ? 0.0 : v1;                       
    \
     }                                                                          
    \
 }
 EBUR128_FILTER(double, 1.0)
-- 
2.54.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to