Commit 03ce7af84b for ffmpeg
commit 03ce7af84be7926d5c12112bec6a8c705e6c2ad9
Author: Lynne <dev@lynne.ee>
Date: Wed Sep 23 20:29:41 2026 +0900
avcodec/aacenc: M/S balance gate, perceptual intensity stereo
M/S balance gate. M/S has no coding gain on a hard-panned band
(|S| ~ |M|) and its quantization noise decorrelates across the unfold,
smearing the panned source into the far channel; require channel
balance within ~6dB for M/S candidacy.
Perceptual intensity stereo, on by default. Zimtohrli penalizes I/S
reconstruction even when the image error sits below the masks, so the
metric cannot govern it; ears above ~8kHz do not hear interaural fine
structure, and they can. Engage I/S on perceptual criteria alone: above
8kHz, inter-channel correlation over 0.85 (sticky), long windows only,
with the image-error budget opened for qualifying bands regardless of
rate pressure. The gates select content like a scalpel - Angels 9.3% of
bands, Enola 6.7%, trumpet 3.0% - and exactly 0% on decorrelated-HF
material (velvet, Berling) where I/S would damage the image. Corpus cost
is metric noise (+0.6% Zim at 128k, +1.4% at 64k - Zimtohrli penalizes
I/S reconstruction the ear does not hear); freed bits improve the
sub-8kHz spectrum 10% on engagement-heavy content. -aac_is 0 disables
I/S altogether.
Measured and not included: lifting the both-channels-noise-like
requirement for stereo PNS (engagement moves less than 0.2% even at
15-18% band usage, corpus flat, and noise in one ear against coded
texture in the other is an imaging risk), and M/S threshold, BMLD side
reduction, M/S equivalence and I/S floor/surgical-candidacy sweeps.
The id3v2 re-encode refs follow the default NMR output; the fast-coder
encode tests are unaffected.
diff --git a/libavcodec/aacenc.c b/libavcodec/aacenc.c
index efd3b4aba9..9398467bc7 100644
--- a/libavcodec/aacenc.c
+++ b/libavcodec/aacenc.c
@@ -753,6 +753,15 @@ static void apply_intensity_stereo(ChannelElement *cpe)
/* PNS-stereo gate: substitute only clearly-decorrelated (wide) bands. */
#define NMR_PNS_STEREO_DECORR 0.6f
+/* M/S balance gate: no M/S on bands panned harder than this energy ratio */
+#define NMR_MS_BALANCE 0.25f
+
+/* Perceptual I/S: highly correlated long-window bands above this frequency
+ * take I/S at this image-error budget regardless of rate pressure */
+#define NMR_IS_PERC_FREQ 8000.0f
+#define NMR_IS_PERC_CORR 0.85f
+#define NMR_IS_PERC_GATE 50.0f
+
/* Recode one band's window group as mid+side in place. */
static void nmr_apply_ms_band(AACEncContext *s, ChannelElement *cpe,
int w, int g, int start, int len, int gl)
@@ -872,7 +881,8 @@ static void nmr_decide_stereo(AACEncContext *s, ChannelElement *cpe)
* not admit it). Unengaged candidates fall back to M/S. */
float is_ramp = s->nmr ? s->nmr->press *
av_clipf((s->nmr->lam_floor - 40.0f) / (120.0f - 40.0f), 0.0f, 1.0f) : 0.0f;
- const int allow_is = s->options.intensity_stereo && is_ramp > 0.0f;
+ /* perceptual I/S (below) makes candidacy pressure-independent */
+ const int allow_is = s->options.intensity_stereo;
const int pidx = (s->cur_channel >> 1) & 15;
const int decoupled = s->psy.pair_decoupled[pidx];
@@ -931,6 +941,18 @@ static void nmr_decide_stereo(AACEncContext *s, ChannelElement *cpe)
float eqgate = NMR_MS_EQUIV * (prev == 1 ? 1.5f : 1.0f); /* stay-until es>0.75em */
/* I/S = lossy economy: image-error budget scales with pressure */
float imgate = NMR_IS_IMG_GATE * is_ramp * (prev == 2 ? NMR_STICKY : 1.0f);
+ /* Perceptual I/S (metric-blind by design - Zimtohrli penalizes
+ * even sub-mask image error, ears above ~8k do not hear
+ * interaural fine structure): engage on genuinely intensity-
+ * panned HF - high inter-channel correlation, long windows,
+ * sticky - regardless of rate pressure. Freed bits are judged
+ * by the sub-8k spectrum; the image itself is judged by ears. */
+ if (cpe->ch[0].ics.num_windows != 8 && ener0 > FLT_MIN && ener1 > FLT_MIN) {
+ float corr = dot / sqrtf(ener0 * ener1);
+ if (start * freq_mult > NMR_IS_PERC_FREQ &&
+ fabsf(corr) > NMR_IS_PERC_CORR * (prev == 2 ? 0.9f : 1.0f))
+ imgate = FFMAX(imgate, NMR_IS_PERC_GATE * (prev == 2 ? NMR_STICKY : 1.0f));
+ }
float es_d = es_tot, em_d = em_tot;
if (s->nmr) {
float *ees = &s->nmr->sema_es[pi][sidx];
@@ -942,10 +964,14 @@ static void nmr_decide_stereo(AACEncContext *s, ChannelElement *cpe)
}
es_d = *ees; em_d = *eem;
}
+ /* Balance gate: M/S has no coding gain on a hard-panned band
+ * (|S| ~ |M|), and M/S quantization noise decorrelates across
+ * the unfold, smearing the panned source into the far channel. */
+ int bal_ok = FFMIN(ener0, ener1) > NMR_MS_BALANCE * FFMAX(ener0, ener1);
int ms_would = s->options.mid_side &&
(s->options.mid_side == 1 ||
- es_d < eqgate * em_d ||
- es_tot < NMR_MS_MASK * thr_g);
+ ((es_d < eqgate * em_d ||
+ es_tot < NMR_MS_MASK * thr_g) && bal_ok));
int ms_ok = ms_would && !decoupled;
float scale, sr_, imgratio; int p;
/* I/S competes with M/S above the frequency limit (candidacy must
diff --git a/tests/ref/fate/id3v2-reenc-delete-metadata b/tests/ref/fate/id3v2-reenc-delete-metadata
index 866aea13ad..5da30d724d 100644
--- a/tests/ref/fate/id3v2-reenc-delete-metadata
+++ b/tests/ref/fate/id3v2-reenc-delete-metadata
@@ -1,5 +1,5 @@
-d551ef153999cb3c8a304768dbce9b80 *tests/data/fate/id3v2-reenc-delete-metadata.nut
-2699 tests/data/fate/id3v2-reenc-delete-metadata.nut
+b8a8450e6cca1ac5097a41f081584618 *tests/data/fate/id3v2-reenc-delete-metadata.nut
+2716 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 4c031cad82..a2edc8e1ff 100644
--- a/tests/ref/fate/id3v2-reenc-delete-metadata-keep
+++ b/tests/ref/fate/id3v2-reenc-delete-metadata-keep
@@ -1,5 +1,5 @@
-8000a5d46a648126fc1c2d1b6240b44d *tests/data/fate/id3v2-reenc-delete-metadata-keep.nut
-2839 tests/data/fate/id3v2-reenc-delete-metadata-keep.nut
+dc08127e67c9284a11dde0cefd31177e *tests/data/fate/id3v2-reenc-delete-metadata-keep.nut
+2856 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 40410ca2b6..1b0e6ec891 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 @@
-8000a5d46a648126fc1c2d1b6240b44d *tests/data/fate/id3v2-reenc-delete-metadata-keep-format.nut
-2839 tests/data/fate/id3v2-reenc-delete-metadata-keep-format.nut
+dc08127e67c9284a11dde0cefd31177e *tests/data/fate/id3v2-reenc-delete-metadata-keep-format.nut
+2856 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 29b487cf79..d9988c16b7 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 @@
-d551ef153999cb3c8a304768dbce9b80 *tests/data/fate/id3v2-reenc-delete-metadata-keep-stream.nut
-2699 tests/data/fate/id3v2-reenc-delete-metadata-keep-stream.nut
+b8a8450e6cca1ac5097a41f081584618 *tests/data/fate/id3v2-reenc-delete-metadata-keep-stream.nut
+2716 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 5658e4d250..2baca102ed 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 @@
-d551ef153999cb3c8a304768dbce9b80 *tests/data/fate/id3v2-reenc-delete-metadata-map-metadata.nut
-2699 tests/data/fate/id3v2-reenc-delete-metadata-map-metadata.nut
+b8a8450e6cca1ac5097a41f081584618 *tests/data/fate/id3v2-reenc-delete-metadata-map-metadata.nut
+2716 tests/data/fate/id3v2-reenc-delete-metadata-map-metadata.nut
[FORMAT]
TAG:title=7rk
[/FORMAT]