PR #25406 opened by Jun Zhao (mypopydev)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25406
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25406.patch

This patch set fixes G.726LE encoding tag selection in the Sun AU muxer 
(libavformat/au.c). 
Previously, au_write_header() looked up the tag via ff_codec_get_tag(), which 
always 
returned the first matching table entry (23, 4-bit / 32 kbit/s) regardless of 
the actual 
sample bit width, causing non-4-bit streams to be reopened with wrong bitrates 
and durations. 

The muxer now selects the encoding tag from bits_per_coded_sample (or derives 
it 
from bit_rate and sample_rate when unset), mapping 3/4/5-bit G.726LE to 
standard 
Sun AU encodings 25/23/26, writing the non-standard '7262' tag with a warning 
for 
2-bit (16 kbit/s) streams, and rejecting unsupported bit widths. 

A new fate-auenc test is added to cover round-trip muxing and invalid bit-width 
rejection.


>From cae778cadea63fbda13c49b3427e1bc5c60bdc0f Mon Sep 17 00:00:00 2001
From: Jun Zhao <[email protected]>
Date: Sat, 24 Oct 2026 10:05:06 +0800
Subject: [PATCH 1/2] avformat/au: select G.726LE encoding tag from sample bit
 width

au_write_header() looked up the AU encoding tag with ff_codec_get_tag(),
which always returned the first AV_CODEC_ID_ADPCM_G726LE entry (23, 4-bit
32 kbit/s) regardless of the stream bit width. Remuxing 16, 24 or
40 kbit/s G.726LE into AU wrote encoding 23, so reopening the file read
it back as 4-bit 32 kbit/s with the wrong duration.

Select the tag from bits_per_coded_sample, falling back to bit_rate and
sample_rate when bits_per_coded_sample is unset: 25 for 3-bit, 23 for
4-bit, 26 for 5-bit, and the existing '7262' tag (with a warning, since
Sun AU defines no standard 2-bit code) for 2-bit. Reject other widths.

Fixes: https://code.ffmpeg.org/FFmpeg/FFmpeg/issues/25401
Signed-off-by: Jun Zhao <[email protected]>
---
 libavformat/au.c | 52 ++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 48 insertions(+), 4 deletions(-)

diff --git a/libavformat/au.c b/libavformat/au.c
index 22449958258..af7e7aec48c 100644
--- a/libavformat/au.c
+++ b/libavformat/au.c
@@ -284,6 +284,44 @@ static int au_get_annotations(AVFormatContext *s, AVBPrint 
*annotations)
     return av_bprint_is_complete(annotations) ? 0 : AVERROR(ENOMEM);
 }
 
