PR #24521 opened by Romain Beauxis (toots)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24521
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24521.patch

This PR reworks the Xing header parsing/rendering logic:
* Finish parsing all available fields
* Add a generic packer API + tests
* Use the generic packer API to enhance mp3 encoding round trips: validates 
inconsistent values, properly signal joint stereo etc.

Fixes #24129 and its duplicate https://trac.ffmpeg.org/ticket/4954


>From 7cd7b243e937b2591461261f4c7ae89fdecc1498 Mon Sep 17 00:00:00 2001
From: Romain Beauxis <[email protected]>
Date: Tue, 15 Sep 2026 10:41:39 -0500
Subject: [PATCH 1/3] avcodec/mpegaudiodecheader: parse all remaining header
 fields

Parse the bitrate index, padding, private bit, copyright, original and
emphasis fields, so that a parsed header holds every field of the
bitstream header.
---
 doc/APIchanges                  |  4 ++++
 libavcodec/mpegaudiodecheader.c | 10 ++++++----
 libavcodec/mpegaudiodecheader.h |  8 +++++++-
 libavcodec/version.h            |  2 +-
 4 files changed, 18 insertions(+), 6 deletions(-)

diff --git a/doc/APIchanges b/doc/APIchanges
index 5d81cd92d7..b67e71d317 100644
--- a/doc/APIchanges
+++ b/doc/APIchanges
@@ -2,6 +2,10 @@ The last version increases of all libraries were on 2026-06-23.
 
 API changes, most recent first:
 
+2026-09-15 - xxxxxxxxxx - lavc 63.14.100 - mpegaudiodecheader.h
+  Add copyright, original, emphasis, bitrate_index, padding and private_bit
+  to MPADecodeHeader, filled by avpriv_mpegaudio_decode_header().
+
 2026-09-12 - xxxxxxxxxx - lavc 63.13.100 - codec_id.h, defs.h
   Add AV_CODEC_ID_ASTC and the AV_PROFILE_ASTC_* profile values.
 
diff --git a/libavcodec/mpegaudiodecheader.c b/libavcodec/mpegaudiodecheader.c
index ef63befbf4..8a86d85500 100644
--- a/libavcodec/mpegaudiodecheader.c
+++ b/libavcodec/mpegaudiodecheader.c
@@ -62,12 +62,14 @@ int avpriv_mpegaudio_decode_header(MPADecodeHeader *s, 
uint32_t header)
 
     bitrate_index = (header >> 12) & 0xf;
     padding = (header >> 9) & 1;
-    //extension = (header >> 8) & 1;
+    s->bitrate_index = bitrate_index;
+    s->padding = padding;
+    s->private_bit = (header >> 8) & 1;
     s->mode = (header >> 6) & 3;
     s->mode_ext = (header >> 4) & 3;
-    //copyright = (header >> 3) & 1;
-    //original = (header >> 2) & 1;
-    //emphasis = header & 3;
+    s->copyright = (header >> 3) & 1;
+    s->original = (header >> 2) & 1;
+    s->emphasis = header & 3;
 
     if (s->mode == MPA_MONO)
         s->nb_channels = 1;
diff --git a/libavcodec/mpegaudiodecheader.h b/libavcodec/mpegaudiodecheader.h
index ed5d1f3b33..42c8736fa6 100644
--- a/libavcodec/mpegaudiodecheader.h
+++ b/libavcodec/mpegaudiodecheader.h
@@ -42,7 +42,13 @@
     int nb_channels; \
     int mode; \
     int mode_ext; \
