Commit 4f924efab5 for ffmpeg

commit 4f924efab554e3b85ef2c141140ffba0ed3bbcc2
Author: Patrick Domack <patrickdk@patrickdk.com>
Date:   Sun Nov 1 04:56:41 2026 -0500

    avcodec/g723_1dec: saturate the synthesis filter like the reference

    The ITU reference runs the LPC synthesis filter in 32 bits and
    saturates the sum after every product (DS2-Anywhere SPEC.md section
    4.7). FFmpeg uses ff_celp_lp_synthesis_filter(), which sums with
    wraparound and clips once at the end. The two agree until a partial
    sum overflows. On loud signals with large LPC coefficients that
    happens, and FFmpeg's output can then be off by the whole 16-bit
    range. This has been so since the decoder was added in 2011.

    Give the decoder its own synthesis filter that does what the reference
    does: the input times 8192, a saturating multiply-subtract for each
    coefficient in the reference's order, a saturating shift by 2, then
    rounding to 16 bits. ff_celp_lp_synthesis_filter() stays as it is for
    the other decoders that use it.

    I checked the cause on three short cuts from loud streams. A Python
    model of the reference's filter reproduces the reference decoder
    sample for sample, a model of FFmpeg's filter reproduces FFmpeg, and
    the reference's sum saturates 17 to 50 times in each cut.

    The test set for this and the next two commits is 371 streams of
    random G.723.1 frames, 822343 frames in all: the 35 random streams of
    the earlier commits, 238 with loud gains or many lost frames, 90 that
    start or lose frames in unusual ways, and a few short cuts. Over them,
    the frames that differ from the reference drop from 61 to 0 with the
    postfilter off and from 263 to 145 with it on.

    saturation.tco holds 48 frames in 961 bytes, loud enough that the
    synthesis filter and the formant postfilter saturate, with a
    reflection coefficient that rounds differently at frame 20. Before the
    G.723.1 fixes, 25 of its 48 frames differ from the reference.

    Frame 42 of saturation.tco is a loud 5.3 kbit/s frame. With the
    postfilter off and every G.723.1 fix but this one, sample 224 is 32767
    where the reference gives 25516, the frame differs by up to 65535, and
    6 frames differ.

    fate-g723_1-dec-12 decodes saturation.tco with the postfilter off. Its
    ref matches the DS2-Anywhere G.723.1 decoder frame for frame. It fails
    with this fix or the Annex A fix left out. fate-g723_1-dec-1 to -8
    pass unchanged.

    Upload the sample to fate-suite as g723_1/saturation.tco.

    Signed-off-by: Patrick Domack <patrickdk@patrickdk.com>

diff --git a/libavcodec/g723_1dec.c b/libavcodec/g723_1dec.c
index 845af6a628..b230b6f1c2 100644
--- a/libavcodec/g723_1dec.c
+++ b/libavcodec/g723_1dec.c
@@ -32,7 +32,6 @@
 #define BITSTREAM_READER_LE
 #include "acelp_vectors.h"
 #include "avcodec.h"
-#include "celp_filters.h"
 #include "codec_internal.h"
 #include "decode.h"
 #include "get_bits.h"
@@ -931,6 +930,20 @@ static void generate_noise(G723_1_ChannelContext *p)
            PITCH_MAX * sizeof(*p->excitation));
 }