+/* Sun encodings 25, 23 and 26 are 3-, 4- and 5-bit G.726. There is no Sun
+ * number for 2-bit G.726; '7262' is the private tag the demuxer reads. */
+static int au_g726le_tag(AVFormatContext *s, AVCodecParameters *par)
+{
+    int64_t bps = par->bits_per_coded_sample;
+
+    if (bps <= 0) {
+        if (par->bit_rate <= 0 || par->sample_rate <= 0) {
+            av_log(s, AV_LOG_ERROR, "Cannot determine G.726LE bit width\n");
+            return AVERROR(EINVAL);
+        }
+        bps = (par->bit_rate + par->sample_rate / 2) / par->sample_rate;
+    }
+
+    switch (bps) {
+    case 2:
+        av_log(s, AV_LOG_WARNING,
+               "Sun AU has no standard encoding for 2-bit G.726LE; "
+               "writing non-standard '7262' tag\n");
+        par->codec_tag = MKBETAG('7', '2', '6', '2');
+        break;
+    case 3:
+        par->codec_tag = 25;
+        break;
+    case 4:
+        par->codec_tag = 23;
+        break;
+    case 5:
+        par->codec_tag = 26;
+        break;
+    default:
+        av_log(s, AV_LOG_ERROR,
+               "Unsupported G.726LE bit width %"PRId64"\n", bps);
+        return AVERROR(EINVAL);
+    }
+    return 0;
+}
+
 static int au_write_header(AVFormatContext *s)
 {
     int ret;
@@ -292,10 +330,16 @@ static int au_write_header(AVFormatContext *s)
     AVCodecParameters *par = s->streams[0]->codecpar;
     AVBPrint annotations;
 
-    par->codec_tag = ff_codec_get_tag(codec_au_tags, par->codec_id);
-    if (!par->codec_tag) {
-        av_log(s, AV_LOG_ERROR, "unsupported codec\n");
-        return AVERROR(EINVAL);
+    if (par->codec_id == AV_CODEC_ID_ADPCM_G726LE) {
+        ret = au_g726le_tag(s, par);
+        if (ret < 0)
+            return ret;
+    } else {
+        par->codec_tag = ff_codec_get_tag(codec_au_tags, par->codec_id);
+        if (!par->codec_tag) {
+            av_log(s, AV_LOG_ERROR, "unsupported codec\n");
+            return AVERROR(EINVAL);
+        }
     }
 
     av_bprint_init(&annotations, 0, INT_MAX - 24);
-- 
2.54.0


>From 673c0017cfee368b417c4ff7611760f8cc84d7b8 Mon Sep 17 00:00:00 2001
From: Jun Zhao <[email protected]>
Date: Sat, 24 Oct 2026 10:05:06 +0800
Subject: [PATCH 2/2] tests/fate: add an AU G.726LE encoding tag round-trip
 test

Cover 2-, 3-, 4- and 5-bit G.726LE, bit-rate fallback when
bits_per_coded_sample is unset, and rejection of unsupported bit widths.

Signed-off-by: Jun Zhao <[email protected]>
---
 libavformat/Makefile         |   1 +
 libavformat/tests/.gitignore |   1 +
 libavformat/tests/auenc.c    | 202 +++++++++++++++++++++++++++++++++++
 tests/fate/libavformat.mak   |   4 +
 tests/ref/fate/auenc         |   8 ++
 5 files changed, 216 insertions(+)
 create mode 100644 libavformat/tests/auenc.c
 create mode 100644 tests/ref/fate/auenc

diff --git a/libavformat/Makefile b/libavformat/Makefile
index dbf78aa4b94..92c09714fe7 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -804,6 +804,7 @@ TESTPROGS-$(CONFIG_NETWORK)              += noproxy
 TESTPROGS-$(CONFIG_SRTP)                 += srtp
 TESTPROGS-$(CONFIG_IMF_DEMUXER)          += imf
 TESTPROGS-$(CONFIG_MP3_MUXER)            += id3v2enc id3v2chap
+TESTPROGS-$(CONFIG_AU_MUXER)             += auenc
 TESTPROGS-$(CONFIG_OGG_MUXER)            += ogg_pages
 
 TOOLS     = aviocat                                                     \
diff --git a/libavformat/tests/.gitignore b/libavformat/tests/.gitignore
index 6e594111121..f3640a487cc 100644
--- a/libavformat/tests/.gitignore
+++ b/libavformat/tests/.gitignore
@@ -1,3 +1,4 @@
+/auenc
 /id3v2
 /id3v2enc
 /fifo_muxer
diff --git a/libavformat/tests/auenc.c b/libavformat/tests/auenc.c
new file mode 100644
index 00000000000..227a07d53a2
--- /dev/null
+++ b/libavformat/tests/auenc.c
@@ -0,0 +1,202 @@
+/*
+ * 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
+ */
+
+#include <inttypes.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "libavutil/mem.h"
+#include "libavformat/avformat.h"
+#include "libavformat/avio.h"
+
+#define AU_PAYLOAD 80
+
+typedef struct {
+    const uint8_t *data;
+    int size;
+    int pos;
+} MemFile;
+
+static int mem_read(void *opaque, uint8_t *buf, int buf_size)
+{
+    MemFile *m = opaque;
+    int left = m->size - m->pos;
+
+    if (left <= 0)
+        return AVERROR_EOF;
+    if (buf_size > left)
+        buf_size = left;
+    memcpy(buf, m->data + m->pos, buf_size);
+    m->pos += buf_size;
+    return buf_size;
+}
+
+static int64_t mem_seek(void *opaque, int64_t offset, int whence)
+{
+    MemFile *m = opaque;
+
+    whence &= ~AVSEEK_FORCE;
+    if (whence == AVSEEK_SIZE)
+        return m->size;
+    if (whence == SEEK_CUR)
+        offset += m->pos;
+    else if (whence == SEEK_END)
+        offset += m->size;
+    else if (whence != SEEK_SET)
+        return AVERROR(EINVAL);
+    if (offset < 0 || offset > m->size)
+        return AVERROR(EINVAL);
+    m->pos = offset;
+    return offset;
+}
+
+static void free_pb(AVIOContext **pb)
+{
+    if (!*pb)
+        return;
+    av_freep(&(*pb)->buffer);
+    avio_context_free(pb);
+}
+
+static int run_case(int bps, int64_t bit_rate)
+{
+    AVFormatContext *out = NULL, *in = NULL;
+    AVIOContext *pb = NULL, *in_pb = NULL;
+    AVStream *st;
+    AVPacket *pkt = NULL;
+    MemFile mem = { 0 };
+    uint8_t *buf = NULL, *io_buf = NULL;
+    int ret, size;
+
+    ret = avformat_alloc_output_context2(&out, NULL, "au", NULL);
+    if (ret < 0)
+        return 1;
+    if (avio_open_dyn_buf(&pb) < 0) {
+        ret = 1;
+        goto done;
+    }
+    out->pb = pb;
+
+    st = avformat_new_stream(out, NULL);
+    if (!st) {
+        ret = 1;
+        goto done;
+    }
+    st->codecpar->codec_type  = AVMEDIA_TYPE_AUDIO;
+    st->codecpar->codec_id    = AV_CODEC_ID_ADPCM_G726LE;
+    st->codecpar->sample_rate = 8000;
+    st->codecpar->bits_per_coded_sample = bps;
+    st->codecpar->bit_rate    = bit_rate;
+    st->codecpar->ch_layout   = (AVChannelLayout)AV_CHANNEL_LAYOUT_MONO;
+
+    ret = avformat_write_header(out, NULL);
+    if (ret < 0) {
+        printf("bps=%d bit_rate=%"PRId64" rejected\n", bps, bit_rate);
+        ret = 0;
+        goto done;
+    }
+
+    pkt = av_packet_alloc();
+    if (!pkt || (ret = av_new_packet(pkt, AU_PAYLOAD)) < 0) {
+        ret = 1;
+        goto done;
+    }
+    pkt->stream_index = 0;
+    pkt->pts = pkt->dts = 0;
+    if ((ret = av_write_frame(out, pkt)) < 0 ||
+        (ret = av_write_trailer(out)) < 0) {
+        ret = 1;
+        goto done;
+    }
+
+    size = avio_close_dyn_buf(pb, &buf);
+    pb = NULL;
+    if (size < 16) {
+        ret = 1;
+        goto done;
+    }
+
+    mem.data = buf;
+    mem.size = size;
+    io_buf = av_malloc(4096);
+    if (!io_buf) {
+        ret = 1;
+        goto done;
+    }
+    in_pb = avio_alloc_context(io_buf, 4096, 0, &mem, mem_read, NULL, 
mem_seek);
+    if (!in_pb) {
+        av_free(io_buf);
+        ret = 1;
+        goto done;
+    }
+
+    in = avformat_alloc_context();
+    if (!in) {
+        ret = 1;
+        goto done;
+    }
+    in->pb = in_pb;
+    if ((ret = avformat_open_input(&in, NULL, NULL, NULL)) < 0 ||
+        !in->nb_streams) {
+        ret = 1;
+        goto done;
+    }
+
+    st = in->streams[0];
+    printf("bps=%d bit_rate=%"PRId64" tag=%02x %02x %02x %02x "
+           "read_bps=%d read_bit_rate=%"PRId64" duration=%"PRId64"\n",
+           bps, bit_rate, buf[12], buf[13], buf[14], buf[15],
+           st->codecpar->bits_per_coded_sample, st->codecpar->bit_rate,
+           st->duration);
+    ret = 0;
+
+done:
+    av_packet_free(&pkt);
+    if (in) {
+        in_pb = in->pb;
+        avformat_close_input(&in);
+    }
+    free_pb(&in_pb);
+    if (pb) {
+        uint8_t *tmp = NULL;
+        avio_close_dyn_buf(pb, &tmp);
+        av_free(tmp);
+    }
+    av_free(buf);
+    avformat_free_context(out);
+    return ret < 0 ? 1 : ret;
+}
+
+int main(void)
+{
+    static const int cases[][2] = {
+        { 2,     0 },
+        { 3,     0 },
+        { 4,     0 },
+        { 5,     0 },
+        { 0, 16000 },
+        { 0, 40000 },
+        { 1,     0 },
+        { 0,     0 },
+    };
+    int i, ret = 0;
+
+    for (i = 0; i < FF_ARRAY_ELEMS(cases); i++)
+        ret |= run_case(cases[i][0], cases[i][1]);
+    return ret;
+}
diff --git a/tests/fate/libavformat.mak b/tests/fate/libavformat.mak
index 68b5f6c101b..18270bbcab9 100644
--- a/tests/fate/libavformat.mak
+++ b/tests/fate/libavformat.mak
@@ -10,6 +10,10 @@ FATE_LIBAVFORMAT-$(CONFIG_MP3_MUXER) += fate-id3v2chap
 fate-id3v2chap: libavformat/tests/id3v2chap$(EXESUF)
 fate-id3v2chap: CMD = run libavformat/tests/id3v2chap$(EXESUF)
 
+FATE_LIBAVFORMAT-$(call ALLYES, AU_MUXER AU_DEMUXER) += fate-auenc
+fate-auenc: libavformat/tests/auenc$(EXESUF)
+fate-auenc: CMD = run libavformat/tests/auenc$(EXESUF)
+
 FATE_LIBAVFORMAT += fate-mkdir
 fate-mkdir: libavformat/tests/mkdir$(EXESUF)
 fate-mkdir: CMD = run libavformat/tests/mkdir$(EXESUF)
diff --git a/tests/ref/fate/auenc b/tests/ref/fate/auenc
new file mode 100644
index 00000000000..974a1e2e8a9
--- /dev/null
+++ b/tests/ref/fate/auenc
@@ -0,0 +1,8 @@
+bps=2 bit_rate=0 tag=37 32 36 32 read_bps=2 read_bit_rate=16000 duration=320
+bps=3 bit_rate=0 tag=00 00 00 19 read_bps=3 read_bit_rate=24000 duration=213
+bps=4 bit_rate=0 tag=00 00 00 17 read_bps=4 read_bit_rate=32000 duration=160
+bps=5 bit_rate=0 tag=00 00 00 1a read_bps=5 read_bit_rate=40000 duration=128
+bps=0 bit_rate=16000 tag=37 32 36 32 read_bps=2 read_bit_rate=16000 
duration=320
+bps=0 bit_rate=40000 tag=00 00 00 1a read_bps=5 read_bit_rate=40000 
duration=128
+bps=1 bit_rate=0 rejected
+bps=0 bit_rate=0 rejected
-- 
2.54.0

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

Reply via email to