-    int lsf;
+    int lsf; \
+    int copyright; \
+    int original; \
+    int emphasis; \
+    int bitrate_index; \
+    int padding; \
+    int private_bit;
 
 typedef struct MPADecodeHeader {
   MPA_DECODE_HEADER
diff --git a/libavcodec/version.h b/libavcodec/version.h
index 755c90bbc1..7531c6c42a 100644
--- a/libavcodec/version.h
+++ b/libavcodec/version.h
@@ -29,7 +29,7 @@
 
 #include "version_major.h"
 
-#define LIBAVCODEC_VERSION_MINOR  13
+#define LIBAVCODEC_VERSION_MINOR  14
 #define LIBAVCODEC_VERSION_MICRO 100
 
 #define LIBAVCODEC_VERSION_INT  AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
-- 
2.52.0


>From 1dbcd5e46c19d10515cf27f44b84160aa07be500 Mon Sep 17 00:00:00 2001
From: Romain Beauxis <[email protected]>
Date: Tue, 15 Sep 2026 10:41:39 -0500
Subject: [PATCH 2/3] avcodec/mpegaudiodecheader: add a header packer

ff_mpa_encode_header() is the inverse of avpriv_mpegaudio_decode_header().
A test program walks the frames of a file, checks that every header
encodes back to its original bits and prints the header fields.
---
 libavcodec/Makefile                           |   1 +
 libavcodec/mpegaudiodecheader.h               |  19 +++
 libavcodec/tests/.gitignore                   |   1 +
 libavcodec/tests/mpegaudiodecheader.c         | 120 ++++++++++++++++++
 tests/fate/mp3.mak                            |  38 ++++++
 tests/ref/fate/mpegaudiodecheader-bitrates    |  15 +++
 tests/ref/fate/mpegaudiodecheader-hecommon    |  10 ++
 .../ref/fate/mpegaudiodecheader-joint-stereo  |   2 +
 tests/ref/fate/mpegaudiodecheader-libmp3lame  |   3 +
 tests/ref/fate/mpegaudiodecheader-mp2         |   2 +
 tests/ref/fate/mpegaudiodecheader-mp3-remux   |   3 +
 tests/ref/fate/mpegaudiodecheader-mpeg25      |   3 +
 tests/ref/fate/mpegaudiodecheader-xing        |   3 +
 13 files changed, 220 insertions(+)
 create mode 100644 libavcodec/tests/mpegaudiodecheader.c
 create mode 100644 tests/ref/fate/mpegaudiodecheader-bitrates
 create mode 100644 tests/ref/fate/mpegaudiodecheader-hecommon
 create mode 100644 tests/ref/fate/mpegaudiodecheader-joint-stereo
 create mode 100644 tests/ref/fate/mpegaudiodecheader-libmp3lame
 create mode 100644 tests/ref/fate/mpegaudiodecheader-mp2
 create mode 100644 tests/ref/fate/mpegaudiodecheader-mp3-remux
 create mode 100644 tests/ref/fate/mpegaudiodecheader-mpeg25
 create mode 100644 tests/ref/fate/mpegaudiodecheader-xing

diff --git a/libavcodec/Makefile b/libavcodec/Makefile
index a5c71995ea..e2c60e9051 100644
--- a/libavcodec/Makefile
+++ b/libavcodec/Makefile
@@ -1393,6 +1393,7 @@ TESTPROGS-$(CONFIG_GOLOMB)                += golomb
 TESTPROGS-$(CONFIG_IDCTDSP)               += dct
 TESTPROGS-$(CONFIG_DXV_ENCODER)           += hashtable
 TESTPROGS-$(CONFIG_MJPEG_ENCODER)         += mjpegenc_huffman
+TESTPROGS-$(CONFIG_MPEGAUDIOHEADER)       += mpegaudiodecheader
 TESTPROGS-$(CONFIG_MPEGVIDEO)             += mpeg12framerate
 TESTPROGS-$(CONFIG_H264_METADATA_BSF)     += h264_levels
 TESTPROGS-$(CONFIG_HEVC_METADATA_BSF)     += h265_levels
diff --git a/libavcodec/mpegaudiodecheader.h b/libavcodec/mpegaudiodecheader.h
index 42c8736fa6..d4802c2bb9 100644
--- a/libavcodec/mpegaudiodecheader.h
+++ b/libavcodec/mpegaudiodecheader.h
@@ -84,4 +84,23 @@ static inline int ff_mpa_check_header(uint32_t header){
     return 0;
 }
 
+static inline uint32_t ff_mpa_encode_header(const MPADecodeHeader *s)
+{
+    int version = s->sample_rate_index < 6 ? 3 - s->sample_rate_index / 3 : 0;
+
+    return 0xffeU                   << 20 |
+           version                  << 19 |
+           (4 - s->layer)           << 17 |
+           !s->error_protection     << 16 |
+           s->bitrate_index         << 12 |
+           s->sample_rate_index % 3 << 10 |
+           s->padding               <<  9 |
+           s->private_bit           <<  8 |
+           s->mode                  <<  6 |
+           s->mode_ext              <<  4 |
+           s->copyright             <<  3 |
+           s->original              <<  2 |
+           s->emphasis;
+}
+
 #endif /* AVCODEC_MPEGAUDIODECHEADER_H */
diff --git a/libavcodec/tests/.gitignore b/libavcodec/tests/.gitignore
index 4977c63e00..8e4f26c2ad 100644
--- a/libavcodec/tests/.gitignore
+++ b/libavcodec/tests/.gitignore
@@ -18,5 +18,6 @@
 /mathops
 /mjpegenc_huffman
 /mpeg12framerate
+/mpegaudiodecheader
 /rangecoder
 /snowenc
diff --git a/libavcodec/tests/mpegaudiodecheader.c 
b/libavcodec/tests/mpegaudiodecheader.c
new file mode 100644
index 0000000000..cf711bb835
--- /dev/null
+++ b/libavcodec/tests/mpegaudiodecheader.c
@@ -0,0 +1,120 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * Walks the MPEG audio frames of a file, checks that every frame header
+ * encodes back to its original bits, and prints each distinct header.
+ */
+
+#include <inttypes.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "libavutil/file.h"
+#include "libavutil/intreadwrite.h"
+
+#include "libavcodec/mpegaudiodecheader.h"
+
+#define PADDING_BIT (1U << 9)
+
+static size_t id3v2_size(const uint8_t *buf, size_t size)
+{
+    if (size < 10 || memcmp(buf, "ID3", 3))
+        return 0;
+
+    return 10 + (buf[5] & 0x10 ? 10 : 0) +
+           ((buf[6] & 0x7f) << 21 | (buf[7] & 0x7f) << 14 |
+            (buf[8] & 0x7f) <<  7 | (buf[9] & 0x7f));
+}
+
+static void print_header(size_t offset, unsigned frames, uint32_t bits,
+                         const MPADecodeHeader *header)
+{
+    printf("offset=%zu frames=%u header=%08"PRIX32" layer=%d sample_rate=%d "
+           "bit_rate=%d crc=%d mode=%d mode_ext=%d copyright=%d original=%d "
+           "emphasis=%d private_bit=%d\n",
+           offset, frames, bits, header->layer, header->sample_rate,
+           header->bit_rate, header->error_protection, header->mode,
+           header->mode_ext, header->copyright, header->original,
+           header->emphasis, header->private_bit);
+}
+
+int main(int argc, char *argv[])
+{
+    MPADecodeHeader run_header;
+    uint8_t *buf;
+    size_t size, pos, run_offset = 0;
+    uint32_t run_bits = 0;
+    unsigned run_frames = 0, total_frames = 0;
+    int ret = 1;
+
+    if (argc != 2) {
+        fprintf(stderr, "Usage: %s <file>\n", argv[0]);
+        return 1;
+    }
+
+    if (av_file_map(argv[1], &buf, &size, 0, NULL) < 0) {
+        fprintf(stderr, "Failed to open '%s'\n", argv[1]);
+        return 1;
+    }
+
+    pos = id3v2_size(buf, size);
+    while (pos + 4 <= size) {
+        uint32_t bits = AV_RB32(buf + pos);
+        MPADecodeHeader header;
+        uint32_t encoded;
+        int decoded;
+
+        if (ff_mpa_check_header(bits) < 0) {
+            pos++;
+            continue;
+        }
+
+        decoded = avpriv_mpegaudio_decode_header(&header, bits);
+        encoded = ff_mpa_encode_header(&header);
+        if (encoded != bits) {
+            printf("offset=%zu header=%08"PRIX32" encoded back as 
%08"PRIX32"\n",
+                   pos, bits, encoded);
+            goto end;
+        }
+
+        if (run_frames && (bits & ~PADDING_BIT) != (run_bits & ~PADDING_BIT)) {
+            print_header(run_offset, run_frames, run_bits, &run_header);
+            run_frames = 0;
+        }
+        if (!run_frames) {
+            run_offset = pos;
+            run_bits   = bits;
+            run_header = header;
+        }
+        run_frames++;
+        total_frames++;
+
+        pos += decoded == 0 ? header.frame_size : 1;
+    }
+    if (run_frames)
+        print_header(run_offset, run_frames, run_bits, &run_header);
+
+    printf("frames=%u\n", total_frames);
+    ret = !total_frames;
+
+end:
+    av_file_unmap(buf, size);
+    return ret;
+}
diff --git a/tests/fate/mp3.mak b/tests/fate/mp3.mak
index 145ce72a6e..edce1f98e6 100644
--- a/tests/fate/mp3.mak
+++ b/tests/fate/mp3.mak
@@ -40,3 +40,41 @@ FATE_MP3-$(call DEMDEC, MP3, MP3FLOAT, ARESAMPLE_FILTER 
PIPE_PROTOCOL) += $(FATE
 
 FATE_SAMPLES_AVCONV += $(FATE_MP3-yes)
 fate-mp3: $(FATE_MP3-yes)
+
+MPA_HEADER_TESTBIN = libavcodec/tests/mpegaudiodecheader$(EXESUF)
+
+FATE_MPA_HEADER-$(CONFIG_MPEGAUDIOHEADER) += fate-mpegaudiodecheader-hecommon
+fate-mpegaudiodecheader-hecommon: $(MPA_HEADER_TESTBIN)
+fate-mpegaudiodecheader-hecommon: CMD = run "$(MPA_HEADER_TESTBIN)" 
"$(TARGET_SAMPLES)/mp3-conformance/hecommon.bit"
+
+FATE_MPA_HEADER-$(CONFIG_MPEGAUDIOHEADER) += fate-mpegaudiodecheader-bitrates
+fate-mpegaudiodecheader-bitrates: $(MPA_HEADER_TESTBIN)
+fate-mpegaudiodecheader-bitrates: CMD = run "$(MPA_HEADER_TESTBIN)" 
"$(TARGET_SAMPLES)/mp3-conformance/he_32khz.bit"
+
+FATE_MPA_HEADER-$(CONFIG_MPEGAUDIOHEADER) += fate-mpegaudiodecheader-mpeg25
+fate-mpegaudiodecheader-mpeg25: $(MPA_HEADER_TESTBIN)
+fate-mpegaudiodecheader-mpeg25: CMD = run "$(MPA_HEADER_TESTBIN)" 
"$(TARGET_SAMPLES)/id3v2/lang_xxx.mp3"
+
+FATE_MPA_HEADER-$(CONFIG_MPEGAUDIOHEADER) += 
fate-mpegaudiodecheader-joint-stereo
+fate-mpegaudiodecheader-joint-stereo: $(MPA_HEADER_TESTBIN)
+fate-mpegaudiodecheader-joint-stereo: CMD = run "$(MPA_HEADER_TESTBIN)" 
"$(TARGET_SAMPLES)/mp3-conformance/sin1k0db.bit"
+
+FATE_MPA_HEADER-$(CONFIG_MPEGAUDIOHEADER) += fate-mpegaudiodecheader-xing
+fate-mpegaudiodecheader-xing: $(MPA_HEADER_TESTBIN)
+fate-mpegaudiodecheader-xing: CMD = run "$(MPA_HEADER_TESTBIN)" 
"$(TARGET_SAMPLES)/exif/embedded_small.mp3"
+
+FATE_MPA_HEADER-$(call ALLYES, MPEGAUDIOHEADER MP3_DEMUXER MP3FLOAT_DECODER 
ARESAMPLE_FILTER MP2_ENCODER MP2_MUXER FILE_PROTOCOL) += 
fate-mpegaudiodecheader-mp2
+fate-mpegaudiodecheader-mp2: $(MPA_HEADER_TESTBIN)
+fate-mpegaudiodecheader-mp2: CMD = run_with_temp "$(FFMPEG) -nostdin 
-hide_banner -loglevel error -i $(TARGET_SAMPLES)/mp3-conformance/compl.bit -ar 
24000 -c:a mp2 -fflags +bitexact -f mp2 -y" "$(MPA_HEADER_TESTBIN)" mp2
+
+FATE_MPA_HEADER-$(call REMUX, MP3, MPEGAUDIOHEADER) += 
fate-mpegaudiodecheader-mp3-remux
+fate-mpegaudiodecheader-mp3-remux: $(MPA_HEADER_TESTBIN)
+fate-mpegaudiodecheader-mp3-remux: CMD = run_with_temp "$(FFMPEG) -nostdin 
-hide_banner -loglevel error -i $(TARGET_SAMPLES)/exif/embedded_small.mp3 -map 
0:a -c copy -fflags +bitexact -f mp3 -y" "$(MPA_HEADER_TESTBIN)" mp3
+
+FATE_MPA_HEADER_LAME-$(call ENCDEC, LIBMP3LAME PCM_S16LE, MP3 WAV, 
MPEGAUDIOHEADER PAN_FILTER ARESAMPLE_FILTER) += 
fate-mpegaudiodecheader-libmp3lame
+fate-mpegaudiodecheader-libmp3lame: tests/data/asynth-44100-2.wav 
$(MPA_HEADER_TESTBIN)
+fate-mpegaudiodecheader-libmp3lame: CMD = run_with_temp "$(FFMPEG) -nostdin 
-hide_banner -loglevel error -i $(TARGET_PATH)/tests/data/asynth-44100-2.wav 
-af pan=stereo|c0=c0|c1=c0 -c:a libmp3lame -b:a 128k -fflags +bitexact -f mp3 
-y" "$(MPA_HEADER_TESTBIN)" mp3
+
+FATE_SAMPLES_FFMPEG += $(FATE_MPA_HEADER-yes)
+FATE_FFMPEG += $(FATE_MPA_HEADER_LAME-yes)
+fate-mpegaudiodecheader: $(FATE_MPA_HEADER-yes) $(FATE_MPA_HEADER_LAME-yes)
diff --git a/tests/ref/fate/mpegaudiodecheader-bitrates 
b/tests/ref/fate/mpegaudiodecheader-bitrates
new file mode 100644
index 0000000000..2b41fc573b
--- /dev/null
+++ b/tests/ref/fate/mpegaudiodecheader-bitrates
@@ -0,0 +1,15 @@
+offset=0 frames=10 header=FFFB18C0 layer=3 sample_rate=32000 bit_rate=32000 
crc=0 mode=3 mode_ext=0 copyright=0 original=0 emphasis=0 private_bit=0
+offset=1440 frames=10 header=FFFB28C0 layer=3 sample_rate=32000 bit_rate=40000 
crc=0 mode=3 mode_ext=0 copyright=0 original=0 emphasis=0 private_bit=0
+offset=3240 frames=10 header=FFFB38C0 layer=3 sample_rate=32000 bit_rate=48000 
crc=0 mode=3 mode_ext=0 copyright=0 original=0 emphasis=0 private_bit=0
+offset=5400 frames=10 header=FFFB48C0 layer=3 sample_rate=32000 bit_rate=56000 
crc=0 mode=3 mode_ext=0 copyright=0 original=0 emphasis=0 private_bit=0
+offset=7920 frames=10 header=FFFB58C0 layer=3 sample_rate=32000 bit_rate=64000 
crc=0 mode=3 mode_ext=0 copyright=0 original=0 emphasis=0 private_bit=0
+offset=10800 frames=10 header=FFFB68C0 layer=3 sample_rate=32000 
bit_rate=80000 crc=0 mode=3 mode_ext=0 copyright=0 original=0 emphasis=0 
private_bit=0
+offset=14400 frames=10 header=FFFB78C0 layer=3 sample_rate=32000 
bit_rate=96000 crc=0 mode=3 mode_ext=0 copyright=0 original=0 emphasis=0 
private_bit=0
+offset=18720 frames=10 header=FFFB88C0 layer=3 sample_rate=32000 
bit_rate=112000 crc=0 mode=3 mode_ext=0 copyright=0 original=0 emphasis=0 
private_bit=0
+offset=23760 frames=10 header=FFFB98C0 layer=3 sample_rate=32000 
bit_rate=128000 crc=0 mode=3 mode_ext=0 copyright=0 original=0 emphasis=0 
private_bit=0
+offset=29520 frames=10 header=FFFBA8C0 layer=3 sample_rate=32000 
bit_rate=160000 crc=0 mode=3 mode_ext=0 copyright=0 original=0 emphasis=0 
private_bit=0
+offset=36720 frames=10 header=FFFBB8C0 layer=3 sample_rate=32000 
bit_rate=192000 crc=0 mode=3 mode_ext=0 copyright=0 original=0 emphasis=0 
private_bit=0
+offset=45360 frames=10 header=FFFBC8C0 layer=3 sample_rate=32000 
bit_rate=224000 crc=0 mode=3 mode_ext=0 copyright=0 original=0 emphasis=0 
private_bit=0
+offset=55440 frames=10 header=FFFBD8C0 layer=3 sample_rate=32000 
bit_rate=256000 crc=0 mode=3 mode_ext=0 copyright=0 original=0 emphasis=0 
private_bit=0
+offset=66960 frames=20 header=FFFBE8C0 layer=3 sample_rate=32000 
bit_rate=320000 crc=0 mode=3 mode_ext=0 copyright=0 original=0 emphasis=0 
private_bit=0
+frames=150
diff --git a/tests/ref/fate/mpegaudiodecheader-hecommon 
b/tests/ref/fate/mpegaudiodecheader-hecommon
new file mode 100644
index 0000000000..4ea48dced2
--- /dev/null
+++ b/tests/ref/fate/mpegaudiodecheader-hecommon
@@ -0,0 +1,10 @@
+offset=0 frames=5 header=FFFB9000 layer=3 sample_rate=44100 bit_rate=128000 
crc=0 mode=0 mode_ext=0 copyright=0 original=0 emphasis=0 private_bit=0
+offset=2089 frames=1 header=FFFA9200 layer=3 sample_rate=44100 bit_rate=128000 
crc=1 mode=0 mode_ext=0 copyright=0 original=0 emphasis=0 private_bit=0
+offset=2507 frames=1 header=FFFA9300 layer=3 sample_rate=44100 bit_rate=128000 
crc=1 mode=0 mode_ext=0 copyright=0 original=0 emphasis=0 private_bit=1
+offset=2925 frames=1 header=FFFA9208 layer=3 sample_rate=44100 bit_rate=128000 
crc=1 mode=0 mode_ext=0 copyright=1 original=0 emphasis=0 private_bit=0
+offset=3343 frames=1 header=FFFA9204 layer=3 sample_rate=44100 bit_rate=128000 
crc=1 mode=0 mode_ext=0 copyright=0 original=1 emphasis=0 private_bit=0
+offset=3761 frames=1 header=FFFA9201 layer=3 sample_rate=44100 bit_rate=128000 
crc=1 mode=0 mode_ext=0 copyright=0 original=0 emphasis=1 private_bit=0
+offset=4179 frames=1 header=FFFA9202 layer=3 sample_rate=44100 bit_rate=128000 
crc=1 mode=0 mode_ext=0 copyright=0 original=0 emphasis=2 private_bit=0
+offset=4597 frames=1 header=FFFA9203 layer=3 sample_rate=44100 bit_rate=128000 
crc=1 mode=0 mode_ext=0 copyright=0 original=0 emphasis=3 private_bit=0
+offset=5015 frames=18 header=FFFA9200 layer=3 sample_rate=44100 
bit_rate=128000 crc=1 mode=0 mode_ext=0 copyright=0 original=0 emphasis=0 
private_bit=0
+frames=30
diff --git a/tests/ref/fate/mpegaudiodecheader-joint-stereo 
b/tests/ref/fate/mpegaudiodecheader-joint-stereo
new file mode 100644
index 0000000000..b813cad988
--- /dev/null
+++ b/tests/ref/fate/mpegaudiodecheader-joint-stereo
@@ -0,0 +1,2 @@
+offset=215 frames=318 header=FFFB9260 layer=3 sample_rate=44100 
bit_rate=128000 crc=0 mode=1 mode_ext=2 copyright=0 original=0 emphasis=0 
private_bit=0
+frames=318
diff --git a/tests/ref/fate/mpegaudiodecheader-libmp3lame 
b/tests/ref/fate/mpegaudiodecheader-libmp3lame
new file mode 100644
index 0000000000..3955a43426
--- /dev/null
+++ b/tests/ref/fate/mpegaudiodecheader-libmp3lame
@@ -0,0 +1,3 @@
+offset=20 frames=1 header=FFFB9000 layer=3 sample_rate=44100 bit_rate=128000 
crc=0 mode=0 mode_ext=0 copyright=0 original=0 emphasis=0 private_bit=0
+offset=437 frames=231 header=FFFB9064 layer=3 sample_rate=44100 
bit_rate=128000 crc=0 mode=1 mode_ext=2 copyright=0 original=1 emphasis=0 
private_bit=0
+frames=232
diff --git a/tests/ref/fate/mpegaudiodecheader-mp2 
b/tests/ref/fate/mpegaudiodecheader-mp2
new file mode 100644
index 0000000000..df9e9ff8d7
--- /dev/null
+++ b/tests/ref/fate/mpegaudiodecheader-mp2
@@ -0,0 +1,2 @@
+offset=0 frames=109 header=FFF5E4C4 layer=2 sample_rate=24000 bit_rate=160000 
crc=0 mode=3 mode_ext=0 copyright=0 original=1 emphasis=0 private_bit=0
+frames=109
diff --git a/tests/ref/fate/mpegaudiodecheader-mp3-remux 
b/tests/ref/fate/mpegaudiodecheader-mp3-remux
new file mode 100644
index 0000000000..c5d4d5cdd7
--- /dev/null
+++ b/tests/ref/fate/mpegaudiodecheader-mp3-remux
@@ -0,0 +1,3 @@
+offset=301 frames=1 header=FFFB9000 layer=3 sample_rate=44100 bit_rate=128000 
crc=0 mode=0 mode_ext=0 copyright=0 original=0 emphasis=0 private_bit=0
+offset=718 frames=21 header=FFFB9064 layer=3 sample_rate=44100 bit_rate=128000 
crc=0 mode=1 mode_ext=2 copyright=0 original=1 emphasis=0 private_bit=0
+frames=22
diff --git a/tests/ref/fate/mpegaudiodecheader-mpeg25 
b/tests/ref/fate/mpegaudiodecheader-mpeg25
new file mode 100644
index 0000000000..c3a11dcd37
--- /dev/null
+++ b/tests/ref/fate/mpegaudiodecheader-mpeg25
@@ -0,0 +1,3 @@
+offset=1062 frames=1 header=FFE338C0 layer=3 sample_rate=8000 bit_rate=24000 
crc=0 mode=3 mode_ext=0 copyright=0 original=0 emphasis=0 private_bit=0
+offset=1278 frames=4 header=FFE318C4 layer=3 sample_rate=8000 bit_rate=8000 
crc=0 mode=3 mode_ext=0 copyright=0 original=1 emphasis=0 private_bit=0
+frames=5
diff --git a/tests/ref/fate/mpegaudiodecheader-xing 
b/tests/ref/fate/mpegaudiodecheader-xing
new file mode 100644
index 0000000000..fbaafedc48
--- /dev/null
+++ b/tests/ref/fate/mpegaudiodecheader-xing
@@ -0,0 +1,3 @@
+offset=16110 frames=1 header=FFFB4000 layer=3 sample_rate=44100 bit_rate=56000 
crc=0 mode=0 mode_ext=0 copyright=0 original=0 emphasis=0 private_bit=0
+offset=16292 frames=21 header=FFFB9064 layer=3 sample_rate=44100 
bit_rate=128000 crc=0 mode=1 mode_ext=2 copyright=0 original=1 emphasis=0 
private_bit=0
+frames=22
-- 
2.52.0


>From e2104b5b9f669678e146060c8f065fc7ad51097a Mon Sep 17 00:00:00 2001
From: Romain Beauxis <[email protected]>
Date: Tue, 15 Sep 2026 10:41:39 -0500
Subject: [PATCH 3/3] avformat/mp3enc: build the Xing frame header from the
 first audio frame

Some hardware decoders reject files whose Xing frame header differs from
the audio frames, e.g. joint stereo from libmp3lame. Fail when the first
frame does not match the Xing frame layout.

Fixes #24129.
---
 libavformat/mp3enc.c                         | 61 +++++++++++++-------
 tests/ref/fate/gapless-mp3-remux             |  2 +-
 tests/ref/fate/mpegaudiodecheader-libmp3lame |  3 +-
 tests/ref/fate/mpegaudiodecheader-mp3-remux  |  3 +-
 4 files changed, 44 insertions(+), 25 deletions(-)

diff --git a/libavformat/mp3enc.c b/libavformat/mp3enc.c
index 6d42feb1ff..c11f5daa9e 100644
--- a/libavformat/mp3enc.c
+++ b/libavformat/mp3enc.c
@@ -126,6 +126,9 @@ typedef struct MP3Context {
     uint64_t bag[XING_NUM_BAGS];
     int initial_bitrate;
     int has_variable_bitrate;
+    MPADecodeHeader xing_header;
+    MPADecodeHeader first_frame;
+    int first_frame_seen;
     int delay;
     int64_t padding;
 
@@ -149,14 +152,13 @@ static int mp3_write_xing(AVFormatContext *s)
     AVCodecParameters *par = s->streams[mp3->audio_stream_idx]->codecpar;
     AVDictionaryEntry *enc = 
av_dict_get(s->streams[mp3->audio_stream_idx]->metadata, "encoder", NULL, 0);
     AVIOContext *dyn_ctx;
-    int32_t        header;
-    MPADecodeHeader  mpah;
-    int srate_idx, i, channels;
+    uint32_t       header;
+    MPADecodeHeader  mpah = { .layer = 3 };
+    int srate_idx, i;
     int bitrate_idx;
     int best_bitrate_idx = -1;
     int best_bitrate_error = INT_MAX;
     int ret;
-    int ver = 0;
     int bytes_needed;
 
     if (!(s->pb->seekable & AVIO_SEEKABLE_NORMAL) || !mp3->write_xing)
@@ -165,12 +167,11 @@ static int mp3_write_xing(AVFormatContext *s)
     for (i = 0; i < FF_ARRAY_ELEMS(ff_mpa_freq_tab); i++) {
         const uint16_t base_freq = ff_mpa_freq_tab[i];
 
-        if      (par->sample_rate == base_freq)     ver = 0x3; // MPEG 1
-        else if (par->sample_rate == base_freq / 2) ver = 0x2; // MPEG 2
-        else if (par->sample_rate == base_freq / 4) ver = 0x0; // MPEG 2.5
+        if      (par->sample_rate == base_freq)     srate_idx = i;     // MPEG 
1
+        else if (par->sample_rate == base_freq / 2) srate_idx = i + 3; // MPEG 
2
+        else if (par->sample_rate == base_freq / 4) srate_idx = i + 6; // MPEG 
2.5
         else continue;
 
-        srate_idx = i;
         break;
     }
     if (i == FF_ARRAY_ELEMS(ff_mpa_freq_tab)) {
@@ -179,21 +180,17 @@ static int mp3_write_xing(AVFormatContext *s)
     }
 
     switch (par->ch_layout.nb_channels) {
-    case 1:  channels = MPA_MONO;                                          
break;
-    case 2:  channels = MPA_STEREO;                                        
break;
+    case 1:  mpah.mode = MPA_MONO;                                         
break;
+    case 2:  mpah.mode = MPA_STEREO;                                       
break;
     default: av_log(s, AV_LOG_WARNING, "Unsupported number of channels, "
                     "not writing Xing header.\n");
              return -1;
     }
 
-    /* dummy MPEG audio header */
-    header  =  0xffU                                 << 24; // sync
-    header |= (0x7 << 5 | ver << 3 | 0x1 << 1 | 0x1) << 16; // 
sync/audio-version/layer 3/no crc*/
-    header |= (srate_idx << 2) << 8;
-    header |= channels << 6;
+    mpah.sample_rate_index = srate_idx;
 
     for (bitrate_idx = 1; bitrate_idx < 15; bitrate_idx++) {
-        int bit_rate = 1000 * ff_mpa_bitrate_tab[ver != 3][3 - 1][bitrate_idx];
+        int bit_rate = 1000 * ff_mpa_bitrate_tab[srate_idx >= 3][3 - 
1][bitrate_idx];
         int error    = FFABS(bit_rate - par->bit_rate);
 
         if (error < best_bitrate_error) {
@@ -204,10 +201,10 @@ static int mp3_write_xing(AVFormatContext *s)
     av_assert0(best_bitrate_idx >= 0);
 
     for (bitrate_idx = best_bitrate_idx; ; bitrate_idx++) {
-        int32_t mask = bitrate_idx << (4 + 8);
         if (15 == bitrate_idx)
             return -1;
-        header |= mask;
+        mpah.bitrate_index = bitrate_idx;
+        header = ff_mpa_encode_header(&mpah);
 
         ret = avpriv_mpegaudio_decode_header(&mpah, header);
         av_assert0(ret >= 0);
@@ -216,9 +213,8 @@ static int mp3_write_xing(AVFormatContext *s)
 
         if (bytes_needed <= mpah.frame_size)
             break;
-
-        header &= ~mask;
     }
+    mp3->xing_header = mpah;
 
     ret = avio_open_dyn_buf(&dyn_ctx);
     if (ret < 0)
@@ -332,6 +328,22 @@ static int mp3_write_audio_packet(AVFormatContext *s, 
AVPacket *pkt)
                 mp3->initial_bitrate = mpah.bit_rate;
             if ((mpah.bit_rate == 0) || (mp3->initial_bitrate != 
mpah.bit_rate))
                 mp3->has_variable_bitrate = 1;
+            if (!mp3->first_frame_seen) {
+                const MPADecodeHeader *xing = &mp3->xing_header;
+
+                if (mp3->xing_offset &&
+                    (mpah.layer       != xing->layer       ||
+                     mpah.sample_rate != xing->sample_rate ||
+                     mpah.nb_channels != xing->nb_channels)) {
+                    av_log(s, AV_LOG_ERROR, "First audio frame (layer %d, %d 
Hz, %d channels) "
+                           "does not match the stream (layer %d, %d Hz, %d 
channels).\n",
+                           mpah.layer, mpah.sample_rate, mpah.nb_channels,
+                           xing->layer, xing->sample_rate, xing->nb_channels);
+                    return AVERROR_INVALIDDATA;
+                }
+                mp3->first_frame      = mpah;
+                mp3->first_frame_seen = 1;
+            }
         } else {
             av_log(s, AV_LOG_WARNING, "Audio packet of size %d (starting with 
%08"PRIX32"...) "
                    "is invalid, writing it anyway.\n", pkt->size, h);
@@ -441,6 +453,15 @@ static void mp3_update_xing(AVFormatContext *s)
     if (!mp3->has_variable_bitrate)
         AV_WL32(mp3->xing_frame + mp3->xing_offset, MKTAG('I', 'n', 'f', 'o'));
 
+    if (mp3->first_frame_seen) {
+        MPADecodeHeader header = mp3->first_frame;
+
+        header.error_protection = mp3->xing_header.error_protection;
+        header.bitrate_index    = mp3->xing_header.bitrate_index;
+        header.padding          = mp3->xing_header.padding;
+        AV_WB32(mp3->xing_frame, ff_mpa_encode_header(&header));
+    }
+
     AV_WB32(mp3->xing_frame + mp3->xing_offset + 8,  mp3->frames);
     AV_WB32(mp3->xing_frame + mp3->xing_offset + 12, mp3->size);
 
diff --git a/tests/ref/fate/gapless-mp3-remux b/tests/ref/fate/gapless-mp3-remux
index 0d3d625e33..6d56043a7f 100644
--- a/tests/ref/fate/gapless-mp3-remux
+++ b/tests/ref/fate/gapless-mp3-remux
@@ -1,3 +1,3 @@
-4458dffaa3fd294d708f05965e1a3186 *tests/data/fate/gapless-mp3-remux.mp3
+a490eb522f8fd2c772d1e27a1b5054a2 *tests/data/fate/gapless-mp3-remux.mp3
 249138 tests/data/fate/gapless-mp3-remux.mp3
 stream|start_pts=353600|duration_ts=218521600
diff --git a/tests/ref/fate/mpegaudiodecheader-libmp3lame 
b/tests/ref/fate/mpegaudiodecheader-libmp3lame
index 3955a43426..ddf18712e5 100644
--- a/tests/ref/fate/mpegaudiodecheader-libmp3lame
+++ b/tests/ref/fate/mpegaudiodecheader-libmp3lame
@@ -1,3 +1,2 @@
-offset=20 frames=1 header=FFFB9000 layer=3 sample_rate=44100 bit_rate=128000 
crc=0 mode=0 mode_ext=0 copyright=0 original=0 emphasis=0 private_bit=0
-offset=437 frames=231 header=FFFB9064 layer=3 sample_rate=44100 
bit_rate=128000 crc=0 mode=1 mode_ext=2 copyright=0 original=1 emphasis=0 
private_bit=0
+offset=20 frames=232 header=FFFB9064 layer=3 sample_rate=44100 bit_rate=128000 
crc=0 mode=1 mode_ext=2 copyright=0 original=1 emphasis=0 private_bit=0
 frames=232
diff --git a/tests/ref/fate/mpegaudiodecheader-mp3-remux 
b/tests/ref/fate/mpegaudiodecheader-mp3-remux
index c5d4d5cdd7..4a7602dc83 100644
--- a/tests/ref/fate/mpegaudiodecheader-mp3-remux
+++ b/tests/ref/fate/mpegaudiodecheader-mp3-remux
@@ -1,3 +1,2 @@
-offset=301 frames=1 header=FFFB9000 layer=3 sample_rate=44100 bit_rate=128000 
crc=0 mode=0 mode_ext=0 copyright=0 original=0 emphasis=0 private_bit=0
-offset=718 frames=21 header=FFFB9064 layer=3 sample_rate=44100 bit_rate=128000 
crc=0 mode=1 mode_ext=2 copyright=0 original=1 emphasis=0 private_bit=0
+offset=301 frames=22 header=FFFB9064 layer=3 sample_rate=44100 bit_rate=128000 
crc=0 mode=1 mode_ext=2 copyright=0 original=1 emphasis=0 private_bit=0
 frames=22
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to