Commit 72d8fb25b6 for ffmpeg

commit 72d8fb25b6a810f40a7a656611d14b1078da71bf
Author: Lynne <dev@lynne.ee>
Date:   Wed Sep 23 20:27:44 2026 +0900

    avcodec/aaccoder_nmr, aacenc: allocation law retune, HF taper, PNS rework, bandwidth ladder

    Allocation and band-selection changes to the NMR coder.

    Allocation law. The law moves from the Apple-derived (0.443, 0.111) to
    (0.50, 0.18), swept on the repaired measurement harness: the old
    constants describe Apple's bit distribution, not this trellis's optimum,
    and the harness defect that anchored them charged full-scale integer
    wraps on hot masters to the louder encoder. Corpus Zimtohrli -38% /
    ViSQOL +0.06 at 128k stereo, -15% / +0.05 at 64k, all listening guards
    flat or better.

    HF precision taper. Bitstream comparison at matched rate shows Apple
    coding 7.5-12.6kHz about 4 scalefactor steps coarser and 12.6kHz+ about
    19 steps coarser (with 22% of those bands zeroed) than a flat allocation
    gives, funding their LF/mid SNR edge; the ear tolerates coarse HF
    texture far better than mid-band noise. A quadratic threshold rise above
    the knee (0.08 dB * ((f - 8kHz)/1kHz)^2) reproduces the rolloff: -5.4%
    corpus Zim at 128k stereo for -0.014 ViSQOL, -2.3% at 64k, -1.2% at 96k.
    A spread-coupled variant measured flat and is not included. Bands the
    taper pushes more than 2dB past their real mask are kept out of the
    nd_ema statistic: their deficit is by design, and on HF-rich content it
    would otherwise ramp the pressure state into noise-class rate control on
    clean tonal signals.

    Group-window integrity. A coded group-band no longer mutes individual
    borderline windows: they share the group scalefactor (near-free to
    keep) and a 3-6ms in-band mute right before an attack reads as a gated
    crunch on every beat.

    PNS. Hole-fill fires at any comfortable lambda: noise-like bands whose
    chosen rendition leaves most energy uncoded turn to sparse spectral
    lines - structure damage the NMR objective cannot see (measured:
    13-16kHz flatness 0.45 in source, 0.07 coded, 0.47 with noise). The
    debounce closes two churn doors: near-masked entry now needs two
    consecutive wants and the loud-band guard exits only after two loud
    frames; bands no longer flip coded/noise at frame rate.

    Not included: skipping the short-window threshold flatten on
    LF-dominated windows. Such a window's HF bins hold sidelobe leakage of
    the LF (phase-locked, acoustically silent), and the flatten hands them
    the LF-sized window-mean threshold, so the residue unmasks as an HF
    smear trailing slow LF attacks (SombreDetune 1.7s: 21x the reference
    HF). The skip takes it to 5.7x and improves the 128k transient panel,
    but the flatten is real anti-starvation protection at 64k and under
    pressure, and no gate tried separates the two classes cleanly.

    Encoder side:
    - The sectioning trellis receives the coder's real operating lambda
      rather than the static outer-loop 120 (measured neutral, but wrong on
      principle).
    - Exact levels: the clip-avoidance pre-attenuation scales every
      near-full-scale frame by up to 0.95, a permanent -0.45dB level error
      on hot masters (dense tonal content: -0.46dB mean partial level error,
      Apple -0.03dB). Float decoders never clip and int16 playback saturates
      the rare overshoot gracefully: -5.1% corpus Zim at 128k stereo.
      Scoped to the NMR coder.
    - The bandwidth ladder was tuned by metric sweeps into Zimtohrli's blind
      spot: it rewards HF presence and cannot hear HF graininess, so sweeps
      always push wide - while coarse HF at these rates reads as
      beat-synchronous crunch. Ear-tuned down to 15/16/19.5 kHz at 48/64/96
      kbps per channel (FDK sits at 14k, Apple at ~16k for 64k/ch).

    Measured and not included: an LF precision tilt in either direction, a
    tonality term in the law, power-mean pooling of group thresholds (the
    current sum is p=1; corpus-flat at every exponent) and a START-frame HF
    tilt.

    The id3v2 re-encode refs follow the default NMR output.

