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]