[PR] avcodec/alac: don't require a clean end tag to stop decoding (PR #23948)

flux via ffmpeg-devel <[email protected]> Wed, 29 Jul 2026 14:18:47 -0000
Newsgroups gmane.comp.video.ffmpeg.devel
Message-ID <178533472822.51.12361070911860821878@29965ddac10e>
PR #23948 opened by flux
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23948
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23948.patch

# Summary of changes

alac_decode_frame()'s element loop stopped only on TYPE_END or running out of bits, with no check for "all declared channels decoded".
Some escape/verbatim frames pad to the packet's byte boundary (matching maxFrameBytes) without an end tag.
The loop then reads the zero padding as a new element, and the channel-count guard rejects it with "invalid element channel count".

Apple's reference decoder breaks out of the same loop once channelIndex >= numChannels:
https://github.com/macosforge/alac/blob/c38887c5c5e64a4b31108733bd79ca9b2496d987/codec/ALACDecoder.cpp#L588-L593

Stop the loop once ch reaches alac->channels, and only require an end tag / check leftover bits when the loop actually reached one.

New fate sample file attached to this PR:

```fate-samples
alac/alac-noendtag.m4a
```

Current build of ffmpeg will fail to decode this file, but afconvert (reference ALAC decoder) and patched ffmpeg build will succeed.


>From cb0ec887c3b2597178bf7931b2a5bd0589b1ee06 Mon Sep 17 00:00:00 2001
From: Thitat Auareesuksakul <[email protected]>
Date: Wed, 29 Jul 2026 22:50:18 +0900
Subject: [PATCH] avcodec/alac: don't require a clean end tag to stop decoding

alac_decode_frame()'s element loop stopped only on TYPE_END or running
out of bits, with no check for "all declared channels decoded". Some
escape/verbatim frames pad to the packet's byte boundary (matching
maxFrameBytes) without an end tag. The loop then reads the zero padding
as a new element, and the channel-count guard rejects it with "invalid
element channel count".

Apple's reference decoder breaks out of the same loop once
channelIndex >= numChannels:
https://github.com/macosforge/alac/blob/c38887c5c5e64a4b31108733bd79ca9b2496d987/codec/ALACDecoder.cpp#L588-L593

Stop the loop once ch reaches alac->channels, and only require an end
tag / check leftover bits when the loop actually reached one.

Signed-off-by: Thitat Auareesuksakul <[email protected]>
---
 libavcodec/alac.c                     | 9 ++++++---
 tests/fate/lossless-audio.mak         | 3 +++
 tests/ref/fate/lossless-alac-noendtag | 1 +
 3 files changed, 10 insertions(+), 3 deletions(-)
 create mode 100644 tests/ref/fate/lossless-alac-noendtag

diff --git a/libavcodec/alac.c b/libavcodec/alac.c
index f91288e97c..241c43e073 100644
--- a/libavcodec/alac.c
+++ b/libavcodec/alac.c
@@ -432,7 +432,10 @@ static int alac_decode_frame(AVCodecContext *avctx, AVFrame *frame,
     got_end = 0;
     alac->nb_samples = 0;
     ch = 0;
-    while (get_bits_left(&alac->gb) >= 3) {
+    /* stop once all channels are decoded instead of requiring a clean end tag.
+     * The reference decoder does the same (channelIndex >= numChannels in its element loop),
+     * since bits after the last audio element may just be padding to the frame's max coded size */
+    while (ch < alac->channels && get_bits_left(&alac->gb) >= 3) {
         element = get_bits(&alac->gb, 3);
         if (element == TYPE_END) {
             got_end = 1;
@@ -458,12 +461,12 @@ static int alac_decode_frame(AVCodecContext *avctx, AVFrame *frame,
 
         ch += channels;
     }
-    if (!got_end) {
+    if (!got_end && ch < alac->channels) {
         av_log(avctx, AV_LOG_ERROR, "no end tag found. incomplete packet.\n");
         return AVERROR_INVALIDDATA;
     }
 
-    if (avpkt->size * 8 - get_bits_count(&alac->gb) > 8) {
+    if (got_end && avpkt->size * 8 - get_bits_count(&alac->gb) > 8) {
         av_log(avctx, AV_LOG_ERROR, "Error : %d bits left\n",
                avpkt->size * 8 - get_bits_count(&alac->gb));
     }
diff --git a/tests/fate/lossless-audio.mak b/tests/fate/lossless-audio.mak
index 8be62b5244..a1d0aa6f08 100644
--- a/tests/fate/lossless-audio.mak
+++ b/tests/fate/lossless-audio.mak
@@ -1,6 +1,9 @@
 FATE_SAMPLES_LOSSLESS_AUDIO-$(call DEMDEC, MOV, ALAC, ARESAMPLE_FILTER) += fate-lossless-alac
 fate-lossless-alac: CMD = md5 -i $(TARGET_SAMPLES)/lossless-audio/inside.m4a -f s16le -af aresample
 
+FATE_SAMPLES_LOSSLESS_AUDIO-$(call DEMDEC, MOV, ALAC, ARESAMPLE_FILTER) += fate-lossless-alac-noendtag
+fate-lossless-alac-noendtag: CMD = md5 -i $(TARGET_SAMPLES)/alac/alac-noendtag.m4a -f s16le -af aresample
+
 FATE_SAMPLES_LOSSLESS_AUDIO-$(call DEMDEC, MLP, MLP) += fate-lossless-meridianaudio
 fate-lossless-meridianaudio: CMD = md5 -i $(TARGET_SAMPLES)/lossless-audio/luckynight-partial.mlp -f s16le
 
diff --git a/tests/ref/fate/lossless-alac-noendtag b/tests/ref/fate/lossless-alac-noendtag
new file mode 100644
index 0000000000..a41cba4217
--- /dev/null
+++ b/tests/ref/fate/lossless-alac-noendtag
@@ -0,0 +1 @@
+989862aa6617be4c1bc1c5df4de81683
-- 
2.52.0

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