Commit f68ccd1b39 for ffmpeg

commit f68ccd1b3941cebba1cafaf6c7982bd977af1580
Author: Lynne <dev@lynne.ee>
Date:   Wed Sep 23 20:26:12 2026 +0900

    avcodec/aacpsy: stronger PE demand shaping, signal-relative mask floor

    PE demand shaping. The 3GPP bit-demand curve modulates per-frame budgets
    far too gently for the trellis coder: amplifying the deviation from
    unity by 2.5x is worth -2.5% Zimtohrli at 128k stereo and -13.5% at 64k
    (ViSQOL up at both), with a sharp cliff beyond ~2.8 as budgets start
    swinging into the clamps. Listening guards flat, pulse-train sample
    stays click-free.

    The amplified factor goes negative in high-PE frames on a starved
    reservoir - 15-30% of CBR channel-frames. The desired PE is then zero or
    below, and the reduction raises every threshold as far as the min-SNR
    and hole rules allow: those frames are shaped for constant SNR rather
    than by the mask. This is load-bearing, not an accident: flooring the
    returned demand at the documented frame_bits/8 costs +5.2% / +3.3%
    Zimtohrli at 64k on the two ABC/HR corpora (mixed at 128k).

    The reservoir-fill correction of the desired PE is clamped
    symmetrically in the log domain, [1/1.15, 1.15] instead of
    [0.85, 1.15]: a negative previous demand sits on the lower bound, and
    the asymmetric 0.85 there costs 2.4% Zimtohrli at 64k.

    Signal-relative mask floor. 3GPP 5.6.1.3.3 exempts bands that are quiet
    against the spread energy from the min-SNR floor ("holes allowed
    here"), and nothing downstream bounds them: measured at 128k, the
    published mask ends up only 1.5-2 dB below the band's own energy across
    the whole 6-16 kHz range, on every sample tested. The model is asserting
    the top of the spectrum is inaudible, and every consumer believes it -
    the allocator buys those bands nothing, and a quality-target solver sees
    a mask it cannot fail.

    A band we still choose to code may now not have its mask come within
    PSY_THRFL_* dB of its own energy. The two rate-control families want
    different shapes, and measurably so, because the mask does a different
    job in each: in quality-target modes it IS the rate authority, so the
    floor is broadband (10 dB); in CBR the budget is fixed and the mask only
    ranks bands against each other, so a broadband floor merely shuffles
    bits and costs ~2% Zim on random content, while restricting it to above
    8 kHz (6 dB) keeps the win. Here the quality-target branch covers -q:a;
    the ABR mode later in the series joins it.

    CBR at matched rate, Zimtohrli / ViSQOL over the two ABC/HR corpora and
    two random-clip corpora: -14% / +0.017, 0% / +0.008, +1.4% / +0.003,
    0% / +0.004. ViSQOL improves on every corpus. The +1.4% Zim is on the 16
    canonical clips only; a fresh 32-clip random corpus is flat there, and
    the ungated floor costs +1.8% on that same set, so the frequency gate is
    what makes CBR safe.

    Threshold-shape experiments from the harness sweeps - spreading slopes,
    the min-SNR clamp, the tonality-dependent TMN/NMT offsets and the TNS
    gain bars - all measured at or worse than the flat 3GPP values,
    confirming the earlier static-threshold verdict, and are not included.

    The fast-coder encode tests move with the thresholds and are re-pinned:
    ln-encode-128k 622 -> 618, is-encode 500 -> 503, ms-encode 550 -> 563.
    The id3v2 re-encode refs follow the default NMR output.

diff --git a/libavcodec/aacpsy.c b/libavcodec/aacpsy.c
index 55ba412de4..ceb978d0a6 100644
--- a/libavcodec/aacpsy.c
+++ b/libavcodec/aacpsy.c
@@ -78,6 +78,30 @@
 #define PSY_3GPP_CLIP_HI_L      0.95f
 #define PSY_3GPP_CLIP_HI_S      0.75f

+/* Floor on how close a coded band's mask may come to the band's own energy.
+ * The two rate-control families want different shapes, and measurably so:
+ * in VBR/ABR the mask IS the rate authority, so a mask that has risen to meet
+ * its band's energy tells the solver the band is free to destroy and it buys
+ * no bits for it - a broadband floor is right there. In CBR the budget is
+ * fixed and the mask only ranks bands against each other, so the same
+ * broadband floor just moves bits around and costs ~2% Zim on random content;
+ * restricted to the top end, where the mask degenerates to within 2 dB of band
+ * energy on every sample measured, it is a clear win. */
+#define PSY_THRFL_QUALITY       10.0f   /* VBR/ABR depth, dB, all bands */
+#define PSY_THRFL_CBR            6.0f   /* CBR depth, dB */
+#define PSY_THRFL_CBR_KNEE    8000.0f   /* CBR: only above this frequency */
+
+/* Strength of the 3GPP bit-demand curve: its deviation from unity is
+ * amplified, since the trellis coder wants far stronger per-frame budget
+ * modulation than the reference encoder's gentle curve provides.
+ * The amplified factor goes negative in high-PE frames on a starved
+ * reservoir (15-30% of CBR frames). The desired PE is then <= 0 and the
+ * reduction raises every threshold as far as the min-SNR and hole rules
+ * allow, so those frames are shaped for constant SNR instead of by the
+ * mask. That regime is load-bearing: flooring the demand at frame_bits/8
+ * costs 3-5% Zimtohrli at 64 kbps stereo. */
+#define PSY_3GPP_DEMAND_SCALE    2.5f
+
 #define PSY_3GPP_AH_THR_LONG    0.5f
 #define PSY_3GPP_AH_THR_SHORT   0.63f

@@ -551,7 +575,7 @@ static int calc_bit_demand(AacPsyContext *ctx, float pe, int bits, int size,
                            int short_window)
 {
     const float bitsave_slope  = short_window ? PSY_3GPP_SAVE_SLOPE_S  : PSY_3GPP_SAVE_SLOPE_L;
-    const float bitsave_add    = short_window ? PSY_3GPP_SAVE_ADD_S    : PSY_3GPP_SAVE_ADD_L;
+    const float bitsave_add   = short_window ? PSY_3GPP_SAVE_ADD_S    : PSY_3GPP_SAVE_ADD_L;
     const float bitspend_slope = short_window ? PSY_3GPP_SPEND_SLOPE_S : PSY_3GPP_SPEND_SLOPE_L;
     const float bitspend_add   = short_window ? PSY_3GPP_SPEND_ADD_S   : PSY_3GPP_SPEND_ADD_L;
     const float clip_low       = short_window ? PSY_3GPP_CLIP_LO_S     : PSY_3GPP_CLIP_LO_L;
@@ -573,6 +597,7 @@ static int calc_bit_demand(AacPsyContext *ctx, float pe, int bits, int size,
      * Hopefully below is correct.
      */
     bit_factor = 1.0f - bit_save + ((bit_spend - bit_save) / (ctx->pe.max - ctx->pe.min)) * (clipped_pe - ctx->pe.min);
+    bit_factor = 1.0f + (bit_factor - 1.0f) * PSY_3GPP_DEMAND_SCALE;
     /* NOTE: The reference encoder attempts to center pe max/min around the current pe.
      * Here we do that by slowly forgetting pe.min when pe stays in a range that makes
      * it unlikely (ie: above the mean)
@@ -806,9 +831,13 @@ static void psy_3gpp_analyze_channel(FFPsyContext *ctx, int channel,
          *       little effect on the final bitrate. Probably a good idea to come
          *       back and do more testing later.
          */
-        if (ctx->bitres.bits > 0)
+        if (ctx->bitres.bits > 0) {
+            /* symmetric in the log domain: a negative previous demand (see
+             * PSY_3GPP_DEMAND_SCALE) sits on the lower bound, and the
+             * asymmetric 0.85 there costs ~2% Zimtohrli at 64 kbps */
             desired_pe *= av_clipf(pctx->pe.previous / PSY_3GPP_BITS_TO_PE(ctx->bitres.bits),
-                                   0.85f, 1.15f);
+                                   1.0f / 1.15f, 1.15f);
+        }
     }
     pctx->pe.previous = PSY_3GPP_BITS_TO_PE(desired_bits);
     ctx->bitres.alloc = desired_bits;
@@ -905,6 +934,32 @@ static void psy_3gpp_analyze_channel(FFPsyContext *ctx, int channel,
         }
     }

