[PR] avcodec/wmaprodec: drain every XMA stream at end of input (PR #24305)

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

This fixes incomplete XMA1/XMA2 multistream draining at end of input.

At EOF, `xma_decode_packet()` previously passed a single got-frame flag through every XMA stream, but afterwards queued only the frame belonging to `current_stream`. Final frames decoded for the other streams were therefore discarded. Short streams could also fail to drain after their first internally buffered frame had been released.

The decoder now prepares, drains, and queues each stream independently. Audio FIFO write failures are also propagated instead of being ignored.

## Regression coverage

A three-channel, three-stream XMA2 sample has been added for FATE coverage. It was produced from entirely synthetic 48 kHz input generated by FFmpeg's `asynth` source and encoded with the Microsoft XMA2 Encoding Tool.

Before this fix, the sample decoded to 7,680 samples per channel. It now produces the complete 8,192 samples per channel and matches the supplied tolerant PCM reference.

The sample and reference are attached to this pull request for FATE CI and use the following fate-suite paths:

* `xma2/xma2-multistream-tail.xma`
* `xma2/xma2-multistream-tail.pcm`

## Additional verification

* A Microsoft-encoded 7.1 fixture previously decoded to 287,488 samples per channel instead of the source length of 288,000. After the fix it produces all 288,000 samples. The old output is a byte-identical prefix of the corrected output, confirming that the change only restores the discarded final frame.
* An existing stereo fixture remains byte-identical before and after the fix, with decoded SHA-256 `454828aed510c3a3f227ddacda9b9a264982d82ad8476d77aae1493b828bac2c`.
* For stereo streams accepted by Microsoft's `/DecodeToPCM` implementation, FFmpeg and Microsoft produced equal sample counts and differed by at most one signed-16 level, with 99.89% bit-identical samples, an RMS difference of 0.03275 levels, and approximately 106.4 dB decoder-to-decoder SNR.
* Fifteen additional cases covering mono through 16 channels, sample rates from 8 kHz through 96 kHz, and very short through ordinary durations decoded without errors and produced the expected sample counts.
* The new FATE target passes under MSYS2 UCRT64.
* A clean minimal Linux ASan/UBSan build decoded the FATE sample, the 7.1 fixture, and the stereo fixture without sanitizer findings.
* `git diff --check` passes.

```fate-samples
xma2/xma2-multistream-tail.xma
xma2/xma2-multistream-tail.pcm
```


>From 3f57706796074052e77910f8d215da93d3a757aa Mon Sep 17 00:00:00 2001
From: Martijn Brouwer <[email protected]>
Date: Fri, 28 Aug 2026 18:45:25 +0200
Subject: [PATCH] avcodec/wmaprodec: drain every XMA stream at end of input

At EOF, xma_decode_packet() passed one got-frame flag through all streams, but queued only the frame belonging to current_stream. Final frames decoded for the other streams were therefore discarded. Short streams could also miss draining entirely after their first internal frame was unreferenced.

Prepare, drain and queue each stream independently, and propagate audio FIFO write failures. Add a three-channel sample test which previously decoded 512 samples per channel short.

Signed-off-by: Martijn Brouwer <[email protected]>
---
 libavcodec/wmaprodec.c | 107 ++++++++++++++++++++++++++++-------------
 tests/fate/wma.mak     |   8 +++
 2 files changed, 81 insertions(+), 34 deletions(-)

diff --git a/libavcodec/wmaprodec.c b/libavcodec/wmaprodec.c
index dd33c56e54..fb1f10dece 100644
--- a/libavcodec/wmaprodec.c
+++ b/libavcodec/wmaprodec.c
@@ -1829,38 +1829,91 @@ static int wmapro_decode_packet(AVCodecContext *avctx, AVFrame *frame,
     return decode_packet(avctx, s, frame, got_frame_ptr, avpkt);
 }
 
+static int xma_queue_frame(XMADecodeCtx *s, int stream_index)
+{
+    AVFrame *frame = s->frames[stream_index];
+    const int nb_samples = frame->nb_samples;
+    void *left[1] = { frame->extended_data[0] };
+    int ret;
+
+    ret = av_audio_fifo_write(s->samples[0][stream_index], left, nb_samples);
+    if (ret != nb_samples)
+        return ret < 0 ? ret : AVERROR_BUG;
+
+    if (s->xma[stream_index].nb_channels > 1) {
+        void *right[1] = { frame->extended_data[1] };
+
+        ret = av_audio_fifo_write(s->samples[1][stream_index], right,
+                                  nb_samples);
+        if (ret != nb_samples)
+            return ret < 0 ? ret : AVERROR_BUG;
+    }
+
+    return 0;
+}
+
+static int xma_get_frame_buffer(AVCodecContext *avctx, XMADecodeCtx *s,
+                                int stream_index)
+{
+    AVFrame *frame = s->frames[stream_index];
+
+    if (frame->data[0] && frame->nb_samples == 512)
+        return 0;
+
+    avctx->internal->skip_samples = 64;
+    av_frame_unref(frame);
+    frame->nb_samples = 512;
+    return ff_get_buffer(avctx, frame, 0);
+}
+
 static int xma_decode_packet(AVCodecContext *avctx, AVFrame *frame,
                              int *got_frame_ptr, AVPacket *avpkt)
 {
     XMADecodeCtx *s = avctx->priv_data;
-    int got_stream_frame_ptr = 0;
     int i, ret = 0, eof = 0;
 
-    if (!s->frames[s->current_stream]->data[0]) {
-        avctx->internal->skip_samples = 64;
-        s->frames[s->current_stream]->nb_samples = 512;
-        if ((ret = ff_get_buffer(avctx, s->frames[s->current_stream], 0)) < 0)
-            return ret;
-    } else if (s->frames[s->current_stream]->nb_samples != 512) {
-        avctx->internal->skip_samples = 64;
-        av_frame_unref(s->frames[s->current_stream]);
-        s->frames[s->current_stream]->nb_samples = 512;
-        if ((ret = ff_get_buffer(avctx, s->frames[s->current_stream], 0)) < 0)
-            return ret;
-    }
-    /* decode current stream packet */
-    if (!s->xma[s->current_stream].eof_done) {
-        ret = decode_packet(avctx, &s->xma[s->current_stream], s->frames[s->current_stream],
-                            &got_stream_frame_ptr, avpkt);
-    }
+    if (avpkt->size) {
+        int got_stream_frame_ptr = 0;
 
-    if (!avpkt->size) {
+        ret = xma_get_frame_buffer(avctx, s, s->current_stream);
+        if (ret < 0)
+            return ret;
+
+        /* decode current stream packet */
+        if (!s->xma[s->current_stream].eof_done) {
+            ret = decode_packet(avctx, &s->xma[s->current_stream],
+                                s->frames[s->current_stream],
+                                &got_stream_frame_ptr, avpkt);
+        }
+
+        if (got_stream_frame_ptr) {
+            int bret = xma_queue_frame(s, s->current_stream);
+
+            if (bret < 0)
+                return bret;
+        } else if (ret < 0) {
+            s->current_stream = 0;
+            return ret;
+        }
+    } else {
         eof = 1;
 
         for (i = 0; i < s->num_streams; i++) {
-            if (!s->xma[i].eof_done && s->frames[i]->data[0]) {
+            if (!s->xma[i].eof_done) {
+                int got_stream_frame_ptr = 0;
+
+                ret = xma_get_frame_buffer(avctx, s, i);
+                if (ret < 0)
+                    return ret;
                 ret = decode_packet(avctx, &s->xma[i], s->frames[i],
                                     &got_stream_frame_ptr, avpkt);
+                if (ret < 0)
+                    return ret;
+                if (got_stream_frame_ptr) {
+                    ret = xma_queue_frame(s, i);
+                    if (ret < 0)
+                        return ret;
+                }
             }
 
             eof &= s->xma[i].eof_done;
@@ -1872,20 +1925,6 @@ static int xma_decode_packet(AVCodecContext *avctx, AVFrame *frame,
     if (s->xma[0].trim_end)
         s->trim_end = s->xma[0].trim_end;
 
-    /* copy stream samples (1/2ch) to sample buffer (Nch) */
-    if (got_stream_frame_ptr) {
-        const int nb_samples = s->frames[s->current_stream]->nb_samples;
-        void *left[1] = { s->frames[s->current_stream]->extended_data[0] };
-        void *right[1] = { s->frames[s->current_stream]->extended_data[1] };
-
-        av_audio_fifo_write(s->samples[0][s->current_stream], left, nb_samples);
-        if (s->xma[s->current_stream].nb_channels > 1)
-            av_audio_fifo_write(s->samples[1][s->current_stream], right, nb_samples);
-    } else if (ret < 0) {
-        s->current_stream = 0;
-        return ret;
-    }
-
     /* find next XMA packet's owner stream, and update.
      * XMA streams find their packets following packet_skips
      * (at start there is one packet per stream, then interleave non-linearly). */
diff --git a/tests/fate/wma.mak b/tests/fate/wma.mak
index 62914c84dd..f1f8eb174e 100644
--- a/tests/fate/wma.mak
+++ b/tests/fate/wma.mak
@@ -18,6 +18,14 @@ $(FATE_WMAPRO-yes): CMP = oneoff
 FATE_SAMPLES_AVCONV += $(FATE_WMAPRO-yes)
 fate-wmapro: $(FATE_WMAPRO-yes)
 
+FATE_XMA2-$(call PCM, WAV, XMA2, ARESAMPLE_FILTER) += fate-xma2-multistream
+fate-xma2-multistream: CMD = pcm -i $(TARGET_SAMPLES)/xma2/xma2-multistream-tail.xma
+fate-xma2-multistream: REF = $(SAMPLES)/xma2/xma2-multistream-tail.pcm
+fate-xma2-multistream: CMP = oneoff
+
+FATE_SAMPLES_AVCONV += $(FATE_XMA2-yes)
+fate-xma2: $(FATE_XMA2-yes)
+
 FATE_WMAVOICE-$(call PCM, ASF, WMAVOICE, ARESAMPLE_FILTER) += fate-wmavoice-7k
 fate-wmavoice-7k: CMD = pcm -i $(TARGET_SAMPLES)/wmavoice/streaming_CBR-7K.wma
 fate-wmavoice-7k: REF = $(SAMPLES)/wmavoice/streaming_CBR-7K_ref.pcm
-- 
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.