fix(Tauri): Linux 输出请求显式周期,消除 pipewire-pulse 约 2s 缓冲延迟 - #17
Burial0268 wants to merge 4 commits into
Conversation
CPAL 的 PulseAudio 后端把 BufferSize::Default 映射为五个字段全是 u32::MAX 的 BufferAttr(协议里"服务端自选"的哨兵值),并且只在 Fixed 时才置 adjust_latency。 真正的 pulseaudio 守护进程恰好会选一个小缓冲,pipewire-pulse 选约 2s, 且没有任何一方去凑延迟目标(RustAudio/cpal#1190,该 issue 被 #1258 关闭, 修的是短音频截断,持续播放的缓冲行为在 0.18.1 里原样保留)。 一个过深的服务端缓冲同时表现为四个问题:按下播放约 1.5s 才出声; 歌词与进度同量领先,因为 render clock 统计的是交给服务端的样本而非已播放的; 每次恢复播放都要重等一次,因为暂停时回调仍在往服务端灌静音而不是停流; seek 之后还会继续播最多约 1.5s 的旧音频,因为 flush 收不回服务端已持有的 PCM。 改为请求显式周期。周期按毫秒推导而非固定帧数:CPAL 会把它翻倍成 Pulse 的 max/target length,固定帧数会让 96kHz sink 的延迟目标减半、192kHz 减到四分之一。 20ms 与 desktop.rs 的 Windows/macOS 策略一致,且远在 48 块(约 512ms) mixer ring 之内,抖动余量仍由软件 ring 提供。 Unknown 区间保持 BufferSize::Default:那是 CPAL ALSA 在读不到 hw params 时的 返回值,ALSA 直连用户本就没有 Pulse 缓冲问题,强推固定周期反而可能直接建流失败。 Range 的收敛不用 clamp,避免后端给出反向区间时 panic 掉输出线程。 Closes #16
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
❌ Deploy Preview for gbcl-ncmplayer failed. Why did it fail? →
|
复核反馈已完成 PR #17 的 Linux 复核。三处收敛(毫秒推导 / Unknown 回落 Default / 免 clamp 收敛)均合理,感谢采纳。 一、自动化验证环境:Arch Linux / Rust 1.97.1 / PipeWire 1.6.8
二、手动实测方式:
有线 + 笔记本音频源均正常,无爆音/卡顿。 三、需要关注的观察歌词对齐:歌词偏移设为 +0.5s(提前) 时视觉对齐优于 0s,提示歌词与声音间仍存在约 0.5s 偏移,比 PR 描述中预期的 ~40ms 大一个数量级。此现象仅作客观记录,未做根因排查,想确认这符合你的预期吗? 日志输出(与 PR 无关): 四、结论修复有效,症状 1-4 全部解决,可以合入。蓝牙场景需补充验证。 |
0ba9748 不是一次编辑,而是把整个模块又插了一遍:文件从第 31 行起出现第二份 use / DEFAULT_QUEUE_BLOCKS / stable_buffer_size / mod tests, STABLE_OUTPUT_BUFFER_MS 出现三次(120、120、20),并留下了 "Fix test later" 这类未完成的注释。重复定义使该模块无法编译。 Windows 上没被发现,是因为 linux.rs 由 #[cfg(target_os = "linux")] 门控, 本机 cargo check 根本不会编译它。 那次改动想把周期从 20ms 提到 120ms,以回应 PR #17 复核里约 0.5s 的歌词偏移, 但方向是反的:cpal 0.18.1 的 make_playback_buffer_attr 把 target_length 设为 周期的两倍并置 adjust_latency,20ms 对应约 40ms 的延迟目标,120ms 会变成约 240ms;Pulse 上报 Range { min: 1, max: ~262144 },5760 帧不会被收敛。 该注释自身也算错了帧数(写成 576 / 480,实际 120ms@48kHz = 5760)。 歌词偏移的符号同样说明问题不在这里:musicLyric.ts 里 +0.5s 是 displayCurrentTime + offset,即时钟落后于声音,而设备缓冲只会让时钟领先。 本次仅还原到 4b05fb0 的内容,输出周期策略保持不变。
PlayPosition / SyncStatus 只带 position,不带发出时刻;前端 AudioTimelineSync 以到达时刻作为外推锚点(_lastPositionEvent.receivedAt),于是事件在 IPC 队列里 待多久,UI 时钟就永久落后多久:frontend = backend - Δ_ipc。 给 AudioThreadEventMessage 增加 sent_at(epoch 毫秒,与 JS Date.now() 同域), 在 forward_msg 里与 seq 一起打戳——那是进入传输层前的最后一刻,之后的都是可以 减掉的延迟,之前的已经算进 position 里了。前端据此把外推锚点回拨到打戳时刻。 只作用于外推锚点:_expectedAnchorPosition 仍用到达时刻,不改动 seek 后的 接受/拒绝行为。回退保护里 hold 与 reject 两个分支锚定的是当前位置,仍用 now, 避免重复推进。带 2000ms 上限、暂停时跳过,与 playerBridge.ts 的 MAX_ANCHOR_LATENCY_MS 一致——从属窗口一直有这个补偿,原生主窗口此前没有。 wasm.rs 保持 sent_at = 0:进程内后端没有传输跳数需要补偿。 types.rs 补了三个测试:epoch 域、to/to_none 保留时间戳(二者逐字段重建信封, 漏掉新字段不会有编译错误)、缺字段反序列化为 0。 这修的是锚点丢弃传输时间这一确定缺陷。PR #17 复核里约 0.5s 的偏移是否由此解释 仍需在 Linux 上实测——那个 0.5s 来自 LyricOffsetControl 的 500ms 步进按钮, 是上界而非测量值,建议改用设置面板的数值输入在 release 构建上细扫。
|
@gozhuimeng 稍微 push 了一下,works on my machine. 你看看你那边的环境修复没有。 |
Closes #16
Generated by Claude
感谢 @gozhuimeng 的详细排查——根因定位是对的,这个 PR 在他的方案基础上做了三处收敛。
根因
CPAL 的 PulseAudio 后端把
BufferSize::Default映射为BufferAttr::default(),即五个字段全是u32::MAX——PA 线协议里"服务端自选"的哨兵值(cpal-0.18.1/src/host/pulseaudio/mod.rs:611,pulseaudio-0.3.1的impl Default for BufferAttr,上游文档原文写着 "this value will default to something like 2s")。同一处代码里adjust_latency = matches!(buffer_size, Fixed(_)),所以Default下我们连"请把延迟控制在某个目标"都没说。真正的 pulseaudio 守护进程恰好会选一个小缓冲,pipewire-pulse 选约 2s。本仓库
Cargo.toml只开了pulseaudio+realtimefeature,cpal 的default_host()顺序是 PipeWire → PulseAudio → ALSA,所以现代 Linux 桌面上必然落到受影响的 PulseAudio host。顺带确认:cpal#1190 已被 #1258 关闭,但那个 PR 修的是"短音频被 drop 截断",#1190 原文即说明持续播放的应用不受截断影响;
BufferAttr::default()的行为在最新的 0.18.1(也就是我们锁的版本)里原样保留。升级 cpal 解决不了这个问题。一个缓冲,四个症状
issue 里报了前两个,另外两个是同源的:
output/render.rs:176的rendered_samples是交给服务端时自增,player/clock.rs:54直接按它外推,全程不扣设备延迟,领先量 == 服务端缓冲深度。output/render.rs:89在paused时是往回调填静音而非停流,2s 的 memblockq 会被静音填满。这解释了为什么现象是"每次按播放"而不只是首次起播。OutputWriter::flush()只作废我们自己 ring 里的块,已交给服务端的 PCM 收不回来。改动
stable_buffer_size改为请求显式周期,另加三点收敛:max_length/target_length,固定 1024 帧在 48kHz 是 21ms,到 96kHz sink 就变成 10.7ms、192kHz 只剩 5.3ms——在不保证 RT 优先级的 Linux 上对音乐播放器偏激进。20ms 与desktop.rs的 Windows/macOS 策略一致,48kHz 下得到 960 帧,和实测有效的 1024 基本等价。SupportedBufferSize::Unknown保持BufferSize::Default。 这是 CPAL 的 ALSA host 读不到 hw params 时的返回值(host/alsa/mod.rs:1456-1477)。ALSA 直连用户本就没有 Pulse 缓冲问题,对一个连范围都读不到的设备强推固定周期,是唯一可能把"延迟高"变成"建流失败/彻底没声"的路径。CPAL 的 pulse host 永远返回Range,所以对本 issue 的受影响用户零成本。clamp。u32::clamp在min > max时会 panic;后端给出的区间不该反向,但输出线程不值得为此冒 panic 的风险。软件侧的抖动余量没动:
DEFAULT_QUEUE_BLOCKS = 48×MIX_BLOCK_FRAMES = 512≈ 512ms ring,以及player/platform.rs的 160ms 起播 / 48ms seek 预缓冲水位,都保持原样。Android、Windows、macOS 走各自独立的platform/*.rs,不受影响。未处理(另开)
时钟仍然不扣设备延迟,只是把领先量从约 1.5s 压到约 40ms(与 Windows WASAPI 的 20ms 同量级)。要彻底修,得用上
output/mod.rs:841现在被丢弃的_info——OutputCallbackInfo::timestamp()能给出 callback / playback 两个时间点。那会动到所有平台的时钟,不适合塞进这个 PR。验证
cargo check -p gmplayer-audio-backend通过(Windows host;仅有 2 条与本改动无关的既存 dead_code warning)。rustfmt --check干净。Unknown回落Default。注意:linux.rs在 Windows 宿主上不参与编译,这些用例是在一个替换掉 cpal 类型、逻辑逐字相同的 harness 里跑过的,尚未在 Linux 上执行cargo test。需要 Linux 上复核
@gozhuimeng 如果方便的话,麻烦在这个分支上再确认几项(都是这次改动的真实风险面):
cd src-tauri && cargo test(output::platform::linux相关用例)Fixed(1024)一致)output_runtime重建路径)不出问题tlength+adjust_latency是最可能出 xrun 的场景output/mod.rs那条音频输出:... buffer=... rate=...日志的前后对比关于你问的"是否需要做成可配置项":暂不做。多一个设置面、改完还要重建输出流,而合理取值实际只有一个;如果后续真的出现需要按机器调的场景,用环境变量做 debug 开关更划算。