[PR] avcodec/ac3enc: preserve strongly out-of-phase coupling bands (PR #24253)

AYOUB NABIL BOUBAGRAT via ffmpeg-devel <[email protected]>
Newsgroups gmane.comp.video.ffmpeg.devel
Message-ID <[email protected]>
PR #24253 opened by AYOUB NABIL BOUBAGRAT (ayoubnabil)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24253
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24253.patch

the encoder always formed the stereo coupling channel as L+R and disabled phsflginu, which could cancel strongly out-of-phase bands before quantization. use an L-R carrier and signal the phase flag only when difference energy dominates in every coupling block, while quiet and ambiguous bands retain the existing L+R path. this reduces the multitone fate stddev from about 1158 to 588.



>From 72f7e7d34353ecda31f6ed139ecb9ccd02e53eaa Mon Sep 17 00:00:00 2001
From: Ayoub Nabil Boubagrat <[email protected]>
Date: Mon, 24 Aug 2026 00:30:08 +0200
Subject: [PATCH] avcodec/ac3enc: preserve strongly out-of-phase coupling bands

the encoder always formed the stereo coupling channel as L+R and disabled phsflginu, which could cancel strongly out-of-phase bands before quantization. use an L-R carrier and signal the phase flag only when difference energy dominates in every coupling block, while quiet and ambiguous bands retain the existing L+R path. this reduces the multitone fate stddev from about 1158 to 588.

Signed-off-by: Ayoub Nabil Boubagrat <[email protected]>
---
 libavcodec/ac3enc.c          | 16 +++++++-
 libavcodec/ac3enc.h          |  2 +
 libavcodec/ac3enc_template.c | 77 +++++++++++++++++++++++++++++++++++-
 tests/fate/ac3.mak           | 29 +++++++++++++-
 4 files changed, 120 insertions(+), 4 deletions(-)

diff --git a/libavcodec/ac3enc.c b/libavcodec/ac3enc.c
index 0482d9ee58..903ba66ee0 100644
--- a/libavcodec/ac3enc.c
+++ b/libavcodec/ac3enc.c
@@ -1183,16 +1183,22 @@ static void count_frame_bits(AC3EncodeContext *s)
 
         /* coupling coordinates */
         if (block->cpl_in_use) {
+            int cpl_coords_exist = 0;
+
             for (ch = 1; ch <= s->fbw_channels; ch++) {
                 if (block->channel_in_cpl[ch]) {
                     if (!s->eac3 || block->new_cpl_coords[ch] != 2)
                         frame_bits++;
                     if (block->new_cpl_coords[ch]) {
+                        cpl_coords_exist = 1;
                         frame_bits += 2;
                         frame_bits += (4 + 4) * s->num_cpl_bands;
                     }
                 }
             }
+            if (s->channel_mode == AC3_CHMODE_STEREO &&
+                s->phase_flags_in_use && cpl_coords_exist)
+                frame_bits += s->num_cpl_bands;
         }
 
         /* stereo rematrixing */
@@ -1725,7 +1731,7 @@ static void output_audio_block(AC3EncodeContext *s, PutBitContext *pb, int blk)
                     put_bits(pb, 1, block->channel_in_cpl[ch]);
             }
             if (s->channel_mode == AC3_CHMODE_STEREO)
-                put_bits(pb, 1, 0); /* phase flags in use */
+                put_bits(pb, 1, s->phase_flags_in_use);
             start_sub = (s->start_freq[CPL_CH] - 37) / 12;
             end_sub   = (s->cpl_end_freq       - 37) / 12;
             put_bits(pb, 4, start_sub);
