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]