PR #25519 opened by Marti2203 URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25519 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25519.patch
The FLT and DBL to S64 conversions in `libswresample/audioconvert.c` scale by 2^63 and call `llrint()` without clipping. A sample of exactly 1.0 or more does not fit in `int64_t`. On x86, `llrint()` then returns `INT64_MIN`, so a full-scale positive sample becomes a full-scale negative one. The conversions to u8, s16 and s32 already clip. Example (master fa4daf630e, x86-64): `ffmpeg -f lavfi -i aevalsrc=1.0:d=0.001 -c:a pcm_s64le out.wav` writes samples that decode as -1.0. The same pipeline with `pcm_s32le` gives 0.9999999995. Decoded lossy audio often goes slightly above 1.0, so any conversion of such a stream to s64 (`pcm_s64le`, `aformat=s64`) turns each overshoot into a full-scale negative click. The fix clips both conversions to the `int64_t` range. Samples inside (-1.0, 1.0) and NaN are converted as before. The second patch adds `fate-swr-dbl-s64-clip` and `fate-swr-flt-s64-clip`. They convert samples from -2.0 to 1.5 in steps of 0.5 (all exact in binary) to s64 from double and from float, then to s32 for the md5. No sample is needed. Both tests fail before the fix and pass after it. With only the double conversion fixed, the float test still fails, so each test covers its own path. `fate-swr` (414 tests) passes unchanged, with release and ASan builds. This issue was found by an LLM-Enhanced Static Analysis Engine and verified manually. 🤖 Generated with [Claude Code](https://claude.com/claude-code) From e8b860d329c9bc0b45fde54c08a5c50f71156d5d Mon Sep 17 00:00:00 2001 From: Martin Mirchev <[email protected]> Date: Tue, 27 Oct 2026 14:41:06 +0800 Subject: [PATCH 1/2] swresample/audioconvert: clip float and double samples converted to s64 The FLT and DBL to S64 conversions scale by 2^63 and call llrint() without clipping. A sample of exactly 1.0 or more does not fit in int64_t, and on x86 llrint() then returns INT64_MIN, so a full-scale positive sample becomes a full-scale negative one. The conversions to the other integer formats clip. Clip to the int64_t range like the s32 conversions. Signed-off-by: Martin Mirchev <[email protected]> --- libswresample/audioconvert.c | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/libswresample/audioconvert.c b/libswresample/audioconvert.c index 5956f29676e..f4098f963ce 100644 --- a/libswresample/audioconvert.c +++ b/libswresample/audioconvert.c @@ -52,6 +52,25 @@ static void CONV_FUNC_NAME(ofmt, ifmt)(DSDContext *st, uint8_t *po, const uint8_ }\ } +/* 1.0 * 2^63 does not fit in int64_t, so clip before converting */ +static av_always_inline int64_t flt_to_s64(float v) +{ + if (v >= 1.0f) + return INT64_MAX; + if (v <= -1.0f) + return INT64_MIN; + return llrintf(v * (UINT64_C(1)<<63)); +} + +static av_always_inline int64_t dbl_to_s64(double v) +{ + if (v >= 1.0) + return INT64_MAX; + if (v <= -1.0) + return INT64_MIN; + return llrint(v * (UINT64_C(1)<<63)); +} + //FIXME put things below under ifdefs so we do not waste space for cases no codec will need CONV_FUNC(AV_SAMPLE_FMT_U8 , uint8_t, AV_SAMPLE_FMT_U8 , *(const uint8_t*)pi) CONV_FUNC(AV_SAMPLE_FMT_S16, int16_t, AV_SAMPLE_FMT_U8 , (*(const uint8_t*)pi - 0x80U)<<8) @@ -80,13 +99,13 @@ CONV_FUNC(AV_SAMPLE_FMT_DBL, double , AV_SAMPLE_FMT_S64, *(const int64_t*)pi*(1 CONV_FUNC(AV_SAMPLE_FMT_U8 , uint8_t, AV_SAMPLE_FMT_FLT, av_clip_uint8( lrintf(*(const float*)pi * (1<<7)) + 0x80)) CONV_FUNC(AV_SAMPLE_FMT_S16, int16_t, AV_SAMPLE_FMT_FLT, av_clip_int16( lrintf(*(const float*)pi * (1<<15)))) CONV_FUNC(AV_SAMPLE_FMT_S32, int32_t, AV_SAMPLE_FMT_FLT, av_clipl_int32(llrintf(*(const float*)pi * (1U<<31)))) -CONV_FUNC(AV_SAMPLE_FMT_S64, int64_t, AV_SAMPLE_FMT_FLT, llrintf(*(const float*)pi * (UINT64_C(1)<<63))) +CONV_FUNC(AV_SAMPLE_FMT_S64, int64_t, AV_SAMPLE_FMT_FLT, flt_to_s64(*(const float*)pi)) CONV_FUNC(AV_SAMPLE_FMT_FLT, float , AV_SAMPLE_FMT_FLT, *(const float*)pi) CONV_FUNC(AV_SAMPLE_FMT_DBL, double , AV_SAMPLE_FMT_FLT, *(const float*)pi) CONV_FUNC(AV_SAMPLE_FMT_U8 , uint8_t, AV_SAMPLE_FMT_DBL, av_clip_uint8( lrint(*(const double*)pi * (1<<7)) + 0x80)) CONV_FUNC(AV_SAMPLE_FMT_S16, int16_t, AV_SAMPLE_FMT_DBL, av_clip_int16( lrint(*(const double*)pi * (1<<15)))) CONV_FUNC(AV_SAMPLE_FMT_S32, int32_t, AV_SAMPLE_FMT_DBL, av_clipl_int32(llrint(*(const double*)pi * (1U<<31)))) -CONV_FUNC(AV_SAMPLE_FMT_S64, int64_t, AV_SAMPLE_FMT_DBL, llrint(*(const double*)pi * (UINT64_C(1)<<63))) +CONV_FUNC(AV_SAMPLE_FMT_S64, int64_t, AV_SAMPLE_FMT_DBL, dbl_to_s64(*(const double*)pi)) CONV_FUNC(AV_SAMPLE_FMT_FLT, float , AV_SAMPLE_FMT_DBL, *(const double*)pi) CONV_FUNC(AV_SAMPLE_FMT_DBL, double , AV_SAMPLE_FMT_DBL, *(const double*)pi) CONV_FUNC(AV_SAMPLE_FMT_DSD, uint8_t, AV_SAMPLE_FMT_DSD, *(const uint8_t*)pi) -- 2.54.0 From 73cab9de85b22f6eab342cc59b0c73199028cb94 Mon Sep 17 00:00:00 2001 From: Martin Mirchev <[email protected]> Date: Tue, 27 Oct 2026 14:47:46 +0800 Subject: [PATCH 2/2] fate/libswresample: add tests for float and double to s64 clipping Convert samples from -2.0 to 1.5 to s64 from double and from float input. Samples of 1.0 and more must become the largest positive value. Signed-off-by: Martin Mirchev <[email protected]> --- tests/fate/libswresample.mak | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/fate/libswresample.mak b/tests/fate/libswresample.mak index bd3b228a584..4d7be7b9935 100644 --- a/tests/fate/libswresample.mak +++ b/tests/fate/libswresample.mak @@ -1116,6 +1116,21 @@ fate-swr-custom-reorder: REF = 3621f90f05eb313858791b3915b9ca18 FATE_SWR += $(FATE_SWR_CUSTOM_REORDER-yes) +# -2.0 to 1.5 in steps of 0.5: values of 1.0 and more must clip to the +# largest positive sample when converted to s64 +FATE_SWR_S64_SRC = (mod(n\,8)-4)/2 + +FATE_SWR_S64_CLIP-$(call ENCMUX, PCM_S32LE, PCM_S32LE, AEVALSRC_FILTER ARESAMPLE_FILTER AFORMAT_FILTER FILE_PROTOCOL) += fate-swr-dbl-s64-clip fate-swr-flt-s64-clip +fate-swr-dbl-s64-clip: CMD = md5 -filter_complex "aevalsrc=$(FATE_SWR_S64_SRC):d=0.01,aresample,aformat=f=s64,aresample,aformat=f=s32[OUT]" -map [OUT] -f s32le +fate-swr-dbl-s64-clip: CMP = oneline +fate-swr-dbl-s64-clip: REF = e47630b4a3176417841c079ae76d2b85 + +fate-swr-flt-s64-clip: CMD = md5 -filter_complex "aevalsrc=$(FATE_SWR_S64_SRC):d=0.01,aresample,aformat=f=flt,aresample,aformat=f=s64,aresample,aformat=f=s32[OUT]" -map [OUT] -f s32le +fate-swr-flt-s64-clip: CMP = oneline +fate-swr-flt-s64-clip: REF = e47630b4a3176417841c079ae76d2b85 + +FATE_SWR += $(FATE_SWR_S64_CLIP-yes) + FATE_SWR_22_2_LAYOUTS = 9.1.6 9.1.4 7.2.3 7.1.6 7.1.4 7.1.2 5.1.6 5.1.4 5.1.2 7.1 5.1 stereo mono FATE_SWR_9_1_6_LAYOUTS = 9.1.4 7.2.3 7.1.6 7.1.4 7.1.2 5.1.6 5.1.4 5.1.2 7.1 5.1 stereo mono FATE_SWR_9_1_4_LAYOUTS = 7.2.3 7.1.4 7.1.2 5.1.4 5.1.2 7.1 5.1 stereo mono -- 2.54.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
