From 7a4cdc8b5edbb12957408f2bf3ccbec061fcd213 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Wed, 19 Aug 2026 23:25:59 +0200 Subject: [PATCH] fix(player): pin loudnorm output to 48 kHz float to stop startup audio stutter Audio Normalization on the mpv path made every video start with stuttering audio on Linux: dynamic-mode loudnorm always outputs float64 at 192 kHz, so the AO opened at f64/192k and PipeWire had to convert/resample on the deadline-critical path (~4x the per-cycle DSP work) while playback startup load was still settling. Appending mpv's native format filter pins the chain back to 48 kHz float, so the conversion happens once on the buffered decode side and the AO opens a normal stream. Integrated loudness output is unchanged (-14 LUFS, verified via ebur128). mpv's own format filter is used instead of lavfi aformat so the fix does not depend on which lavfi filters each platform's bundled ffmpeg compiles in. Addresses the normalization half of #1720; the residual startup micro-stutter with normalization disabled is a separate regression. --- lib/mpv/player/player_base.dart | 10 +++++++++- test/mpv/player_native_bridge_test.dart | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/mpv/player/player_base.dart b/lib/mpv/player/player_base.dart index a8e1c8a5b..962768682 100644 --- a/lib/mpv/player/player_base.dart +++ b/lib/mpv/player/player_base.dart @@ -1149,7 +1149,15 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { /// mpv loudnorm targeting streaming-style loudness; mirrored by the /// Android ExoPlayer effect parameters in AudioNormalizationEffect.kt. - static const _loudnormFilter = 'loudnorm=I=-14:TP=-3:LRA=4'; + /// + /// Dynamic-mode loudnorm always outputs float64 at 192 kHz, which made the + /// AO open at f64/192k and forced the OS mixer to convert/resample on the + /// deadline-critical path (~4x the per-cycle DSP work), underrunning during + /// playback startup (#1720). The trailing mpv-native format filter pins the + /// chain back to 48 kHz float, so the conversion runs once on the buffered + /// decode side. mpv's own `format` filter is used instead of lavfi + /// `aformat` because the bundled Linux ffmpeg prunes lavfi filters. + static const _loudnormFilter = 'loudnorm=I=-14:TP=-3:LRA=4,format=srate=48000:format=floatp'; @override Future setAudioNormalization(bool enabled) async { diff --git a/test/mpv/player_native_bridge_test.dart b/test/mpv/player_native_bridge_test.dart index 74ef30387..7df0be6cb 100644 --- a/test/mpv/player_native_bridge_test.dart +++ b/test/mpv/player_native_bridge_test.dart @@ -1003,9 +1003,9 @@ void main() { await expectLater(player.setAudioPassthrough(true), throwsA(isA())); expect(propertyWrites.where((write) => write.$1 == 'af').map((write) => write.$2), [ - 'loudnorm=I=-14:TP=-3:LRA=4', + 'loudnorm=I=-14:TP=-3:LRA=4,format=srate=48000:format=floatp', '', - 'loudnorm=I=-14:TP=-3:LRA=4', + 'loudnorm=I=-14:TP=-3:LRA=4,format=srate=48000:format=floatp', ]); expect(player.audioPassthroughActive, isFalse); } finally {