+    /* Signal-relative mask ceiling. 5.6.1.3.3 exempts bands that are quiet
+     * relative to the spread energy from the min-SNR floor ("holes allowed
+     * here"), so on spectrally lopsided programme their mask is free to rise
+     * until it meets their own energy - the model then calls a band that
+     * carries real texture inaudible, and every consumer of the mask agrees:
+     * the allocator buys it nothing and the quality-target solver sees a mask
+     * it cannot fail. A band we still choose to code must never be allowed
+     * noise within PSY_THRFL_* dB of its own energy, whatever the hole logic said. */
+    {
+        int qmode = !!(ctx->avctx->flags & AV_CODEC_FLAG_QSCALE);
+        float lim  = qmode ? PSY_THRFL_QUALITY : PSY_THRFL_CBR;
+        float knee = qmode ? 0.0f : PSY_THRFL_CBR_KNEE;
+        float lo   = ff_exp10f(-lim / 10.0f);
+        float l2f  = ctx->avctx->sample_rate / 2.0f /
+                     (wi->num_windows == 1 ? 1024.0f : 128.0f);
+        for (w = 0; w < wi->num_windows*16; w += 16) {
+            int start = 0;
+            for (g = 0; g < num_bands; g++) {
+                AacPsyBand *band = &pch->band[w+g];
+                if (start * l2f >= knee)
+                    band->thr = FFMIN(band->thr, band->energy * lo);
+                start += band_sizes[g];
+            }
+        }
+    }
+
     for (w = 0; w < wi->num_windows*16; w += 16) {
         for (g = 0; g < num_bands; g++) {
             AacPsyBand *band     = &pch->band[w+g];
diff --git a/tests/fate/aac.mak b/tests/fate/aac.mak
index 7bbcc6d5f6..ec49b83b7d 100644
--- a/tests/fate/aac.mak
+++ b/tests/fate/aac.mak
@@ -224,7 +224,7 @@ FATE_AAC_ENCODE += fate-aac-ln-encode-128k
 fate-aac-ln-encode-128k: CMD = enc_dec_pcm mp4 wav s16le $(TARGET_SAMPLES)/audio-reference/luckynight_2ch_44kHz_s16.wav -c:a aac -aac_coder fast -aac_is 0 -aac_pns 0 -aac_ms 0 -aac_tns 0 -b:a 128k -cutoff 22050 -fflags +bitexact -flags +bitexact
 fate-aac-ln-encode-128k: CMP = stddev
 fate-aac-ln-encode-128k: REF = $(SAMPLES)/audio-reference/luckynight_2ch_44kHz_s16.wav
-fate-aac-ln-encode-128k: CMP_TARGET = 622
+fate-aac-ln-encode-128k: CMP_TARGET = 618
 fate-aac-ln-encode-128k: FUZZ = 10

 FATE_AAC_ENCODE += fate-aac-pns-encode
@@ -245,14 +245,14 @@ FATE_AAC_ENCODE += fate-aac-is-encode
 fate-aac-is-encode: CMD = enc_dec_pcm mp4 wav s16le $(TARGET_SAMPLES)/audio-reference/luckynight_2ch_44kHz_s16.wav -c:a aac -aac_coder fast -aac_pns 0 -aac_is 1 -aac_ms 0 -b:a 128k -aac_tns 0 -cutoff 22050 -fflags +bitexact -flags +bitexact
 fate-aac-is-encode: CMP = stddev
 fate-aac-is-encode: REF = $(SAMPLES)/audio-reference/luckynight_2ch_44kHz_s16.wav
-fate-aac-is-encode: CMP_TARGET = 500
+fate-aac-is-encode: CMP_TARGET = 503
 fate-aac-is-encode: FUZZ = 10

 FATE_AAC_ENCODE += fate-aac-ms-encode
 fate-aac-ms-encode: CMD = enc_dec_pcm mp4 wav s16le $(TARGET_SAMPLES)/audio-reference/luckynight_2ch_44kHz_s16.wav -c:a aac -aac_coder fast -aac_pns 0 -aac_is 0 -aac_ms 1 -aac_tns 0 -b:a 128k -cutoff 22050 -fflags +bitexact -flags +bitexact
 fate-aac-ms-encode: CMP = stddev
 fate-aac-ms-encode: REF = $(SAMPLES)/audio-reference/luckynight_2ch_44kHz_s16.wav
-fate-aac-ms-encode: CMP_TARGET = 550
+fate-aac-ms-encode: CMP_TARGET = 563
 fate-aac-ms-encode: FUZZ = 15

 #Ticket1784
diff --git a/tests/ref/fate/id3v2-reenc-delete-metadata b/tests/ref/fate/id3v2-reenc-delete-metadata
index 2d19a3071b..bd7aad1783 100644
--- a/tests/ref/fate/id3v2-reenc-delete-metadata
+++ b/tests/ref/fate/id3v2-reenc-delete-metadata
@@ -1,5 +1,5 @@
-54b8e2b9f3d83cbd601ae625978a5c4a *tests/data/fate/id3v2-reenc-delete-metadata.nut
-2683 tests/data/fate/id3v2-reenc-delete-metadata.nut
+1805fde23636947a81abf947d75d2115 *tests/data/fate/id3v2-reenc-delete-metadata.nut
+2651 tests/data/fate/id3v2-reenc-delete-metadata.nut
 [FORMAT]
 TAG:title=7rk
 [/FORMAT]
diff --git a/tests/ref/fate/id3v2-reenc-delete-metadata-keep b/tests/ref/fate/id3v2-reenc-delete-metadata-keep
index 98caa8c82b..84c562559d 100644
--- a/tests/ref/fate/id3v2-reenc-delete-metadata-keep
+++ b/tests/ref/fate/id3v2-reenc-delete-metadata-keep
@@ -1,5 +1,5 @@
-46e8ca22aa8be69d14bfad4f4ed3cefc *tests/data/fate/id3v2-reenc-delete-metadata-keep.nut
-2823 tests/data/fate/id3v2-reenc-delete-metadata-keep.nut
+759156778fafe46d062c502bb5bfdc3f *tests/data/fate/id3v2-reenc-delete-metadata-keep.nut
+2791 tests/data/fate/id3v2-reenc-delete-metadata-keep.nut
 [FORMAT]
 TAG:title=7rk
 TAG:comment-iTunSMPB-eng= 00000000 00000210 0000086A 0000000000066486 00000000 0002DA9D 00000000 00000000 00000000 00000000 00000000 00000000
diff --git a/tests/ref/fate/id3v2-reenc-delete-metadata-keep-format b/tests/ref/fate/id3v2-reenc-delete-metadata-keep-format
index a3e9fbd819..2e823807b9 100644
--- a/tests/ref/fate/id3v2-reenc-delete-metadata-keep-format
+++ b/tests/ref/fate/id3v2-reenc-delete-metadata-keep-format
@@ -1,5 +1,5 @@
-46e8ca22aa8be69d14bfad4f4ed3cefc *tests/data/fate/id3v2-reenc-delete-metadata-keep-format.nut
-2823 tests/data/fate/id3v2-reenc-delete-metadata-keep-format.nut
+759156778fafe46d062c502bb5bfdc3f *tests/data/fate/id3v2-reenc-delete-metadata-keep-format.nut
+2791 tests/data/fate/id3v2-reenc-delete-metadata-keep-format.nut
 [FORMAT]
 TAG:title=7rk
 TAG:comment-iTunSMPB-eng= 00000000 00000210 0000086A 0000000000066486 00000000 0002DA9D 00000000 00000000 00000000 00000000 00000000 00000000
diff --git a/tests/ref/fate/id3v2-reenc-delete-metadata-keep-stream b/tests/ref/fate/id3v2-reenc-delete-metadata-keep-stream
index 6415519a53..4a4df4db15 100644
--- a/tests/ref/fate/id3v2-reenc-delete-metadata-keep-stream
+++ b/tests/ref/fate/id3v2-reenc-delete-metadata-keep-stream
@@ -1,5 +1,5 @@
-54b8e2b9f3d83cbd601ae625978a5c4a *tests/data/fate/id3v2-reenc-delete-metadata-keep-stream.nut
-2683 tests/data/fate/id3v2-reenc-delete-metadata-keep-stream.nut
+1805fde23636947a81abf947d75d2115 *tests/data/fate/id3v2-reenc-delete-metadata-keep-stream.nut
+2651 tests/data/fate/id3v2-reenc-delete-metadata-keep-stream.nut
 [FORMAT]
 TAG:title=7rk
 [/FORMAT]
diff --git a/tests/ref/fate/id3v2-reenc-delete-metadata-map-metadata b/tests/ref/fate/id3v2-reenc-delete-metadata-map-metadata
index 62a9411e4e..725ab61b1c 100644
--- a/tests/ref/fate/id3v2-reenc-delete-metadata-map-metadata
+++ b/tests/ref/fate/id3v2-reenc-delete-metadata-map-metadata
@@ -1,5 +1,5 @@
-54b8e2b9f3d83cbd601ae625978a5c4a *tests/data/fate/id3v2-reenc-delete-metadata-map-metadata.nut
-2683 tests/data/fate/id3v2-reenc-delete-metadata-map-metadata.nut
+1805fde23636947a81abf947d75d2115 *tests/data/fate/id3v2-reenc-delete-metadata-map-metadata.nut
+2651 tests/data/fate/id3v2-reenc-delete-metadata-map-metadata.nut
 [FORMAT]
 TAG:title=7rk
 [/FORMAT]