This is an automated email from the git hooks/post-receive script. Git pushed a commit to branch master in repository ffmpeg.
commit 72d8fb25b6a810f40a7a656611d14b1078da71bf Author: Lynne <[email protected]> AuthorDate: Wed Sep 23 20:27:44 2026 +0900 Commit: Lynne <[email protected]> CommitDate: Sun Oct 4 17:51:01 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. --- libavcodec/aaccoder_nmr.h | 77 ++++++++++++++++++---- libavcodec/aacenc.c | 23 ++++++- libavcodec/aacenc.h | 1 + tests/ref/fate/id3v2-reenc-delete-metadata | 4 +- tests/ref/fate/id3v2-reenc-delete-metadata-keep | 4 +- .../fate/id3v2-reenc-delete-metadata-keep-format | 4 +- .../fate/id3v2-reenc-delete-metadata-keep-stream | 4 +- .../fate/id3v2-reenc-delete-metadata-map-metadata | 4 +- 8 files changed, 98 insertions(+), 23 deletions(-) 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] -- To stop receiving notification emails like this one, please contact [email protected]. _______________________________________________ ffmpeg-cvslog mailing list -- [email protected] To unsubscribe send an email to [email protected]