diff --git a/libavcodec/aaccoder_nmr.h b/libavcodec/aaccoder_nmr.h
index 224cb24d7d..bb5c4538df 100644
--- a/libavcodec/aaccoder_nmr.h
+++ b/libavcodec/aaccoder_nmr.h
@@ -73,6 +73,9 @@
  * energy-matched noise (PNS); an audible hole sounds worse than matched noise. */
 #define NMR_PNS_HOLE_FRAC   0.5f
 #define NMR_PNS_HOLE_SPREAD 0.5f
+/* Hole fill applies at any operating lambda above this (comfortable rates
+ * included): sparse-line renditions of noise are audible at every rate. */
+#define NMR_PNS_HOLE_LAM    20.0f

 /* RC servo gain: scale the corridor centre by exp2(-K*fill/R) each frame to hold
  * the long-run mean rate; without it a bad centre drifts for dozens of frames. */
@@ -99,6 +102,15 @@
  * threshold to zero (marginal bands flicker audibly otherwise). */
 #define NMR_ZERO_STICKY 0.5f

+/* Short-window groups keep a borderline window coded down to this fraction
+ * of its zero threshold rather than muting it inside a coded group. */
+#define NMR_GROUP_KEEP 0.25f
+
+/* HF precision taper: thresholds rise by NMR_HF_TAPER dB per kHz^2 above the
+ * knee (Apple's measured HF precision rolloff). */
+#define NMR_HF_TAPER      0.08f
+#define NMR_HF_TAPER_KNEE 8000.0f
+
 /* Transient bit-burst: an isolated onset (preceded by >= NMR_BURST_GAP long frames)
  * is coded NMR_BURST_GAIN x finer, held uniform across the run, repaid from steady stretches. */
 #define NMR_BURST_GAP   10
