Audacity 音频延迟实战:PortAudio 上报延迟为何不可信,以及如何用回环实验精确测量往返延迟
【免费下载链接】audacityAudio Editor项目地址: https://gitcode.com/GitHub_Trending/au/audacity
本文基于 Audacity 仓库中的调研文档 portaudio-reported-playback-capture-latency.md,围绕“边播放边录制时如何做延迟补偿”这一具体问题展开:先讲清延迟补偿的原理,再给出一个可复现的线缆回环实验来精确测量音频设备的往返延迟(round-trip latency),并用实测数据说明 PortAudio 上报的缓冲区延迟在多平台上系统性偏离真实值。读完本文,你能理解 Audacity 录音延迟补偿的实现路径(从 PortAudio 流信息到RecordingSchedule的样本丢弃逻辑),并掌握自己测量设备真实延迟的实验方法。
一、问题背景:边播边录为什么会错位
文档首先给出了一个直觉模型:想象有一根音频线,把你的声卡输出接回了输入。你按下播放键后,项目里的音频经过一段时间延迟才会从输入端被录回来。所谓延迟补偿(latency compensation),就是丢弃所有入站音频,直到项目音频真正到达输入端为止。这样用户就能一边播放工程、一边实时录制自己的声音,且录到的内容天然与播放同步,不需要任何手动微调。
Audacity 已经提供了一个供用户手动调节的延迟补偿设置。文档指出:只要能知道音频设备的真实往返延迟,把这个设置自动调到最优值就是件琐碎的事。
而难点在于:PortAudio 会报告输入/输出延迟(inputLatency/outputLatency),但实验证明这些值并不可靠。这份文档记录了一个作者认为能够给出精确往返延迟估计的实验。
二、实验设计:回环接线 + 脉冲正弦标记
文档给出的实验步骤完整如下(可直接复现):
- 用一根音频线把所选输出设备连接到所选输入设备;
- 在开始录音前,先创建一个 wav 文件;
- 在音频 IO 回调中(运行于音频驱动线程):
- 用一秒静音,然后接一段正弦波去覆写输出缓冲区;
- 把输入缓冲区的采样写入该 wav 文件;
- 录音停止时关闭 wav 文件;
- 在 Audacity 中打开这个文件,观察延迟。
原理很直白:正弦波从输出出发,经过“硬件 → 线缆 → 硬件”的完整往返路径后进入麦克风输入通道。在录得的文件中,静音段到正弦波起点之间的长度,就是该设备链路的真实往返延迟。这个方法绕过了驱动/系统的所有“纸面参数”,直接测量端到端时间,因此结论是硬件层面的事实。
三、实验结果:上报值与实测值的系统性偏差
文档给出的完整实测数据表如下(Windows 平台,三组音频主机 API):
| OS | Host | 请求缓冲区大小 (ms) (*) | PA 上报输出缓冲区大小 (ms) (**) | PA 上报输入缓冲区大小 (ms) | 实测往返延迟 (ms) | 误差 (上报 − 实测) (ms) |
|---|---|---|---|---|---|---|
| Win | WASAPI | 250 | 270 | 260 | 75 | 455 |
| Win | WASAPI | 100 | 120 | 110 | 75 | 155 |
| Win | WASAPI | 20 | 40 | 30 | 75 | -5 |
| Win | DirectSound | 250 | 250 | 62.5 | 420 | -107.5 |
| Win | DirectSound | 100 | 100 | 25 | 115 | 10 |
| Win | DirectSound | 20 | 不工作 | 不工作 | 不工作 | 不工作 |
| Win | ASIO | 500 | 50.5 | 47.9 | 100 | -1.6 |
| Win | ASIO | 100 | 50.5 | 47.9 | 100 | -1.6 |
| Win | ASIO | 20 | 26.5 | 24.7 | 54 | -2.8 |
(*)该值同时应用于输入和输出,例如 250 表示输入 250 + 输出 250,合计 500。 (**)同一个请求大小下,上报值可能因“仅播放”还是“播放+录音”都处于活动状态而剧烈不同。表中列出的是两者都活动时的取值。
从表中数据可以直接读出几个重要结论:
- 上报误差可正可负、量级可达数百毫秒。WASAPI 请求 250ms 时,PA 上报的输入+输出合计 530ms,而实测往返只有 75ms,偏差高达 455ms;而 DirectSound 请求 250ms 时方向恰好相反(合计 312.5ms,实测 420ms)。如果自动补偿直接信任上报值,录音会与播放错位数百毫秒。
- 实测往返延迟在很大程度上与请求的缓冲区大小无关:WASAPI 三组请求下实测恒为 75ms,ASIO 在 500/100ms 请求下恒为 100ms。这说明往返延迟主要由设备与驱动路径的固有延迟决定,而 PA 上报值却随请求大小、以及播放/录音是否同时活动而变化——这正是它“不可靠”的直观体现。
- 上报值对运行状态敏感:同一请求大小下,仅播放与播放+录音同时进行时 PA 的上报值可能“剧烈不同”,即同一段代码在不同场景会得到不同的“事实”。
- 各 Host API 的可信度差异极大:ASIO 的上报值最接近实测(误差在 ±3ms 内),DirectSound 次之,WASAPI 在大缓冲区请求下误差最大。
- DirectSound 在 20ms 请求下完全无法工作,说明小缓冲请求在不同 API 上还可能直接失败。
四、源码印证:Audacity 如何使用(并不完全信任)这些延迟
文档的结论并非空谈,仓库源码中可以看到 Audacity 对 PortAudio 上报延迟的实际用法,以及针对其不可靠性留下的补丁式处理。
4.1 打开流之后读取 PA 上报延迟
在 AudioIO.cpp 中,Pa_OpenStream成功后立即取流信息并注释道“把上报延迟作为硬件缓冲大小的提示(hint)”。随后源码对上报值打了三处明显的折扣:
- JACK 特例:注释明确说明“When using Jack as a host, PA calculates the wrong latency if a non system port is used”(JACK 作为 host 且使用非系统端口时 PA 算错了延迟),于是直接改用用户设置的延迟时长(代码注释还指向了一个记录该问题的 issue);
- ALSA 特例:在
__WXGTK__下,源码大段注释描述了 PortAudio 在 ALSA 上不报告缓冲大小、只报告periodSize * (periodsCount - 1)、periodsCount 会被 ALSA 自行改动等乱象,最终的处理是把播放延迟帧数乘以 3——注释原话是“Why 3? 2 doesn't work for me, 3 does :-)”; - 常规路径则照单收下
stream->outputLatency,但注释里自己都承认它是 “(likely incorrect) latency reported by PA”。
这些补丁恰恰是文档结论的代码侧佐证:不同平台的上报延迟偏差方向不一,只能逐平台打补丁,无法依赖统一公式。
4.2 自动延迟补偿的落地:mLatencyCompensation = -inputLatency - outputLatency
同样在 AudioIO.cpp,当自动补偿开关打开时:
if (AudioIOAutomaticLatencyCompensation.Read()) { mRecordingSchedule.mLatencyCompensation = -stream->inputLatency - outputLatency; }即自动补偿量取 PA 上报输入延迟与输出延迟之和的负值。而手动路径则直接读取用户设置(AudioIO.cpp):
mRecordingSchedule.mLatencyCompensation = AudioIOLatencyCompensation.Read() / 1000.0;这两个设置项的定义与默认值在 AudioIOBase.cpp:
| 设置键 | 类型 | 默认值 | 含义 |
|---|---|---|---|
/AudioIO/AutomaticLatencyCompensation | Bool | true | 是否用 PA 上报值自动补偿 |
/AudioIO/LatencyCompensation | Double | -130.0(ms) | 手动补偿量(负值表示丢弃前段录音) |
/AudioIO/LatencyDuration | Double | 100.0(ms) | 用户请求的延迟时长 |
值得注意的是手动默认值 -130ms 的“保守”量级——与 WASAPI 大缓冲场景下数百毫秒的真实误差相比,它更像是一个经验兜底值,而不是精确值。手动补偿项在 DevicePrefs.cpp 中还受到取值范围约束,并在设备参数变化时被Invalidate()(DevicePrefs.cpp)。新版 Qt 界面中,该开关暴露为偏好设置里的复选框,见 BufferAndLatencySection.qml,对应的配置字段枚举AutomaticLatencyCompensation = 1 << 5定义在 audioconfigurationtypes.h,驱动层通过设置键au3audio/AudioIO/AutomaticLatencyCompensation监听其变化(au3audiodrivercontroller.cpp)。
4.3 补偿如何变成“丢弃样本”:RecordingSchedule
补偿量最终落在 PlaybackSchedule.h 的RecordingSchedule结构中:
struct RecordingSchedule { double mLeadInTime{}; double mLatencyCompensation{}; // negative value usually ... double TotalCorrection() const { return mLatencyCompensation - mLeadInTime; } double ToConsume() const; double Consumed() const; double ToDiscard() const; };其中mLatencyCompensation注释标明“通常为负值”。具体的丢弃/消费计算在 PlaybackSchedule.cpp:
double RecordingSchedule::Consumed() const { return std::max(0.0, mPosition + TotalCorrection()); } double RecordingSchedule::ToDiscard() const { return std::max(0.0, -(mPosition + TotalCorrection())); }即:总修正量为负时(正常补偿情形),录到的前段样本按ToDiscard()计入“应丢弃”量,随录音推进逐步被消费——这正是文档第一段所述“丢弃所有入站音频直到项目音频到达”的精确实现。AudioIO.cpp 附近DrainInputBuffers的注释也再次确认:被丢弃的就是“leading frames that DrainInputBuffers discards for latency compensation”(因延迟补偿而丢弃的前导帧)。
4.4 上报延迟的另一用途:作为“低延迟提示”
除补偿外,PA 的设备级上报值还用于流参数的初始建议。AudioIOBase.cpp 在设置建议延迟时,播放取Pa_GetDeviceInfo(playDeviceNum)->defaultLowOutputLatency,录音取defaultLowInputLatency;DeviceManager.cpp 中同样以defaultLowInputLatency作为默认。设备信息面板也会直接展示这四项上报值(“Low/High Recording/Playback Latency”,AudioIOBase.cpp)。也就是说,上报值在 Audacity 中同时扮演两个角色:给用户的参考信息、以及自动补偿的原始输入——而本文档实验表明,后者在相当一部分平台上是错误输入。
五、对实践者的启示
结合文档实验与源码实现,可以得出如下可操作的结论:
- 不要直接信任 PortAudio 的
Pa_GetStreamInfo()延迟值,尤其在 WASAPI 大缓冲、ALSA、JACK 非系统端口等场景(本仓库源码中已存在后两者的专门修正代码)。 - 要精确知道自己的设备往返延迟,用回环实验测:一根音频线、一秒静音加正弦波、在 IO 回调中同时覆写输出与落盘输入,成本极低,结果却是端到端事实。
- 自动补偿公式本身没有问题(
-inputLatency - outputLatency在概念上正是文档所述“丢弃到项目音频到达”),问题出在喂给它的输入数据上;因此对精度要求高的场景,应像手动设置/AudioIO/LatencyCompensation一样,用实测往返延迟覆盖自动值。 - 不同 Host API 的上报行为不可互相类推:ASIO 几乎精确、WASAPI 系统性虚高、DirectSound 随场景漂移,这解释了为何 Audacity 源码中出现了按平台分叉的延迟处理分支。
延伸阅读
- 原始调研文档:docs/portaudio-reported-playback-capture-latency.md
- 流打开与延迟读取主逻辑:au3/libraries/au3-audio-io/AudioIO.cpp
- 补偿设置项定义:au3/libraries/au3-audio-devices/AudioIOBase.cpp
- 录音调度与样本丢弃实现:au3/libraries/au3-audio-io/PlaybackSchedule.h、au3/libraries/au3-audio-io/PlaybackSchedule.cpp
【免费下载链接】audacityAudio Editor项目地址: https://gitcode.com/GitHub_Trending/au/audacity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考