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]