+static void lp_synthesis_filter(int16_t *out, const int16_t *lpc,
+                                const int16_t *in)
+{
+    for (int n = 0; n < SUBFRAME_LEN; n++) {
+        int sum = in[n] * (1 << 13);
+
+        for (int i = 0; i < LPC_ORDER; i++)
+            sum = av_sat_dsub32(sum, lpc[i] * out[n - 1 - i]);
+
+        sum    = av_clipl_int32((int64_t)sum * 4);
+        out[n] = av_sat_add32(sum, 1 << 15) >> 16;
+    }
+}
+
 static int g723_1_decode_frame(AVCodecContext *avctx, AVFrame *frame,
                                int *got_frame_ptr, AVPacket *avpkt)
 {
@@ -1087,9 +1100,7 @@ static int g723_1_decode_frame(AVCodecContext *avctx, AVFrame *frame,

         memcpy(p->audio, p->synth_mem, LPC_ORDER * sizeof(*p->audio));
         for (i = LPC_ORDER, j = 0; j < SUBFRAMES; i += SUBFRAME_LEN, j++)
-            ff_celp_lp_synthesis_filter(p->audio + i, &lpc[j * LPC_ORDER],
-                                        audio + i, SUBFRAME_LEN, LPC_ORDER,
-                                        0, 1, 1 << 12);
+            lp_synthesis_filter(p->audio + i, &lpc[j * LPC_ORDER], audio + i);
         memcpy(p->synth_mem, p->audio + FRAME_LEN, LPC_ORDER * sizeof(*p->audio));

         if (s->postfilter) {
diff --git a/tests/fate/voice.mak b/tests/fate/voice.mak
index d5c09c2daf..e71e95690c 100644
--- a/tests/fate/voice.mak
+++ b/tests/fate/voice.mak
@@ -53,6 +53,9 @@ fate-g723_1-dec-9: CMD = framecrc -postfilter 1 -i $(TARGET_SAMPLES)/g723_1/cng-
 FATE_G723_1 += fate-g723_1-dec-10
 fate-g723_1-dec-10: CMD = framecrc -postfilter 0 -i $(TARGET_SAMPLES)/g723_1/cng-erasures.tco -af aresample

+FATE_G723_1 += fate-g723_1-dec-12
+fate-g723_1-dec-12: CMD = framecrc -postfilter 0 -i $(TARGET_SAMPLES)/g723_1/saturation.tco -af aresample
+
 FATE_G723_1 += fate-g723_1-dec-13
 fate-g723_1-dec-13: CMD = framecrc -postfilter 1 -i $(TARGET_SAMPLES)/g723_1/lost-start.tco -af aresample

diff --git a/tests/ref/fate/g723_1-dec-12 b/tests/ref/fate/g723_1-dec-12
new file mode 100644
index 0000000000..0c24561525
--- /dev/null
+++ b/tests/ref/fate/g723_1-dec-12
@@ -0,0 +1,53 @@
+#tb 0: 1/8000
+#media_type 0: audio
+#codec_id 0: pcm_s16le
+#sample_rate 0: 8000
+#channel_layout_name 0: mono
+0,          0,          0,      240,      480, 0x01b2e49d
+0,        240,        240,      240,      480, 0xdc83f49b
+0,        480,        480,      240,      480, 0xdf12efa2
+0,        720,        720,      240,      480, 0x1b4e050f
+0,        960,        960,      240,      480, 0x3939f6c5
+0,       1200,       1200,      240,      480, 0xd66def25
+0,       1440,       1440,      240,      480, 0xdfdbe8ed
+0,       1680,       1680,      240,      480, 0x5d60f777
+0,       1920,       1920,      240,      480, 0xbcc9ed22
+0,       2160,       2160,      240,      480, 0x0590ef0a
+0,       2400,       2400,      240,      480, 0x8ef0e897
+0,       2640,       2640,      240,      480, 0x0913d866
+0,       2880,       2880,      240,      480, 0xb51cded3
+0,       3120,       3120,      240,      480, 0x4e20e45a
+0,       3360,       3360,      240,      480, 0xa2afebcf
+0,       3600,       3600,      240,      480, 0xcdfef505
+0,       3840,       3840,      240,      480, 0xd99ae7c7
+0,       4080,       4080,      240,      480, 0x9072f588
+0,       4320,       4320,      240,      480, 0x627ae859
+0,       4560,       4560,      240,      480, 0x2a54e445
+0,       4800,       4800,      240,      480, 0xc903f0bd
+0,       5040,       5040,      240,      480, 0xcb6cfcba
+0,       5280,       5280,      240,      480, 0x8257f79c
+0,       5520,       5520,      240,      480, 0xb4ade802
+0,       5760,       5760,      240,      480, 0x0242f960
+0,       6000,       6000,      240,      480, 0xd6f9ede7
+0,       6240,       6240,      240,      480, 0x87bee16b
+0,       6480,       6480,      240,      480, 0x0326eeb0
+0,       6720,       6720,      240,      480, 0x8debf274
+0,       6960,       6960,      240,      480, 0x350ef634
+0,       7200,       7200,      240,      480, 0x7bd20d93
+0,       7440,       7440,      240,      480, 0x010cd501
+0,       7680,       7680,      240,      480, 0x5fe1d1a3
+0,       7920,       7920,      240,      480, 0x148f1e5d
+0,       8160,       8160,      240,      480, 0x0d0c1340
+0,       8400,       8400,      240,      480, 0xf495db89
+0,       8640,       8640,      240,      480, 0x985ddd43
+0,       8880,       8880,      240,      480, 0xbbb9e18d
+0,       9120,       9120,      240,      480, 0xe11dde0a
+0,       9360,       9360,      240,      480, 0x87cef472
+0,       9600,       9600,      240,      480, 0x4c0fe75b
+0,       9840,       9840,      240,      480, 0x46d5e56f
+0,      10080,      10080,      240,      480, 0xdce2f488
+0,      10320,      10320,      240,      480, 0x4eddf50d
+0,      10560,      10560,      240,      480, 0x19a2f38d
+0,      10800,      10800,      240,      480, 0x4e29ee4d
+0,      11040,      11040,      240,      480, 0xd66ee964
+0,      11280,      11280,      240,      480, 0xc9aeefb1