[PR] avformat/movenc: fix dec3 chan_loc derivation for E-AC-3 dependent substreams (PR #24238)

phyd via ffmpeg-devel <[email protected]>
Newsgroups gmane.comp.video.ffmpeg.devel
Message-ID <[email protected]>
PR #24238 opened by phyd
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24238
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24238.patch

# Summary of changes

The mov/mp4 muxer derives the `dec3` box's `chan_loc` field from the E-AC-3
bitstream's `chanmap` with a plain shift, but the two fields use opposite bit
orders. Every E-AC-3 stream carrying a dependent substream — i.e. 7.1 and above
— is therefore muxed into MP4 with the wrong channel layout signalled in the
container.

`libavformat/movenc.c`, `handle_eac3()`:

```c
info->substream[parent].chan_loc |= (hdr->channel_map >> 5) & 0x1f;
```

`chanmap` is a 16-bit field read MSB-first (`get_bits(gbc, 16)` in
`ac3_parser.c`), so flag index *i* sits at bit `15 - i` — the same ordering
`ff_eac3_custom_channel_map_locations` is indexed against a few lines above.
`chan_loc` bit *j* carries `chanmap` flag index `5 + j` per ETSI TS 102 366
Annex F, i.e. bit `10 - j`. Converting between them is a bit reversal, not a
shift. The `0x1f` mask is also 5 bits where 9 are needed, so some flags are
dropped rather than merely misplaced.

Effect on the layouts that use a dependent substream:

| layout | `chanmap` | before | after |
|---|---|---|---|
| 7.1   | `0x1A00` | `0x010` | `0x002` |
| 5.1.2 | `0x0010` | `0x000` | `0x040` |
| 5.1.4 | `0x0210` | `0x010` | `0x042` |

For 7.1 the field claimed `Lsd/Rsd` (surround direct) — a position no 7.1
speaker layout has — instead of `Lrs/Rrs`.

This is invisible to FFmpeg itself, because our decoder reads the layout from
the bitstream, so an affected file still probes as a correct 7.1. Demuxers that
trust the container do act on it.

# Steps to reproduce

Using FFmpeg's own public sample, so no upload is needed:

```
curl -O https://samples.ffmpeg.org/A-codecs/AC3/eac3/7_pt_1.eac3
ffmpeg -i 7_pt_1.eac3 -c:a copy out.mp4
```

Its dependent substream declares `chanmap = 0x1A00` (Ls, Rs, Lrs/Rrs). Reading
`chan_loc` back out of the resulting `dec3`:

```
before this patch : chan_loc = 0x010   [Lsd/Rsd]
after this patch  : chan_loc = 0x002   [Lrs/Rrs]
GPAC MP4Box       : chan_loc = 0x002   [Lrs/Rrs]     (same input, for comparison)
```

An independent muxer agreeing with the patched output seemed worth checking
before assuming FFmpeg was the one in the wrong here.

A decoder that reads the container shows the difference directly. Apple's
AudioToolbox (`afconvert`/`afinfo`) on the same input:

```
unpatched : 7.1 (L C R Ls Rs LFE Lsd Rsd)
patched   : 7.1 (L C R Ls Rs LFE Rls Rrs)
```

On that decoder the practical result of the unpatched output is that the track
collapses — measured on a commercially authored DD+ 7.1 title, the centre
channel, all four surrounds and the LFE decoded to silence, leaving a quiet
stereo downmix. Rewriting that one byte restores all eight channels.

# Environment

Reproduced and fixed against current master, `eb0bfa852e` (`avutil/opt:
reformat set_string_binary`):

```
ffmpeg version git-2026-08-21-eb0bfa8 Copyright (c) 2000-2026 the FFmpeg developers
built with Apple clang version 21.0.0 (clang-2100.1.1.101)
configuration: --disable-everything --disable-autodetect --disable-doc
  --disable-network --disable-avdevice --disable-swscale
  --enable-protocol='file,pipe' --enable-demuxer='eac3,ac3,mov,matroska'
  --enable-muxer='mov,mp4,eac3' --enable-decoder='eac3,ac3' --enable-parser=ac3
```

macOS 15 (Darwin 25.6.0), Apple Silicon. Also reproduced on the 8.1.2 release
build. The line is unchanged in master; the decoder-side dependent-substream
support added in 4.0 (`ae92970`) is unrelated.

Nothing below 7.1 is affected — those carry no dependent substream, so
`num_dep_sub` is 0 and `chan_loc` is never written.



From 782e7b6e4cc50318911557041135bac2681bf23d Mon Sep 17 00:00:00 2001
From: Brian Leake <[email protected]>
Date: Fri, 21 Aug 2026 16:03:12 -0700
Subject: [PATCH] avformat/movenc: fix dec3 chan_loc derivation for E-AC-3
 dependent substreams

chan_loc was derived from the bitstream's chanmap with a plain shift, but
the two fields use opposite bit orders, so every E-AC-3 stream carrying a
dependent substream (7.1 and above) was muxed with the wrong channel
layout signalled in the container.

chanmap is a 16-bit field read MSB-first, so flag index i sits at bit
(15 - i); this ordering is the same one ff_eac3_custom_channel_map_locations
is indexed against in ac3_parser.c. chan_loc bit j carries chanmap flag
index 5 + j per ETSI TS 102 366 Annex F, i.e. bit (10 - j). Converting
between them is a bit reversal rather than a shift. The 0x1f mask was also
5 bits where 9 are needed, so some flags were dropped rather than merely
misplaced.

Effect on the layouts that use a dependent substream:

    layout   chanmap   before   after
    7.1      0x1A00    0x010    0x002
    5.1.2    0x0010    0x000    0x040
    5.1.4    0x0210    0x010    0x042

For 7.1 the field claimed Lsd/Rsd (surround direct), a position no 7.1
speaker layout has, instead of Lrs/Rrs. This is invisible to FFmpeg, whose
decoder reads the layout from the bitstream, but demuxers that trust the
container act on it: Apple's AudioToolbox reported the affected files as
"7.1 (L C R Ls Rs LFE Lsd Rsd)" and collapsed them to a stereo downmix,
losing the centre channel, all four surrounds and the LFE.

Verified with samples.ffmpeg.org/A-codecs/AC3/eac3/7_pt_1.eac3, whose
dependent substream declares chanmap 0x1A00. After this change the muxed
dec3 carries 0x002, matching GPAC MP4Box for the same input, and
AudioToolbox reports "7.1 (L C R Ls Rs LFE Rls Rrs)" with all eight
channels intact.

Signed-off-by: Brian Leake <[email protected]>
---
 libavformat/movenc.c | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/libavformat/movenc.c b/libavformat/movenc.c
index 367caecee9..9aef241a37 100644
--- a/libavformat/movenc.c
+++ b/libavformat/movenc.c
@@ -572,10 +572,18 @@ static int handle_eac3(MOVMuxContext *mov, AVPacket *pkt, MOVTrack *track)
                 ret /= 8;
 
                 /* get the dependent stream channel map, if exists */
-                if (hdr->channel_map_present)
-                    info->substream[parent].chan_loc |= (hdr->channel_map >> 5) & 0x1f;
-                else
+                if (hdr->channel_map_present) {
+                    /* chanmap is a 16-bit field read MSB-first, so flag index
+                     * i sits at bit (15 - i). chan_loc bit j carries chanmap
+                     * flag index 5 + j, i.e. bit (10 - j). Converting between
+                     * the two is a bit reversal, not a shift. */
+                    for (int j = 0; j < 9; j++) {
+                        if ((hdr->channel_map >> (10 - j)) & 1)
+                            info->substream[parent].chan_loc |= 1 << j;
+                    }
+                } else {
                     info->substream[parent].chan_loc |= hdr->channel_mode;
+                }
                 cumul_size += hdr->frame_size;
             }
         }
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.