@@ -297,7 +309,10 @@ static int nmr_setup_channel(AVCodecContext *avctx, AACEncContext *s,

     /* Allocation law; short frames blend to softer energy exponents under
      * pressure (roll anti-starvation, see memory). */
-    float a_ae = 0.443f, a_at = 0.111f;
+    /* The Apple-RE constants (0.443/0.111) describe THEIR bit distribution,
+     * not this trellis's optimum: more psy-threshold influence is worth -38%
+     * Zim / +0.06 ViSQOL at 128k stereo and -15% at 64k, flat plateau beyond. */
+    float a_ae = 0.50f, a_at = 0.18f;
     if (sce->ics.num_windows == 8 && s->nmr) {
         /* blend to mask-weighted exponents under rate pressure */
         a_ae += (0.35f - a_ae) * s->nmr->press;
@@ -331,6 +346,17 @@ static int nmr_setup_channel(AVCodecContext *avctx, AACEncContext *s,
                 ratio = eside / FFMAX(mb->energy * sce->ics.group_len[w], 1e-9f);
                 zthr_mul *= 0.25f + 0.75f * av_clipf(ratio / 0.3f, 0.0f, 1.0f);
             }
+            /* HF precision taper (bitstream RE): at 128k Apple codes
+             * 7.5-12.6k ~6dB coarser and 12.6k+ ~28dB coarser than we do,
+             * funding their LF/mid SNR edge; a quadratic thr rise above the
+             * knee reproduces that rolloff */
+            float hftdb;
+            {
+                float bfreq = start * (avctx->sample_rate * (sce->ics.num_windows == 8 ? 4.0f : 0.5f)) / 1024.0f;
+                float k = FFMAX(0.0f, bfreq - NMR_HF_TAPER_KNEE) / 1000.0f;
+                hftdb = NMR_HF_TAPER * k * k;
+            }
+            t->hftx[w*16+g] = 0;
             for (int w2 = 0; w2 < sce->ics.group_len[w]; w2++) {
                 FFPsyBand *band = &s->psy.ch[s->cur_channel].psy_bands[(w+w2)*16+g];
                 ener   += band->energy;
@@ -343,11 +369,36 @@ static int nmr_setup_channel(AVCodecContext *avctx, AACEncContext *s,
                 uplim += band->threshold;
                 nz = 1;
             }
+            if (nz && sce->ics.group_len[w] > 1) {
+                /* a coded group must not mute individual borderline windows:
+                 * they share the group's scalefactor (near-free to keep) and
+                 * a 3-6ms in-band mute right before an attack reads as a
+                 * gated crunch on every beat */
+                for (int w2 = 0; w2 < sce->ics.group_len[w]; w2++) {
+                    FFPsyBand *band = &s->psy.ch[s->cur_channel].psy_bands[(w+w2)*16+g];
+                    if (sce->zeroes[(w+w2)*16+g] && start < cutoff &&
+                        band->threshold > 0.0f &&
+                        band->energy > band->threshold * zthr_mul * NMR_GROUP_KEEP) {
+                        sce->zeroes[(w+w2)*16+g] = 0;
+                        uplim += band->threshold;
+                    }
+                }
+            }
             zprev[w*16+g] = !nz;
             sce->zeroes[w*16+g] = !nz;
             t->thr_real[w*16+g] = uplim;    /* real mask, before the allocation law (PNS gate) */
-            if (nz && ener > 0.0f && uplim > 0.0f)   /* allocation law */
+            if (nz && ener > 0.0f && uplim > 0.0f) { /* allocation law */
                 uplim = expf(a_ae * logf(ener) + a_at * logf(uplim));
+                if (hftdb != 0.0f) {
+                    uplim *= powf(10.0f, hftdb * 0.1f);
+                    /* a band pushed well past its real mask reads as huge
+                     * achieved-nd; keep the deliberate deficit out of the
+                     * psy-reliability stat or press ramps into noise-class
+                     * RC on clean tonal content */
+                    if (hftdb > 2.0f)
+                        t->hftx[w*16+g] = 1;
+                }
+            }
             t->thr[w*16+g]     = uplim;
             t->pener[w*16+g]   = ener;
             t->pspread[w*16+g] = spread;
@@ -752,7 +803,7 @@ static void nmr_solve_group(AVCodecContext *avctx, AACEncContext *s,
             float (*ndk)[NMR_NCAND] = s->nmr->nd[t->si];
             for (int b_ = 0; b_ < t->nact; b_++) {
                 int b = t->act[b_], bi = t->bidx[b];
-                if (t->thr_real[bi] > 0.0f && t->thr[bi] > 0.0f) {
+                if (t->thr_real[bi] > 0.0f && t->thr[bi] > 0.0f && !t->hftx[bi]) {
                     ndsum += ndk[b][t->chosen[b]] * t->thr[bi] / t->thr_real[bi];
                     ndn++;
                 }
@@ -826,13 +877,17 @@ static void nmr_solve_group(AVCodecContext *avctx, AACEncContext *s,
                 /* (can_pns was already checked above; gates below fill `want`) */
                 if (t->pener[bi] > NMR_PNS_MAX_ET * t->thr_real[bi]) {
                     force_exit = 1;                       /* loud-band guard */
+                } else if (lam > NMR_PNS_HOLE_LAM &&
+                           (frac = ndk[b][t->chosen[b]] * t->thr[bi] /
+                                   FFMAX(t->pener[bi], 1e-9f)) > NMR_PNS_HOLE_FRAC * (was ? 0.7f : 1.0f) &&
+                           spread > NMR_PNS_HOLE_SPREAD) {
+                    /* Spectral-hole fill: a noise-like band whose chosen
+                     * rendition leaves most of its energy uncoded turns to
+                     * sparse lines - structure damage the NMR objective
+                     * cannot see - at ANY comfortable lambda */
+                    want = 1;
                 } else if (lam > pns_lam) {
-                    /* Spectral-hole fill: a noise-like band left mostly empty */
-                    frac = ndk[b][t->chosen[b]] * t->thr[bi] / FFMAX(t->pener[bi], 1e-9f);
-                    if (spread > NMR_PNS_HOLE_SPREAD &&
-                        frac > NMR_PNS_HOLE_FRAC * (was ? 0.7f : 1.0f)) {
-                        want = 1;
-                    } else if (ndk[b][t->chosen[b]] * t->thr[bi] >
+                    if (ndk[b][t->chosen[b]] * t->thr[bi] >
                                NMR_PNS_NDGATE * t->thr_real[bi] * (was ? 0.5f : 1.0f)) {
                         /* replace only a band coded audibly badly; cost of
                          * energy-matched noise = its non-noise-like fraction */
@@ -851,9 +906,9 @@ static void nmr_solve_group(AVCodecContext *avctx, AACEncContext *s,
                     if (want) { if (*ron  < 255) (*ron)++;  *roff = 0; }
                     else      { if (*roff < 255) (*roff)++; *ron  = 0; }
                     if (force_exit)
-                        want = 0;
+                        want = was && *roff < 2;   /* tolerate 1-frame loudness blips */
                     else if (!was)
-                        want = near ? want : *ron >= NMR_PNS_ON;
+                        want = *ron >= (near ? 2 : NMR_PNS_ON);
                     else if (near)
                         want = 1;   /* physics-hysteresis: noise until audible */
                     else
diff --git a/libavcodec/aacenc.c b/libavcodec/aacenc.c
index 3b7dce27f6..efd3b4aba9 100644
--- a/libavcodec/aacenc.c
+++ b/libavcodec/aacenc.c
@@ -1042,7 +1042,16 @@ static void encode_band_info(AACEncContext *s, SingleChannelElement *sce)
         s->coder->set_special_band_scalefactors(s, sce);

     for (w = 0; w < sce->ics.num_windows; w += sce->ics.group_len[w])
-        s->coder->encode_window_bands_info(s, sce, w, sce->ics.group_len[w], s->lambda);
+        {
+            /* the sectioning trellis must trade section bits against
+             * spectral bits at the coder's REAL operating lambda; the
+             * NMR outer-loop lambda is a static 120 */
+            float slam = s->lambda;
+            if (s->options.coder == AAC_CODER_NMR && s->nmr &&
+                s->nmr->lam_slew > 0.0f)
+                slam = s->nmr->lam_slew;
+            s->coder->encode_window_bands_info(s, sce, w, sce->ics.group_len[w], slam);
+        }
 }

 /**
@@ -1321,6 +1330,12 @@ static int aac_encode_frame(AVCodecContext *avctx, AVPacket *avpkt,
                     max = FFMAX(max, fabsf(wbuf[j]));
                 wi[ch].clipping[w] = max;
             }
+            /* Pre-attenuating hot frames costs 0.45 dB of level accuracy on
+             * loud masters; float decoders don't clip, so the NMR coder
+             * keeps levels exact. */
+            if (s->options.coder == AAC_CODER_NMR)
+                for (w = 0; w < ics->num_windows; w++)
+                    wi[ch].clipping[w] = 0;
             for (w = 0; w < ics->num_windows; w++) {
                 if (wi[ch].clipping[w] > CLIP_AVOIDANCE_FACTOR) {
                     ics->window_clipping[w] = 1;
@@ -1841,8 +1856,12 @@ static av_cold int aac_encode_init(AVCodecContext *avctx)
                        (avctx->bit_rate / avctx->ch_layout.nb_channels);

         if (s->options.coder == AAC_CODER_NMR && frame_br >= 24000) {
+            /* Ear-tuned, not metric-tuned: Zim rewards HF presence and cannot
+             * hear HF graininess, so metric sweeps push this table wide. At
+             * these rates coarse HF reads as beat-synchronous crunch (velvet);
+             * FDK sits at 14k and Apple at ~16k for 64 kbps/ch. */
             static const int rates[] = { 24000, 32000, 48000, 64000, 96000, 192000 };
-            static const int bws[]   = { 14000, 14000, 18500, 20000, 21000, 22000 };
+            static const int bws[]   = { 14000, 14000, 15000, 16000, 19500, 22000 };
             int bw_i = 0;
             for (; bw_i < FF_ARRAY_ELEMS(rates) - 2 && frame_br > rates[bw_i + 1]; bw_i++);
             s->bandwidth = bws[bw_i] + (int)((int64_t)(bws[bw_i + 1] - bws[bw_i]) *
diff --git a/libavcodec/aacenc.h b/libavcodec/aacenc.h
index a59c9458eb..dfd5c23a2b 100644
--- a/libavcodec/aacenc.h
+++ b/libavcodec/aacenc.h
@@ -200,6 +200,7 @@ typedef struct NMRSlot {
     float pener[128];                            ///< band energy (PNS noise target)
     float pspread[128];                          ///< band tonality spread (1 = noise)
     uint8_t is_pns[128];                         ///< band coded as noise
+    uint8_t hftx[128];                           ///< band strongly HF-tapered: excluded from the nd stat (deficit is by design)
 } NMRSlot;

 typedef struct AACNMRCurves {
diff --git a/tests/ref/fate/id3v2-reenc-delete-metadata b/tests/ref/fate/id3v2-reenc-delete-metadata
index bd7aad1783..866aea13ad 100644
--- a/tests/ref/fate/id3v2-reenc-delete-metadata
+++ b/tests/ref/fate/id3v2-reenc-delete-metadata
@@ -1,5 +1,5 @@
-1805fde23636947a81abf947d75d2115 *tests/data/fate/id3v2-reenc-delete-metadata.nut
-2651 tests/data/fate/id3v2-reenc-delete-metadata.nut
+d551ef153999cb3c8a304768dbce9b80 *tests/data/fate/id3v2-reenc-delete-metadata.nut
+2699 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 84c562559d..4c031cad82 100644
--- a/tests/ref/fate/id3v2-reenc-delete-metadata-keep
+++ b/tests/ref/fate/id3v2-reenc-delete-metadata-keep
@@ -1,5 +1,5 @@
-759156778fafe46d062c502bb5bfdc3f *tests/data/fate/id3v2-reenc-delete-metadata-keep.nut
-2791 tests/data/fate/id3v2-reenc-delete-metadata-keep.nut
+8000a5d46a648126fc1c2d1b6240b44d *tests/data/fate/id3v2-reenc-delete-metadata-keep.nut
+2839 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 2e823807b9..40410ca2b6 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 @@
-759156778fafe46d062c502bb5bfdc3f *tests/data/fate/id3v2-reenc-delete-metadata-keep-format.nut
-2791 tests/data/fate/id3v2-reenc-delete-metadata-keep-format.nut
+8000a5d46a648126fc1c2d1b6240b44d *tests/data/fate/id3v2-reenc-delete-metadata-keep-format.nut
+2839 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 4a4df4db15..29b487cf79 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 @@
-1805fde23636947a81abf947d75d2115 *tests/data/fate/id3v2-reenc-delete-metadata-keep-stream.nut
-2651 tests/data/fate/id3v2-reenc-delete-metadata-keep-stream.nut
+d551ef153999cb3c8a304768dbce9b80 *tests/data/fate/id3v2-reenc-delete-metadata-keep-stream.nut
+2699 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 725ab61b1c..5658e4d250 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 @@
-1805fde23636947a81abf947d75d2115 *tests/data/fate/id3v2-reenc-delete-metadata-map-metadata.nut
-2651 tests/data/fate/id3v2-reenc-delete-metadata-map-metadata.nut
+d551ef153999cb3c8a304768dbce9b80 *tests/data/fate/id3v2-reenc-delete-metadata-map-metadata.nut
+2699 tests/data/fate/id3v2-reenc-delete-metadata-map-metadata.nut
 [FORMAT]
 TAG:title=7rk
 [/FORMAT]