PR #25287 opened by Forgejo_Fairy URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25287 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25287.patch
A WAV file with a zero-length data chunk and trailing metadata currently returns that metadata as audio. The attachments in #24934 produce 40 and 2,160 erroneous PCM bytes. Apply the RIFF-size distinction suggested by kvog in #24934: honor zero-length data when the enclosing RIFF size is known, while preserving the compatibility fallback for zero or UINT32_MAX RIFF sizes and UINT32_MAX data sizes. Continue parsing footer metadata when the data boundary is known, and record zero duration for known-empty streams to prevent bitrate-based duration estimation from counting metadata. The explicit ignore_length override remains effective. Adds a portable, in-memory FATE test with 28 cases covering seekable/nonseekable RIFF, RIFX, RF64 and BW64 input, empty/nonempty data, unknown lengths, metadata, duration and ignore_length. Expected output is stored in tests/ref/fate/wav-demux; no external samples are needed. Validation on Linux AArch64 with GCC 13.3.0: - Both issue attachments produce zero PCM bytes after the fix, using file and pipe input; ignore_length=1 retains the previous byte totals. - The reported AIFF conversion changes from 10 sample frames to zero. - The new fate-wav-demux test fails with the original demuxer and passes with this change. - make -j8 fate-wav fate-wav-demux fate-w64 fate-wav-ac3 SAMPLES=/opt/fate-suite passes. The original regression was verified by building b7e0f88f2b3b and its parent: the first attachment changes from zero PCM bytes to 40. Fixes #24934. >From 7f37c5b2e250238fe45f73a088944c699fe4b395 Mon Sep 17 00:00:00 2001 From: Michael Niedermayer <[email protected]> Date: Wed, 21 Oct 2026 05:19:54 +0000 Subject: [PATCH] avformat/wavdec: respect empty data chunks with a known RIFF size Keep treating a zero data size as unknown when the RIFF size is also unknown, preserving compatibility with old FFmpeg pipe output. With a known RIFF size, honor an empty data chunk and continue parsing footer metadata instead of returning it as audio packets. Record a zero duration for a known-empty stream so that trailing metadata is not used to estimate a positive duration. Add an in-memory FATE test covering seekable and nonseekable RIFF, RIFX, RF64 and BW64 input, legacy unknown sizes and ignore_length. Expected packet sizes, metadata and durations live in the FATE reference file. Fixes #24934. Assisted-by: Fairy --- libavformat/Makefile | 1 + libavformat/tests/.gitignore | 1 + libavformat/tests/wav.c | 213 +++++++++++++++++++++++++++++++++++ libavformat/wavdec.c | 13 ++- tests/fate/libavformat.mak | 4 + tests/ref/fate/wav-demux | 28 +++++ 6 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 libavformat/tests/wav.c create mode 100644 tests/ref/fate/wav-demux diff --git a/libavformat/Makefile b/libavformat/Makefile index 7c17892c22f..49d7505e384 100644 --- a/libavformat/Makefile +++ b/libavformat/Makefile @@ -803,6 +803,7 @@ TESTPROGS-$(CONFIG_SRTP) += srtp TESTPROGS-$(CONFIG_IMF_DEMUXER) += imf TESTPROGS-$(CONFIG_MP3_MUXER) += id3v2enc TESTPROGS-$(CONFIG_OGG_MUXER) += ogg_pages +TESTPROGS-$(CONFIG_WAV_DEMUXER) += wav TOOLS = aviocat \ ismindex \ diff --git a/libavformat/tests/.gitignore b/libavformat/tests/.gitignore index 40603e70a51..b624988e360 100644 --- a/libavformat/tests/.gitignore +++ b/libavformat/tests/.gitignore @@ -13,3 +13,4 @@ /seek_utils /http /ogg_pages +/wav diff --git a/libavformat/tests/wav.c b/libavformat/tests/wav.c new file mode 100644 index 00000000000..9aebbb6620b --- /dev/null +++ b/libavformat/tests/wav.c @@ -0,0 +1,213 @@ +/* + * 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 <stdint.h> +#include <stdio.h> +#include <string.h> + +#include "libavutil/common.h" +#include "libavutil/dict.h" +#include "libavutil/error.h" +#include "libavutil/intreadwrite.h" +#include "libavutil/log.h" +#include "libavutil/mem.h" +#include "libavformat/avformat.h" +#include "libavformat/avio.h" + +struct Buffer { + uint8_t *data; + int size; + int pos; +}; + +static int read_packet(void *opaque, uint8_t *buf, int size) +{ + struct Buffer *b = opaque; + size = FFMIN(size, b->size - b->pos); + if (!size) + return AVERROR_EOF; + memcpy(buf, b->data + b->pos, size); + b->pos += size; + return size; +} + +static int64_t seek(void *opaque, int64_t offset, int whence) +{ + struct Buffer *b = opaque; + if (whence == AVSEEK_SIZE) + return b->size; + if (whence == SEEK_CUR) + offset += b->pos; + else if (whence == SEEK_END) + offset += b->size; + if (offset < 0 || offset > b->size) + return AVERROR(EINVAL); + b->pos = offset; + return offset; +} + +/* riff_size == 1 requests the actual RIFF size. */ +static int test(const char *magic, uint32_t riff_size, uint32_t data_size, + int ignore_length, int seekable) +{ + AVFormatContext *fmt = NULL; + AVIOContext *pb = NULL, *writer; + AVPacket *pkt = NULL; + AVDictionary *opts = NULL; + const AVDictionaryEntry *title; + struct Buffer b = { 0 }; + int big_endian = !strcmp(magic, "RIFX"); + int rf64 = !strcmp(magic, "RF64") || !strcmp(magic, "BW64"); + void (*w16)(AVIOContext *, unsigned) = big_endian ? avio_wb16 : avio_wl16; + void (*w32)(AVIOContext *, unsigned) = big_endian ? avio_wb32 : avio_wl32; + int ret, bytes = 0, packets = 0; + uint8_t *io_buffer; + + ret = avio_open_dyn_buf(&writer); + if (ret < 0) + return ret; + avio_write(writer, magic, 4); + w32(writer, riff_size); + avio_write(writer, "WAVE", 4); + if (rf64) { + avio_write(writer, "ds64", 4); + avio_wl32(writer, 28); + avio_wl64(writer, 0); // filled in below + avio_wl64(writer, data_size); + avio_wl64(writer, data_size / 2); + avio_wl32(writer, 0); + } + avio_write(writer, "fmt ", 4); + w32(writer, 16); + w16(writer, 1); // PCM + w16(writer, 1); // mono + w32(writer, 8000); + w32(writer, 16000); + w16(writer, 2); + w16(writer, 16); + avio_write(writer, "data", 4); + w32(writer, rf64 ? UINT32_MAX : data_size); + if (data_size == 4) + avio_write(writer, "PCM!", 4); + avio_write(writer, "LQBE", 4); + w32(writer, 4); + avio_write(writer, "junk", 4); + /* LIST/INFO subchunks are read as little-endian even in RIFX. */ + avio_write(writer, "LIST", 4); + w32(writer, 16); + avio_write(writer, "INFOINAM", 8); + avio_wl32(writer, 4); + avio_write(writer, "test", 4); + b.size = avio_close_dyn_buf(writer, &b.data); + if (riff_size == 1) { + if (big_endian) + AV_WB32(b.data + 4, b.size - 8); + else + AV_WL32(b.data + 4, b.size - 8); + } + if (rf64) + AV_WL64(b.data + 20, b.size - 8); + + io_buffer = av_malloc(4096); + if (!io_buffer) { + ret = AVERROR(ENOMEM); + goto end; + } + pb = avio_alloc_context(io_buffer, 4096, 0, &b, read_packet, + NULL, seekable ? seek : NULL); + if (!pb) { + av_free(io_buffer); + ret = AVERROR(ENOMEM); + goto end; + } + fmt = avformat_alloc_context(); + pkt = av_packet_alloc(); + if (!fmt || !pkt) { + ret = AVERROR(ENOMEM); + goto end; + } + fmt->pb = pb; + ret = av_dict_set_int(&opts, "ignore_length", ignore_length, 0); + if (ret < 0) + goto end; + ret = avformat_open_input(&fmt, NULL, av_find_input_format("wav"), &opts); + if (ret < 0) + goto end; + title = av_dict_get(fmt->metadata, "title", NULL, 0); + while ((ret = av_read_frame(fmt, pkt)) >= 0) { + bytes += pkt->size; + packets++; + av_packet_unref(pkt); + } + printf("%s riff=%"PRIu32" data=%"PRIu32" ignore=%d seek=%d: " + "packets=%d bytes=%d title=%s duration=%"PRId64" end=%s\n", + magic, riff_size, data_size, ignore_length, seekable, + packets, bytes, title ? title->value : "-", + fmt->streams[0]->duration == AV_NOPTS_VALUE ? -1 : fmt->streams[0]->duration, + av_err2str(ret)); + if (ret == AVERROR_EOF) + ret = 0; +end: + av_dict_free(&opts); + av_packet_free(&pkt); + avformat_close_input(&fmt); + if (pb) + av_freep(&pb->buffer); + avio_context_free(&pb); + av_free(b.data); + return ret; +} + +int main(void) +{ + static const struct { + const char *magic; + uint32_t riff_size, data_size; + int ignore_length; + } cases[] = { + { "RIFF", 1, 0, 0 }, + { "RIFF", 1, 0, 1 }, + { "RIFF", 0, 0, 0 }, + { "RIFF", UINT32_MAX, 0, 0 }, + { "RIFF", 1, UINT32_MAX, 0 }, + { "RIFF", UINT32_MAX, UINT32_MAX, 0 }, + { "RIFF", 1, 4, 0 }, + { "RIFX", 1, 0, 0 }, + { "RIFX", 0, 0, 0 }, + { "RIFX", 1, 4, 0 }, + { "RF64", UINT32_MAX, 0, 0 }, + { "RF64", UINT32_MAX, 4, 0 }, + { "BW64", UINT32_MAX, 0, 0 }, + { "BW64", UINT32_MAX, 4, 0 }, + }; + int ret; + + av_log_set_level(AV_LOG_ERROR); + for (int i = 0; i < FF_ARRAY_ELEMS(cases); i++) { + for (int seekable = 0; seekable <= 1; seekable++) { + ret = test(cases[i].magic, cases[i].riff_size, cases[i].data_size, + cases[i].ignore_length, seekable); + if (ret < 0) { + fprintf(stderr, "%s\n", av_err2str(ret)); + return 1; + } + } + } + return 0; +} diff --git a/libavformat/wavdec.c b/libavformat/wavdec.c index d1f061d65b8..d41cfb787b4 100644 --- a/libavformat/wavdec.c +++ b/libavformat/wavdec.c @@ -364,7 +364,7 @@ static int wav_read_header(AVFormatContext *s) int64_t size, av_uninit(data_size); int64_t sample_count = 0; int rf64 = 0, bw64 = 0; - uint32_t tag; + uint32_t tag, riff_size; AVIOContext *pb = s->pb; AVStream *st = NULL; WAVDemuxContext *wav = s->priv_data; @@ -398,7 +398,7 @@ static int wav_read_header(AVFormatContext *s) } /* read chunk size */ - avio_rl32(pb); + riff_size = wav->rifx ? avio_rb32(pb) : avio_rl32(pb); /* read format */ if (avio_rl32(pb) != MKTAG('W', 'A', 'V', 'E')) { @@ -469,11 +469,13 @@ static int wav_read_header(AVFormatContext *s) if (rf64 || bw64) { wav->data_end = av_sat_add64(avio_tell(pb), data_size); next_tag_ofs = av_sat_add64(wav->data_end, data_size & 1); - } else if (size > 0 && size != 0xFFFFFFFF) { + } else if (size != UINT32_MAX && + (size || (riff_size && riff_size != UINT32_MAX))) { data_size = size; wav->data_end = avio_tell(pb) + size; next_tag_ofs = wav->data_end + (size & 1); } else { + /* Old FFmpeg versions used zero for both sizes when streaming. */ av_log(s, AV_LOG_WARNING, "Ignoring maximum wav data size, " "file may be invalid\n"); data_size = 0; @@ -485,7 +487,7 @@ static int wav_read_header(AVFormatContext *s) /* don't look for footer metadata if we can't seek or if we don't * know where the data tag ends */ - if (!(pb->seekable & AVIO_SEEKABLE_NORMAL) || (!(rf64 && !bw64) && !size)) + if (!(pb->seekable & AVIO_SEEKABLE_NORMAL) || wav->data_end == INT64_MAX) goto break_loop; break; case MKTAG('f', 'a', 'c', 't'): @@ -668,7 +670,8 @@ break_loop: / (st->codecpar->ch_layout.nb_channels * (uint64_t)av_get_bits_per_sample(st->codecpar->codec_id)); - if (sample_count) + /* Do not estimate an empty stream's duration from trailing metadata. */ + if (sample_count || (!wav->ignore_length && wav->data_end == data_ofs)) st->duration = sample_count; if (st->codecpar->codec_id == AV_CODEC_ID_PCM_S32LE && diff --git a/tests/fate/libavformat.mak b/tests/fate/libavformat.mak index 45878891fd9..cd37db86393 100644 --- a/tests/fate/libavformat.mak +++ b/tests/fate/libavformat.mak @@ -73,6 +73,10 @@ fate-seek_utils: libavformat/tests/seek_utils$(EXESUF) fate-seek_utils: CMD = run libavformat/tests/seek_utils$(EXESUF) fate-seek_utils: CMP = null +FATE_LIBAVFORMAT-$(CONFIG_WAV_DEMUXER) += fate-wav-demux +fate-wav-demux: libavformat/tests/wav$(EXESUF) +fate-wav-demux: CMD = run libavformat/tests/wav$(EXESUF) + FATE_LIBAVFORMAT += $(FATE_LIBAVFORMAT-yes) FATE-$(CONFIG_AVFORMAT) += $(FATE_LIBAVFORMAT) fate-libavformat: $(FATE_LIBAVFORMAT) diff --git a/tests/ref/fate/wav-demux b/tests/ref/fate/wav-demux new file mode 100644 index 00000000000..9a0589bfd0e --- /dev/null +++ b/tests/ref/fate/wav-demux @@ -0,0 +1,28 @@ +RIFF riff=1 data=0 ignore=0 seek=0: packets=0 bytes=0 title=- duration=0 end=End of file +RIFF riff=1 data=0 ignore=0 seek=1: packets=0 bytes=0 title=test duration=0 end=End of file +RIFF riff=1 data=0 ignore=1 seek=0: packets=1 bytes=36 title=- duration=-1 end=End of file +RIFF riff=1 data=0 ignore=1 seek=1: packets=1 bytes=36 title=test duration=-1 end=End of file +RIFF riff=0 data=0 ignore=0 seek=0: packets=1 bytes=36 title=- duration=-1 end=End of file +RIFF riff=0 data=0 ignore=0 seek=1: packets=1 bytes=36 title=- duration=-1 end=End of file +RIFF riff=4294967295 data=0 ignore=0 seek=0: packets=1 bytes=36 title=- duration=-1 end=End of file +RIFF riff=4294967295 data=0 ignore=0 seek=1: packets=1 bytes=36 title=- duration=-1 end=End of file +RIFF riff=1 data=4294967295 ignore=0 seek=0: packets=1 bytes=36 title=- duration=-1 end=End of file +RIFF riff=1 data=4294967295 ignore=0 seek=1: packets=1 bytes=36 title=- duration=-1 end=End of file +RIFF riff=4294967295 data=4294967295 ignore=0 seek=0: packets=1 bytes=36 title=- duration=-1 end=End of file +RIFF riff=4294967295 data=4294967295 ignore=0 seek=1: packets=1 bytes=36 title=- duration=-1 end=End of file +RIFF riff=1 data=4 ignore=0 seek=0: packets=1 bytes=4 title=- duration=-1 end=End of file +RIFF riff=1 data=4 ignore=0 seek=1: packets=1 bytes=4 title=test duration=2 end=End of file +RIFX riff=1 data=0 ignore=0 seek=0: packets=0 bytes=0 title=- duration=0 end=End of file +RIFX riff=1 data=0 ignore=0 seek=1: packets=0 bytes=0 title=test duration=0 end=End of file +RIFX riff=0 data=0 ignore=0 seek=0: packets=1 bytes=36 title=- duration=-1 end=End of file +RIFX riff=0 data=0 ignore=0 seek=1: packets=1 bytes=36 title=- duration=-1 end=End of file +RIFX riff=1 data=4 ignore=0 seek=0: packets=1 bytes=4 title=- duration=-1 end=End of file +RIFX riff=1 data=4 ignore=0 seek=1: packets=1 bytes=4 title=test duration=2 end=End of file +RF64 riff=4294967295 data=0 ignore=0 seek=0: packets=0 bytes=0 title=- duration=0 end=End of file +RF64 riff=4294967295 data=0 ignore=0 seek=1: packets=0 bytes=0 title=test duration=0 end=End of file +RF64 riff=4294967295 data=4 ignore=0 seek=0: packets=1 bytes=4 title=- duration=2 end=End of file +RF64 riff=4294967295 data=4 ignore=0 seek=1: packets=1 bytes=4 title=test duration=2 end=End of file +BW64 riff=4294967295 data=0 ignore=0 seek=0: packets=0 bytes=0 title=- duration=0 end=End of file +BW64 riff=4294967295 data=0 ignore=0 seek=1: packets=0 bytes=0 title=test duration=0 end=End of file +BW64 riff=4294967295 data=4 ignore=0 seek=0: packets=1 bytes=4 title=- duration=2 end=End of file +BW64 riff=4294967295 data=4 ignore=0 seek=1: packets=1 bytes=4 title=test duration=2 end=End of file -- 2.54.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
