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.
This commit is contained in:
edde746
2026-08-19 23:25:59 +02:00
parent 20896a3d48
commit 7a4cdc8b5e
2 changed files with 11 additions and 3 deletions
+9 -1
View File
@@ -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<void> setAudioNormalization(bool enabled) async {
+2 -2
View File
@@ -1003,9 +1003,9 @@ void main() {
await expectLater(player.setAudioPassthrough(true), throwsA(isA<PlatformException>()));
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 {