@@ -1742,11 +1748,14 @@ static void output_audio_block(AC3EncodeContext *s, PutBitContext *pb, int blk)
 
     /* coupling coordinates */
     if (block->cpl_in_use) {
+        int cpl_coords_exist = 0;
+
         for (ch = 1; ch <= s->fbw_channels; ch++) {
             if (block->channel_in_cpl[ch]) {
                 if (!s->eac3 || block->new_cpl_coords[ch] != 2)
                     put_bits(pb, 1, block->new_cpl_coords[ch]);
                 if (block->new_cpl_coords[ch]) {
+                    cpl_coords_exist = 1;
                     put_bits(pb, 2, block->cpl_master_exp[ch]);
                     for (bnd = 0; bnd < s->num_cpl_bands; bnd++) {
                         put_bits(pb, 4, block->cpl_coord_exp [ch][bnd]);
@@ -1755,6 +1764,11 @@ static void output_audio_block(AC3EncodeContext *s, PutBitContext *pb, int blk)
                 }
             }
         }
+        if (s->channel_mode == AC3_CHMODE_STEREO &&
+            s->phase_flags_in_use && cpl_coords_exist) {
+            for (bnd = 0; bnd < s->num_cpl_bands; bnd++)
+                put_bits(pb, 1, s->phase_flags[bnd]);
+        }
     }
 
     /* stereo rematrixing */
diff --git a/libavcodec/ac3enc.h b/libavcodec/ac3enc.h
index 3e92af17f0..e63867d0e7 100644
--- a/libavcodec/ac3enc.h
+++ b/libavcodec/ac3enc.h
@@ -213,6 +213,8 @@ typedef struct AC3EncodeContext {
 
     int cpl_on;                             ///< coupling turned on for this frame
     int cpl_enabled;                        ///< coupling enabled for all frames
+    int phase_flags_in_use;                 ///< coupling phase flags are in use
+    uint8_t phase_flags[AC3_MAX_CPL_BANDS]; ///< coupling phase flags                   (phsflg)
     int num_cpl_subbands;                   ///< number of coupling subbands            (ncplsubnd)
     int num_cpl_bands;                      ///< number of coupling bands               (ncplbnd)
     uint8_t cpl_band_sizes[AC3_MAX_CPL_BANDS];  ///< number of coeffs in each coupling band
diff --git a/libavcodec/ac3enc_template.c b/libavcodec/ac3enc_template.c
index 5331b45cb9..6fb2e70545 100644
--- a/libavcodec/ac3enc_template.c
+++ b/libavcodec/ac3enc_template.c
@@ -44,6 +44,10 @@
 #define RENAME(element) element ## _fixed
 #endif
 
+/* power ratios for the -42 dB band floor and the 12 dB difference margin. */
+#define PHASE_BAND_ENERGY_DENOMINATOR (1 << 14)
+#define PHASE_DIFF_ENERGY_FACTOR       (1 << 4)
+
 /*
  * Apply the MDCT to input samples to generate frequency coefficients.
  * This applies the KBD window and normalizes the input to reduce precision
@@ -97,6 +101,7 @@ static void apply_channel_coupling(AC3EncodeContext *s)
     CoefSumType energy[AC3_MAX_BLOCKS][AC3_MAX_CHANNELS][16] = {{{0}}};
     int cpl_start, num_cpl_coefs;
 
+    s->phase_flags_in_use = 0;
     memset(cpl_coords,       0, AC3_MAX_BLOCKS * sizeof(*cpl_coords));
 #if AC3ENC_FLOAT
     memset(fixed_cpl_coords, 0, AC3_MAX_BLOCKS * sizeof(*cpl_coords));
@@ -108,6 +113,59 @@ static void apply_channel_coupling(AC3EncodeContext *s)
     num_cpl_coefs = FFALIGN(s->num_cpl_subbands * 12 + 1, 32);
     cpl_start     = FFMIN(256, cpl_start + num_cpl_coefs) - num_cpl_coefs;
 
+    if (s->channel_mode == AC3_CHMODE_STEREO) {
+        uint8_t phase_flags[AC3_MAX_CPL_BANDS];
+        int cpl_blocks = 0;
+
+        /* use a single phase strategy for the frame. a difference carrier is
+         * selected only when it has at least 12 dB more energy and the band
+         * is within 42 dB of the coded channel energy in every coupling
+         * block. */
+        memset(phase_flags, 1, s->num_cpl_bands);
+        for (blk = 0; blk < s->num_blocks; blk++) {
+            AC3Block *block = &s->blocks[blk];
+            CoefSumType sum[AC3_MAX_CPL_BANDS][4];
+            /* the DSP also returns sum and difference energy in slots 2/3. */
+            CoefSumType block_energy[4];
+            CoefSumType max_energy;
+
+            if (!block->cpl_in_use)
+                continue;
+            cpl_blocks++;
+            sum_square_butterfly(s, block_energy,
+                                 block->mdct_coef[1], block->mdct_coef[2],
+                                 s->start_freq[CPL_CH]);
+            i = s->start_freq[CPL_CH];
+            for (bnd = 0; bnd < s->num_cpl_bands; bnd++) {
+                sum_square_butterfly(s, sum[bnd],
+                                     block->mdct_coef[1] + i,
+                                     block->mdct_coef[2] + i,
+                                     s->cpl_band_sizes[bnd]);
+                /* reused by the coupling coordinate calculation below. */
+                energy[blk][1][bnd] = sum[bnd][0];
+                energy[blk][2][bnd] = sum[bnd][1];
+                block_energy[0] += sum[bnd][0];
+                block_energy[1] += sum[bnd][1];
+                i += s->cpl_band_sizes[bnd];
+            }
+            max_energy = FFMAX(block_energy[0], block_energy[1]);
+            for (bnd = 0; bnd < s->num_cpl_bands; bnd++) {
+                CoefSumType band_energy = FFMAX(sum[bnd][0], sum[bnd][1]);
+                int significant = band_energy >
+                    max_energy / PHASE_BAND_ENERGY_DENOMINATOR;
+
+                phase_flags[bnd] &= significant &&
+                    sum[bnd][3] / PHASE_DIFF_ENERGY_FACTOR > sum[bnd][2];
+            }
+        }
+        for (bnd = 0; bnd < s->num_cpl_bands; bnd++) {
+            int phase = phase_flags[bnd] && cpl_blocks;
+
+            s->phase_flags[bnd] = phase;
+            s->phase_flags_in_use |= phase;
+        }
+    }
+
     /* calculate coupling channel from fbw channels */
     for (blk = 0; blk < s->num_blocks; blk++) {
         AC3Block *block = &s->blocks[blk];
@@ -123,6 +181,20 @@ static void apply_channel_coupling(AC3EncodeContext *s)
                 cpl_coef[i] += ch_coef[i];
         }
 
+        if (s->channel_mode == AC3_CHMODE_STEREO) {
+            CoefType *left  = block->mdct_coef[1];
+            CoefType *right = block->mdct_coef[2];
+
+            i = s->start_freq[CPL_CH];
+            for (bnd = 0; bnd < s->num_cpl_bands; bnd++) {
+                if (s->phase_flags[bnd]) {
+                    for (j = 0; j < s->cpl_band_sizes[bnd]; j++)
+                        block->mdct_coef[CPL_CH][i + j] = left[i + j] - right[i + j];
+                }
+                i += s->cpl_band_sizes[bnd];
+            }
+        }
+
         /* coefficients must be clipped in order to be encoded */
         clip_coefficients(&s->adsp, cpl_coef, num_cpl_coefs);
     }
