[PR] avcodec/ac3dec_fixed: fix dither at exponent 24 (PR #24015)

AYOUB NABIL BOUBAGRAT via ffmpeg-devel <[email protected]> Wed, 05 Aug 2026 00:47:14 -0000
Newsgroups gmane.comp.video.ffmpeg.devel
Message-ID <178589083462.59.14039784747397778207@29965ddac10e>
PR #24015 opened by AYOUB NABIL BOUBAGRAT (ayoubnabil)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24015
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24015.patch

the fixed decoder discarded fractional coefficient data before the imdct,
causing low-level coefficients to be truncated at large exponents. 
dithered bap 0 coefficients at exponent 24 were consequently quantized with a negative bias.
keep ac-3 coefficients in q2, compensate during windowing, and round the affected dither symmetrically. 
keep the forced e-ac-3 path in q0.
add a synthetic fate test covering the corrected output.
tested with fate-ac3 on x86-64, x86-32, and ubsan.


>From 99340678be03292725894380a4e7881b6e4b7c2c Mon Sep 17 00:00:00 2001
From: Ayoub Nabil <[email protected]>
Date: Wed, 5 Aug 2026 02:45:10 +0200
Subject: [PATCH] avcodec/ac3dec_fixed: fix dither at exponent 24

the fixed decoder discarded fractional coefficient data before the imdct, causing low-level coefficients to be truncated at large exponents. dithered bap 0 coefficients at exponent 24 were consequently quantized with a negative bias.

keep ac-3 coefficients in q2, compensate during windowing, and round the affected dither symmetrically. keep the forced e-ac-3 path in q0.

add a synthetic fate test covering the corrected output.
---
 libavcodec/ac3dec.c             | 70 +++++++++++++++++++++++++++++----
 libavcodec/ac3dec_fixed.c       | 54 +++++++++++++++++++++++++
 tests/fate/ac3.mak              | 24 ++++++++++-
 tests/ref/fate/ac3-fixed-dexp24 |  6 +++
 4 files changed, 146 insertions(+), 8 deletions(-)
 create mode 100644 tests/ref/fate/ac3-fixed-dexp24

diff --git a/libavcodec/ac3dec.c b/libavcodec/ac3dec.c
index 6f7306ea59..99df08c319 100644
--- a/libavcodec/ac3dec.c
+++ b/libavcodec/ac3dec.c
@@ -390,6 +390,27 @@ typedef struct mant_groups {
     int b4;
 } mant_groups;
 
+static av_always_inline int dequantize_coeff(int mantissa, int exponent,
+                                             int coeff_bits)
+{
+#if USE_FIXED
+    return (mantissa * (1 << coeff_bits)) >> exponent;
+#else
+    (void)coeff_bits;
+    return mantissa >> exponent;
+#endif
+}
+
+#if USE_FIXED
+static av_always_inline int dequantize_dexp24_dither(int mantissa)
+{
+    int scaled = mantissa * (1 << AC3_FIXED_COEFF_BITS);
+    int round  = 1 << (AC3_FIXED_EXPONENT_MAX - 1);
+
+    return (scaled + round - (scaled < 0)) >> AC3_FIXED_EXPONENT_MAX;
+}
+#endif
+
 /**
  * Decode the transform coefficients for a particular channel
  * reference: Section 7.3 Quantization and Decoding of Mantissas
@@ -402,6 +423,11 @@ static void ac3_decode_transform_coeffs_ch(AC3DecodeContext *s, int ch_index, ma
     int8_t *exps   = s->dexps[ch_index];
     int32_t *coeffs = s->fixed_coeffs[ch_index];
     int dither     = (ch_index == CPL_CH) || s->dither_flag[ch_index];
+#if USE_FIXED
+    int coeff_bits = fixed_coeff_bits(s);
+#else
+    int coeff_bits = 0;
+#endif
     GetBitContext *gbc = &s->gbc;
     int freq;
 
@@ -411,10 +437,19 @@ static void ac3_decode_transform_coeffs_ch(AC3DecodeContext *s, int ch_index, ma
         switch (bap) {
         case 0:
             /* random noise with approximate range of -0.707 to 0.707 */
-            if (dither)
+            if (dither) {
                 mantissa = (((av_lfg_get(&s->dith_state)>>8)*181)>>8) - 5931008;
-            else
+#if USE_FIXED
+                /* At dexp 24 the dither is below half a Q0 step. Keep two
+                 * fractional bits so it is not truncated to -1 or 0. */
+                if (coeff_bits && exps[freq] == AC3_FIXED_EXPONENT_MAX) {
+                    coeffs[freq] = dequantize_dexp24_dither(mantissa);
+                    continue;
+                }
+#endif
+            } else {
                 mantissa = 0;
+            }
             break;
         case 1:
             if (m->b1) {
@@ -466,7 +501,7 @@ static void ac3_decode_transform_coeffs_ch(AC3DecodeContext *s, int ch_index, ma
             mantissa = (unsigned)get_sbits(gbc, ff_ac3_quantization_tab[bap]) << (24 - ff_ac3_quantization_tab[bap]);
             break;
         }
-        coeffs[freq] = mantissa >> exps[freq];
+        coeffs[freq] = dequantize_coeff(mantissa, exps[freq], coeff_bits);
     }
 }
 
