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.
This commit is contained in:
Lynne committed 2026-10-04 17:51:01 +09:00
1 parent 77506207d2
commit 03ce7af84b
6 files changed
+39 -13

No files matched your search

+29 -3
View File
@@ -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
+2 -2
View File
@@ -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]
@@ -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
@@ -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
@@ -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]
@@ -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]