@@ -133,7 +205,10 @@ static void apply_channel_coupling(AC3EncodeContext *s)
     i = s->start_freq[CPL_CH];
     while (i < s->cpl_end_freq) {
         int band_size = s->cpl_band_sizes[bnd];
-        for (ch = CPL_CH; ch <= s->fbw_channels; ch++) {
+        /* stereo channel energies were filled during phase analysis above. */
+        int last_ch = s->channel_mode == AC3_CHMODE_STEREO ?
+                      CPL_CH : s->fbw_channels;
+        for (ch = CPL_CH; ch <= last_ch; ch++) {
             for (blk = 0; blk < s->num_blocks; blk++) {
                 AC3Block *block = &s->blocks[blk];
                 if (!block->cpl_in_use || (ch > CPL_CH && !block->channel_in_cpl[ch]))
diff --git a/tests/fate/ac3.mak b/tests/fate/ac3.mak
index 2eb85f63b8..605a411799 100644
--- a/tests/fate/ac3.mak
+++ b/tests/fate/ac3.mak
@@ -103,6 +103,31 @@ FATE_AC3-$(call ENCDEC, AC3, MP4 MOV, WAV_MUXER WAV_DEMUXER ARESAMPLE_FILTER PCM
 fate-ac3-encode: CMD = enc_dec_pcm mp4 wav s16le $(subst $(SAMPLES),$(TARGET_SAMPLES),$(REF)) -c:a ac3 -b:a 128k
 fate-ac3-encode: CMP_TARGET = 404.53
 
+# coupling must not cancel persistent out-of-phase stereo content.
+AC3_PHASE_EXPR = 0.025*(sin(16000*PI*t)+sin(20000*PI*t)+sin(24000*PI*t)+sin(28000*PI*t))
+tests/data/fate/ac3-phase.wav: TAG = GEN
+tests/data/fate/ac3-phase.wav: ffmpeg$(PROGSSUF)$(EXESUF) | tests/data/fate
+	$(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin -f lavfi \
+	-i "aevalsrc=$(AC3_PHASE_EXPR)|-$(AC3_PHASE_EXPR):s=48000:d=1" \
+	-c:a pcm_s16le -y $(TARGET_PATH)/$@ 2>/dev/null
+
+AC3_PHASE_DEPS = FFMPEG LAVFI_INDEV AEVALSRC_FILTER ARESAMPLE_FILTER \
+                 MP4_MUXER MOV_DEMUXER WAV_MUXER WAV_DEMUXER \
+                 PCM_S16LE_ENCODER FILE_PROTOCOL PIPE_PROTOCOL
+FATE_AC3_PHASE-$(call ALLYES, $(AC3_PHASE_DEPS) AC3_ENCODER AC3_DECODER) += fate-ac3-phase
+FATE_AC3_PHASE-$(call ALLYES, $(AC3_PHASE_DEPS) AC3_FIXED_ENCODER AC3_DECODER) += fate-ac3-fixed-phase
+FATE_AC3_PHASE-$(call ALLYES, $(AC3_PHASE_DEPS) EAC3_ENCODER EAC3_DECODER) += fate-eac3-phase
+# without phase restoration the measured stddev is about 1158.
+fate-ac3-phase: CMD = enc_dec_pcm mp4 wav s16le $(TARGET_PATH)/tests/data/fate/ac3-phase.wav -c:a ac3 -b:a 128k
+fate-ac3-phase: CMP_TARGET = 588.4
+fate-ac3-fixed-phase: CMD = enc_dec_pcm mp4 wav s16le $(TARGET_PATH)/tests/data/fate/ac3-phase.wav -c:a ac3_fixed -b:a 128k
+fate-ac3-fixed-phase: CMP_TARGET = 588.4
+fate-eac3-phase: CMD = enc_dec_pcm mp4 wav s16le $(TARGET_PATH)/tests/data/fate/ac3-phase.wav -c:a eac3 -b:a 128k
+fate-eac3-phase: CMP_TARGET = 588.4
+fate-ac3-phase fate-ac3-fixed-phase fate-eac3-phase: tests/data/fate/ac3-phase.wav
+fate-ac3-phase fate-ac3-fixed-phase fate-eac3-phase: CMP = stddev
+fate-ac3-phase fate-ac3-fixed-phase fate-eac3-phase: FUZZ = 2
+fate-ac3-phase fate-ac3-fixed-phase fate-eac3-phase: REF = tests/data/fate/ac3-phase.wav
 
 FATE_EAC3-$(call ENCDEC, EAC3, MP4 MOV, WAV_MUXER WAV_DEMUXER ARESAMPLE_FILTER PCM_S16LE_ENCODER PIPE_PROTOCOL) += fate-eac3-encode
 fate-eac3-encode: CMD = enc_dec_pcm mp4 wav s16le $(subst $(SAMPLES),$(TARGET_SAMPLES),$(REF)) -c:a eac3 -b:a 128k
@@ -189,6 +214,6 @@ 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_DITHER-yes) $(FATE_AC3_FIXED_DEXP24-yes)
+FATE_FFMPEG += $(FATE_AC3_DITHER-yes) $(FATE_AC3_FIXED_DEXP24-yes) $(FATE_AC3_PHASE-yes)
 
-fate-ac3: $(FATE_AC3-yes) $(FATE_EAC3-yes) $(FATE_AC3_DITHER-yes) $(FATE_AC3_FIXED_DEXP24-yes)
+fate-ac3: $(FATE_AC3-yes) $(FATE_EAC3-yes) $(FATE_AC3_DITHER-yes) $(FATE_AC3_FIXED_DEXP24-yes) $(FATE_AC3_PHASE-yes)
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.