@@ -500,7 +535,8 @@ static inline void decode_transform_coeffs_ch(AC3DecodeContext *s, int blk,
         if (CONFIG_EAC3_DECODER && !blk)
             ff_eac3_decode_transform_coeffs_aht_ch(s, ch);
         for (bin = s->start_freq[ch]; bin < s->end_freq[ch]; bin++) {
-            s->fixed_coeffs[ch][bin] = s->pre_mantissa[ch][bin][blk] >> s->dexps[ch][bin];
+            s->fixed_coeffs[ch][bin] = dequantize_coeff(
+                s->pre_mantissa[ch][bin][blk], s->dexps[ch][bin], 0);
         }
     }
 }
@@ -571,6 +607,9 @@ static void do_rematrixing(AC3DecodeContext *s)
 static inline void do_imdct(AC3DecodeContext *s, int channels, int offset)
 {
     int ch;
+#if USE_FIXED
+    int window_bits = 8 + fixed_coeff_bits(s);
+#endif
 
     for (ch = 1; ch <= channels; ch++) {
         if (s->block_switch[ch]) {
@@ -581,7 +620,7 @@ static inline void do_imdct(AC3DecodeContext *s, int channels, int offset)
             s->tx_fn_128(s->tx_128, s->tmp_output, x, sizeof(INTFLOAT));
 #if USE_FIXED
             s->fdsp->vector_fmul_window_scaled(s->outptr[ch - 1], s->delay[ch - 1 + offset],
-                                       s->tmp_output, s->window, 128, 8);
+                                       s->tmp_output, s->window, 128, window_bits);
 #else
             s->fdsp->vector_fmul_window(s->outptr[ch - 1], s->delay[ch - 1 + offset],
                                        s->tmp_output, s->window, 128);
@@ -593,7 +632,7 @@ static inline void do_imdct(AC3DecodeContext *s, int channels, int offset)
             s->tx_fn_256(s->tx_256, s->tmp_output, s->transform_coeffs[ch], sizeof(INTFLOAT));
 #if USE_FIXED
             s->fdsp->vector_fmul_window_scaled(s->outptr[ch - 1], s->delay[ch - 1 + offset],
-                                       s->tmp_output, s->window, 128, 8);
+                                       s->tmp_output, s->window, 128, window_bits);
 #else
             s->fdsp->vector_fmul_window(s->outptr[ch - 1], s->delay[ch - 1 + offset],
                                        s->tmp_output, s->window, 128);
@@ -1287,7 +1326,11 @@ static int decode_audio_block(AC3DecodeContext *s, int blk, int offset)
             gain = s->dynamic_range[audio_channel];
 
 #if USE_FIXED
-        scale_coefs(s->transform_coeffs[ch], s->fixed_coeffs[ch], gain, 256);
+        if (fixed_coeff_bits(s))
+            scale_coefs_q2(s->transform_coeffs[ch], s->fixed_coeffs[ch], gain,
+                           256);
+        else
+            scale_coefs(s->transform_coeffs[ch], s->fixed_coeffs[ch], gain, 256);
 #else
         if (s->target_level != 0)
           gain = gain * s->level_gain[audio_channel];
@@ -1356,6 +1399,9 @@ static int ac3_decode_frame(AVCodecContext *avctx, AVFrame *frame,
     AC3DecodeContext *s = avctx->priv_data;
     int blk, ch, err, offset, ret;
     int i;
+#if USE_FIXED
+    int previous_coeff_bits;
+#endif
     int skip = 0, got_independent_frame = 0;
     const uint8_t *channel_map;
     uint8_t extended_channel_map[EAC3_MAX_CHANNELS];
@@ -1390,6 +1436,9 @@ static int ac3_decode_frame(AVCodecContext *avctx, AVFrame *frame,
 
     buf = s->input_buffer;
 dependent_frame:
+#if USE_FIXED
+    previous_coeff_bits = fixed_coeff_bits(s);
+#endif
     /* initialize the GetBitContext with the start of valid AC-3 Frame */
     if ((ret = init_get_bits8(&s->gbc, buf, buf_size)) < 0)
         return ret;
@@ -1397,6 +1446,13 @@ dependent_frame:
     /* parse the syncinfo */
     err = parse_frame_header(s);
 
+#if USE_FIXED
+    /* Do not mix Q0 and Q2 overlap samples if a malformed or explicitly
+     * forced stream switches between E-AC-3 and AC-3. */
+    if (!err && previous_coeff_bits != fixed_coeff_bits(s))
+        memset(s->delay, 0, sizeof(s->delay));
+#endif
+
     if (err) {
         switch (err) {
         case AC3_PARSE_ERROR_SYNC:
diff --git a/libavcodec/ac3dec_fixed.c b/libavcodec/ac3dec_fixed.c
index 285553c22e..5fb07dc58f 100644
--- a/libavcodec/ac3dec_fixed.c
+++ b/libavcodec/ac3dec_fixed.c
@@ -55,6 +55,17 @@
 
 #include "ac3dec.h"
 
+/* Keep two fractional bits in fixed-point transform coefficients. */
+#define AC3_FIXED_COEFF_BITS 2
+#define AC3_FIXED_EXPONENT_MAX 24
+
+static av_always_inline int fixed_coeff_bits(const AC3DecodeContext *s)
+{
+    /* ac3_fixed normally decodes AC-3. Keep Q0 when it is explicitly forced
+     * to decode E-AC-3, whose AHT coefficient bounds are different. */
+    return s->eac3 ? 0 : AC3_FIXED_COEFF_BITS;
+}
+
 static const int end_freq_inv_tab[8] =
 {
     50529027, 44278013, 39403370, 32292987, 27356480, 23729101, 20951060, 18755316
@@ -124,6 +135,49 @@ static void scale_coefs (
     }
 }
 
+static void scale_coefs_q2(int32_t *dst, const int32_t *src, int dynrng,
+                           int len)
+{
+    int i, shift;
+    int mul;
+
+    mul = (dynrng & 0x1f) + 0x20;
+    shift = 4 - (sign_extend(dynrng, 9) >> 5);
+
+    /* AC-3 mantissas have magnitude at most 2^23, hence Q2 coefficients
+     * have magnitude at most 2^25. Coupling uses MULH(coeff * 2^4, coord)
+     * with coord < 2^31, so coupled coefficients are below 2^28.
+     * Rematrixing can at most double them, keeping src below 2^29. */
+    if (dynrng == 32) {
+        for (i = 0; i < len; i++)
+            dst[i] = src[i] * 4;
+        return;
+    }
+
+    if (shift >= 4) {
+        const int round = 1 << (shift - 1);
+        const int unit  = 1 << shift;
+
+        /* With shift >= 4, quotient * mul is below 2^25 * 63. Splitting
+         * quotient and remainder therefore keeps both products in int32_t. */
+        for (i = 0; i < len; i++) {
+            int quotient  = src[i] >> shift;
+            int remainder = src[i] - quotient * unit;
+
+            dst[i] = quotient * mul + ((remainder * mul + round) >> shift);
+        }
+    } else if (shift > 0) {
+        const int round = 1 << (shift - 1);
+
+        for (i = 0; i < len; i++)
+            dst[i] = av_clipl_int32(((int64_t)src[i] * mul + round) >> shift);
+    } else {
+        mul <<= -shift;
+        for (i = 0; i < len; i++)
+            dst[i] = av_clipl_int32((int64_t)src[i] * mul);
+    }
+}
+
 /**
  * Downmix samples from original signal to stereo or mono (this is for 16-bit samples
  * and fixed point decoder - original (for 32-bit samples) is in ac3dsp.c).
diff --git a/tests/fate/ac3.mak b/tests/fate/ac3.mak
index 875fa12418..f2497ec044 100644
--- a/tests/fate/ac3.mak
+++ b/tests/fate/ac3.mak
@@ -102,11 +102,33 @@ fate-ac3-fixed-encode-3: tests/data/asynth-44100-6.wav
 fate-ac3-fixed-encode-3: SRC = $(TARGET_PATH)/tests/data/asynth-44100-6.wav
 fate-ac3-fixed-encode-3: CMD = framecrc -i $(SRC) -c:a ac3_fixed -flags2 +fixed_frame_size -ab 256k -af aresample,atrim=start_sample=0:end_sample=12096
 
+# With coupling and rematrixing disabled, this produces bap=0, dexp=24 bins
+# whose dither affects the decoded output.
+tests/data/fate/ac3-fixed-dexp24.ac3: TAG = GEN
+tests/data/fate/ac3-fixed-dexp24.ac3: ffmpeg$(PROGSSUF)$(EXESUF) tests/data/asynth-44100-2.wav | tests/data/fate
+	$(M)$(TARGET_EXEC) $(TARGET_PATH)/ffmpeg$(PROGSSUF)$(EXESUF) \
+	-hide_banner -loglevel error -nostdin \
+	-i $(TARGET_PATH)/tests/data/asynth-44100-2.wav \
+	-c:a ac3_fixed -b:a 192k \
+	-channel_coupling 0 -stereo_rematrixing 0 -flags +bitexact \
+	-frames:a 173 -f ac3 -y $(TARGET_PATH)/$@
+
+FATE_AC3_FIXED_DEXP24-$(call ALLYES, FFMPEG WAV_DEMUXER ARESAMPLE_FILTER ATRIM_FILTER \
+                                    AC3_FIXED_ENCODER FRAMECRC_MUXER \
+                                    AC3_MUXER AC3_DEMUXER AC3_FIXED_DECODER \
+                                    PCM_S16LE_DECODER PCM_S16LE_ENCODER \
+                                    FILE_PROTOCOL) += fate-ac3-fixed-dexp24
+fate-ac3-fixed-dexp24: tests/data/fate/ac3-fixed-dexp24.ac3
+fate-ac3-fixed-dexp24: CMD = framecrc -auto_conversion_filters -c ac3_fixed \
+                                   -i $(TARGET_PATH)/tests/data/fate/ac3-fixed-dexp24.ac3 \
+                                   -af atrim=start_sample=264192
+
 FATE_EAC3-$(call ALLYES, EAC3_DEMUXER EAC3_MUXER EAC3_CORE_BSF) += fate-eac3-core-bsf
 fate-eac3-core-bsf: CMD = md5pipe -i $(TARGET_SAMPLES)/eac3/the_great_wall_7.1.eac3 -c:a copy -bsf:a eac3_core -fflags +bitexact -f eac3
 fate-eac3-core-bsf: CMP = oneline
 fate-eac3-core-bsf: REF = b704bf851e99b7442e9bed368b60e6ca
 
 FATE_SAMPLES_AVCONV += $(FATE_AC3-yes) $(FATE_EAC3-yes)
+FATE_FFMPEG += $(FATE_AC3_FIXED_DEXP24-yes)
 
-fate-ac3: $(FATE_AC3-yes) $(FATE_EAC3-yes)
+fate-ac3: $(FATE_AC3-yes) $(FATE_EAC3-yes) $(FATE_AC3_FIXED_DEXP24-yes)
diff --git a/tests/ref/fate/ac3-fixed-dexp24 b/tests/ref/fate/ac3-fixed-dexp24
new file mode 100644
index 0000000000..857d6c20ec
--- /dev/null
+++ b/tests/ref/fate/ac3-fixed-dexp24
@@ -0,0 +1,6 @@
+#tb 0: 1/44100
+#media_type 0: audio
+#codec_id 0: pcm_s16le
+#sample_rate 0: 44100
+#channel_layout_name 0: stereo
+0,     264134,     264134,     1536,     6144, 0xff5a9359
-- 
2.52.0

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