WebRTC 音频对讲总体架构:声音怎样从麦克风到达远端扬声器?

来源:飞一样的成长
原文:https://mp.weixin.qq.com/s/BgNhepELC1R3Jaax_PMDSA

在一块开发板上,麦克风能录音,扬声器能播放,WebRTC 也已经显示连接成功,远端却仍然可能听不到声音。即使两边终于能说话,接下来还会遇到回声、声音忽大忽小、短暂断续,或者越讲延迟越大。

这些现象发生在同一条音频链路上,却不一定属于同一个模块的问题。网络连接解决了报文怎样到达对端;完整的语音对讲还要求采集、音频处理、编解码、缓冲和设备播放按各自的节奏持续工作。

本文从麦克风输出的 PCM 开始,沿一条发送方向走到远端扬声器,再把反向声音和回声消除的参考信号接回来。先弄清数据是什么、由谁推动、在哪里等待,然后再进入相应源码。

1. 一场音频对讲,由哪些部分共同完成

1.1 先沿一个方向看声音

假设设备 A 放在门口,设备 B 是室内终端。有人对着 A 的麦克风说话,B 的扬声器最终要重现这段声音。

这条链路大致经过下面几类处理:

部分输入与输出主要责任
音频硬件与驱动声音、电信号、数字采样之间转换持续采集或播放,提供正确的格式、节拍和缓冲状态
设备适配平台音频缓冲与 WebRTC 音频块之间交接对接 ADM,适配块大小、采样率、声道及设备时序
发送端音频处理采集 PCM 进入,处理后的 PCM 输出根据配置完成回声消除、降噪、增益控制等
编码与实时传输PCM 变成编码数据,再成为网络报文压缩、RTP 封装、安全保护、发送调度与反馈处理
接收端音频恢复不规则到达的报文变成连续 PCM管理乱序与缓冲,解码,处理迟到或丢失的数据
混音与设备播放一路或多路 PCM 变成设备需要的输出按播放节奏供数,适配输出格式,最终推动扬声器
WebRTC 音频对讲总体架构:声音怎样从麦克风到达远端扬声器?
图 1:先画出 A 到 B 的一个声音方向。完整对讲还包括 B 到 A;每个端点的播放内容又会成为本机回声消除的重要参考。图中表示主要数据关系,省略了部分代理、任务切换和平台辅助对象。

有两个分界很重要:编码器之前主要处理 PCM,网络上主要传输编码后的受保护报文;接收端必须重新得到 PCM,扬声器才能播放。

所谓“把声音发过去”,不是把录音线程中的一块内存原样交给另一端的声卡。中间的数据形态、时间单位和驱动方式都会变化。

1.2 会话控制与声音流动各自解决什么

创建会话时,应用需要接入音频设备、组织媒体轨、协商编解码能力和收发方向,并建立传输与安全上下文。PeerConnection、媒体引擎和音频流对象把这些能力组织起来。

进入稳定通话以后,声音主要依靠采集回调、编码任务、收包处理与播放请求持续流动。接听按钮和 SDP 交换不会代替设备持续供给 PCM,也不会代替播放设备按时取走 PCM。

同样,ICE 已连接、DTLS 已建立,只能说明相应传输条件取得进展。它们不能证明麦克风采到了有效声音,也不能证明远端功放已经打开。

2. 麦克风交进来的到底是什么数据

2.1 PCM 需要同时说明四件事

PCM 描述的是声音随时间变化的采样值。仅传一个指针和一段字节长度,接收模块仍然不知道应该怎样解释它。

至少要明确下面四项:

参数含义示例
采样率每个声道每秒有多少采样点48 kHz,即每声道每秒 48,000 个采样点
声道数每个采样时刻有几路声音单声道、双声道
样本格式一个采样值怎样表示有符号 16 位整数、浮点数
布局与连续性多声道怎样排列,这批样本接在哪一批之后交错或平面布局,连续样本位置与时间信息

对采集端来说,还要知道这批数据与真实采样时间的关系。程序拿到缓冲的时刻,通常已经晚于 ADC 采到其中第一个样本的时刻。驱动缓存、DMA 周期和线程调度都会拉开这两个时间点。

下面统一使用 48 kHz、单声道、16 位整数 PCM 做计算示例:

每声道样本数 = 采样率 × 时长
10 ms 的样本数 = 48,000 × 0.010 = 480
10 ms 的 PCM 字节数 = 480 × 1 声道 × 2 字节 = 960 字节

改成双声道后,10 ms 仍然是每声道 480 个样本,只是总字节数变成 1,920 字节。声道翻倍,不代表这批声音的时间长度翻倍。

嵌入式平台的 DMA 缓冲也可能使用 24 位有效数据装在 32 位槽中的格式。此时应按实际内存布局解释缓冲,不能把“有效位宽”和“每个样本占用的字节数”混为一谈,再强行套用上面的 16 位示例。

2.2 硬件 period、处理块和编码包,是三种粒度

阅读 WebRTC 音频源码时,会频繁遇到 10 ms。这主要反映了若干音频接口的处理粒度。

沿本文的 Native 路径,采集交接、音频编码模块的 Add10MsData(),以及 NetEq 的 GetAudio(),都能看到 10 ms 这个单位。但它不能推出“硬件每 10 ms 中断一次”,也不能推出“网络每 10 ms 必须发送一个音频包”。

WebRTC 音频对讲总体架构:声音怎样从麦克风到达远端扬声器?
图 2:以 48 kHz 单声道为例,两个 10 ms 输入块可以组成一个携带 20 ms 声音的 Opus 包。这里的 20 ms 是选定的教学配置,实际包时长需要看协商、编码器配置与运行策略。

硬件可能每次交出更小或更大的缓冲。适配层需要先积累、拆分或缓存,再以内部接口需要的大小交接。编码器也可能连续接收几个 10 ms 块,等到满足当前包时长后才产生编码输出。

当前 modules/audio_device/fine_audio_buffer.cc 就体现了这种适配:录音侧把收到的数据积累到足够的 10 ms 再向上交付;播放侧按需获取内部音频块,再切出设备本次需要的数据。这个类展示了可复用的适配方式,不代表每一个平台后端都必须使用它。

2.3 “一帧音频”必须说明所处层次

讨论接口时,尽量说“10 ms PCM 块”“20 ms 编码包”或“一个 RTP 包”,少用没有上下文的“一帧”。

PCM 块、编解码器内部帧、Opus 包与 RTP 包不是天然一一对应。即使一段代码里都用了 frame 这个词,也要继续确认它表示样本块、编码单元,还是传输对象。

这层区别会直接影响缓冲大小、时间戳步长和延迟估算。

3. 采集设备怎样把 PCM 交给 WebRTC

3.1 ADM 是平台音频接入的边界

ADM 是 Audio Device Module。它对上提供音频设备控制与数据交接能力,对下接入平台的录音、播放接口。

在嵌入式 Linux 上,实际后端可能使用 ALSA,也可能对接芯片厂商的音频 SDK。I2S、音频 Codec 芯片、DMA 和驱动缓冲仍由相应平台层负责;WebRTC 不会因为创建了 PeerConnection,就自动完成板级声卡路由和功放控制。这里的音频 Codec 芯片承担 ADC、DAC 等硬件功能,与 Opus 这样的压缩编解码器处在不同层次。

当前设备接口的主要头文件是 api/audio/audio_device.h。modules/audio_device/include/audio_device.h 在本文版本中保留为兼容包含入口,阅读新代码时应继续进入实际定义。

从职责上,可以把 ADM 与 AudioTransport 看成两侧的接口:

  • ADM 管理平台设备怎样开始、停止、采集和播放。
  • AudioTransport 承接录音数据,并响应播放侧需要 PCM 的请求。
  • AudioTransportImpl 把这些设备回调接入音频处理、发送流和混音器。

3.2 采集是“数据到了,往前送”

沿当前 Native 主线,平台适配完成后,通过 RecordedDataIsAvailable() 把录音 PCM 交到 AudioTransportImpl。

这里不会立即把这批 PCM 当成 UDP 负载发送。audio/audio_transport_impl.cc 先进行适合后续处理的声道和采样率适配,再进入配置好的采集处理路径,最后把处理结果交给发送流。

如果配置了异步音频处理,结果还可能经过异步处理完成回调后再进入发送。因此图中的“处理后交给发送流”表达的是数据依赖,不能理解为所有工作都必然在一个函数栈中一次做完。

WebRTC 音频对讲总体架构:声音怎样从麦克风到达远端扬声器?
图 3:采集端在数据准备好后向前推送;接收端的网络报文进入 NetEq,而播放链按照设备需要主动拉取 PCM。实线表示音频数据,虚线表示取数请求。网络到达节拍与播放节拍并不相同。

3.3 接入时最容易犯的错,往往发生在缓冲交接

采集回调中的内存可能属于驱动或 SDK,返回以后就会被下一批数据覆盖。若需要跨任务序列处理,必须采用明确的复制或所有权转移方式,不能把一个即将复用的裸指针异步留给编码任务。

同时,音频回调通常有持续供数的时序要求。把大文件写入、网络等待、长时间锁竞争塞进回调,会让后面的声音即使“数据没丢”,也越来越晚。

这些问题不应靠无限增大队列掩盖。需要记录回调间隔、积压的样本数、缓冲复用时机和异常恢复行为,再判断是设备供数不稳、处理超时,还是队列没有受到约束。

还有一个接口细节容易误读:传统 RecordedDataIsAvailable() 的 bytes_per_sample 参数,在当前实现中与交错声道数据的总步长相关;对 16 位输入,代码会按 2 × channels 检查它。不能只按参数名猜成“每声道固定传 2”,更不能据此混淆总字节数与每声道样本数。

4. APM 怎样处理声音,AEC 为什么需要播放参考

4.1 3A 分别处理什么问题

APM 是 Audio Processing Module。它位于音频处理链中,可以对采集信号进行语音增强。

常说的音频 3A,通常指 AEC、ANS/NS、AGC。WebRTC 中常用 NS 表示降噪。它们处理的问题各不相同:

能力要处理的现象需要注意的边界
AEC,声学回声消除本机扬声器的声音被本机麦克风再次采入,随后传回远端依赖合适的播放参考与时序,不能只看开关是否开启
NS,噪声抑制风扇、环境等噪声混入采集信号不能保证消除所有非语音声音,抑制强度也可能影响语音
AGC,自动增益控制讲话距离或声音大小变化,使发送电平差异过大不能恢复 ADC 已经削顶的信号,也不等于扬声器音量控制

这些模块的最终启用状态,需要从应用配置一直看到媒体引擎与设备能力。裸 AudioProcessing::Config 中的初始值,不等于一场实际通话的最终配置。

例如,当前 WebRtcVoiceEngine 会建立默认音频选项,再结合平台差异和设备内建 AEC、AGC、NS 的能力调整软件处理。不能仅看到原始配置结构里的 enabled = false,就判断 WebRTC 通话默认没有 3A;也不能反过来假定所有平台始终同时启用同一套软件算法。

当前默认实现中,在启用回声处理且没有注入自定义回声控制实现的情况下,可以沿代码进入 AEC3。增益控制也存在不同配置路径。总体架构图只画职责,不把 AEC、NS、AGC 的内部调用顺序写成所有版本都不变的公式。

4.2 回声究竟在哪里产生

B 说话,声音通过网络到达 A。A 的扬声器把它播出来,A 的麦克风又采到了这部分声音。

此时 A 的麦克风信号里可能同时包含:

  • A 附近的人正在说的话。
  • A 扬声器播放的 B 的声音经过房间传播后的回声。
  • 环境噪声。

如果这些信号一起原样发送回 B,B 就会听见自己的声音绕了一圈回来。

AEC 因此需要两个方向的信息:本机麦克风采到了什么,以及本机扬声器准备播放或正在播放什么。前者是待处理的采集信号,后者提供回声参考。

WebRTC 音频对讲总体架构:声音怎样从麦克风到达远端扬声器?
图 4:AEC 处理的是本机播放与本机采集之间的声学耦合。参考取自相应的播放 PCM 路径;网络另一端也需要独立处理自己的回声关系。

参考信号提供了已知播放内容,算法据此估计它经过扬声器、房间和麦克风以后会怎样出现在采集信号中。这个关系会受摆放、音量、设备延迟和非线性失真影响,不是把两段 PCM 按相同数组下标直接相减。

4.3 Native 路径中的参考在哪里送进去

当前 AudioTransportImpl::NeedMorePlayData() 会先让混音器产生播放 PCM,再在存在 APM 时进入 ProcessReverseAudioFrame(),之后按设备要求进行输出适配。

“Reverse”是相对于采集处理方向而言:采集方向走 ProcessStream(),播放参考进入相应反向处理接口。它不是“把音频包反向发送回网络”。

所以,正确参考通常应覆盖本机实际播放的内容及其时间关系。一个刚收到的 Opus 压缩包还没有完成解码、丢包补偿与混音,不能直接拿来当 AEC 的 PCM 参考。

如果应用绕过这条路径,另外向扬声器播放提示音,或者在参考点之后增加混音、增益和较大的缓冲,就要重新核对参考与实际输出的对应关系。硬件 AEC、系统 AEC 和软件 AEC 也需要统筹配置,避免不经评估地重复处理同一段声音。

PullRenderData() 在当前源码中另有 Chromium 相关说明:该路径不在这里执行反向 APM,因为 Chromium 的集成位置不同。这也是本文明确限定 Native 路线的原因。

4.4 VAD、DTX 与双讲检测,不是同一件事

VAD 判断一段信号中是否存在语音活动。它可以参与语音业务或某些算法的决策,但不能直接替代回声消除。

双讲指近端和远端同时说话。AEC 在这种状态下需要尽量保留近端语音,同时处理远端播放造成的回声。仅凭“检测到语音”这个结果,无法说明检测到的是近端说话、远端回声,还是两者混合。

DTX 则与编码发送策略有关:在适当条件下减少静音阶段的传输,接收侧保持相应的舒适噪声或连续输出。它不意味着声卡停止采集,也不意味着播放线程可以等到下一次有人说话才恢复运行。

因此,开发“有人说话才触发业务”时,需要明确使用哪一种检测结果、在哪个处理位置观察信号、怎样防止截断开头和结尾。不能看到旧资料里的 VAD 接口名称,就假定当前 APM 仍公开相同接口。

5. 处理后的 PCM,怎样变成发给远端的报文

5.1 编码之前,数据仍然是按时间连续的 PCM

APM 处理完成后,发送流把 PCM 交给音频发送通道。当前路径可以从 AudioSendStream::SendAudioData() 跟到 ChannelSend::ProcessAndEncodeAudio()。

编码工作进入相应的编码任务队列,再由音频编码模块接收 10 ms 数据。这里引入任务队列,是实现中的执行组织;队列本身不会自动保证任意负载下都没有积压。

Opus 编码器把输入声音压缩成适合传输的数据。压缩的是表示方式,声音对应的时间长度仍然存在。20 ms 的 PCM 经过编码后,负载可以小很多,但它表示的仍然是那 20 ms 的声音。

在本文示例中,编码器连续接收两个 10 ms 块,满足 20 ms 包时长后产生一个编码包。第一次提交数据时没有输出完整包,可以是正常的组包行为,不能立即判断为丢音。

5.2 RTP 序号与时间戳分别回答两个问题

RTP 序号用于标识报文的先后关系,便于识别乱序与缺口。RTP 时间戳则描述媒体采样时间轴上的位置。

以连续发送、每包携带 20 ms Opus 音频的例子说明:

第一个包:sequence = N,     timestamp = T
第二个包:sequence = N + 1, timestamp = T + 960
第三个包:sequence = N + 2, timestamp = T + 1920

RFC 7587 规定,Opus 的 RTP 时间戳采用 48 kHz 时钟,所以 20 ms 对应的增量是 48,000 × 0.020 = 960。这不是字节数,也不会因为双声道而再乘 2。

这里的 T 是媒体时钟中的位置,不能直接理解成毫秒系统时间。包时长变化、DTX 和丢包会改变观察到的序号或时间戳关系,所以这个例子只描述选定条件下的连续发送。

也不能看见 SDP 中的 opus/48000/2,就断言板上的麦克风一定以 48 kHz 双声道采集。RTP 时钟、Opus 的信令约定、实际编码内容和硬件采集格式,需要分别确认。

5.3 编码输出之后,还有发送调度与安全传输

ChannelSend::SendData() 取得编码结果后,将它接入 RTP 发送路径。RTP 相关模块组织负载、序号、时间戳和头扩展,发送调度决定报文怎样进入实际传输。

到传输层,音频 RTP 经过 SRTP 保护,控制报文相应使用 SRTCP。DTLS 参与建立安全上下文和导出所需密钥;正常音频 RTP 不需要逐包封装成 DTLS 应用数据。

ICE 提供实际使用的传输路径,可能直连,也可能经过 TURN。编码器不需要知道底层是否存在家庭路由器,但应用排查质量问题时必须知道实际路径和网络条件。

发送侧还会接收反馈。码率、丢包恢复及发送策略需要与可用网络能力配合,不能因为音频码率比视频小,就认为音频不需要考虑拥塞和排队。本文先说明这些模块的位置,具体算法另行展开。

6. 远端收到音频包,为什么还不能立即播放

6.1 报文到达顺序,不等于声音播放顺序

发送端按声音时间轴组织报文,网络却可能让某个包晚到、重复到达,或者完全丢失。

例如,本应先后到达的包 101、102、103,在接收侧可能表现为 101、103、102。若按照 socket 收包顺序立即播放,声音的时间顺序就会被打乱。

另一方面,扬声器不能无限期停在那里等待 102。播放设备消耗数据有自己的节奏:即使网络暂时没有送来正确内容,它也需要下一段 PCM。

接收链因此需要把“不规则到达的编码报文”转成“按播放需求连续输出的音频”。

6.2 NetEq 管的是从包到连续 PCM 的过程

NetEq 是 WebRTC 音频接收链中的自适应抖动缓冲与音频恢复模块。它的工作范围比一个普通 FIFO 更大。

当前 ChannelReceive 将收到的有效编码负载交给 NetEq::InsertPacket()。播放方向需要数据时,再通过 NetEq::GetAudio() 取得 10 ms PCM。

WebRTC 音频对讲总体架构:声音怎样从麦克风到达远端扬声器?
图 5:NetEq 管理包与播放时间关系,并按需要调用解码器、执行丢包掩盖和时间调节。图中各能力是职责示意,不表示每次输出都必须顺序执行所有步骤。

内部大致需要协调四件事:

工作为什么需要
管理包、时间戳与冗余信息按媒体时间关系使用数据,处理乱序、重复和可用冗余
估计合适的缓冲目标为网络波动留余量,同时限制额外等待
调用解码器并管理已解码 PCM一个较长编码包产生的 PCM,可以分多次供播放取走
处理缺失内容与缓冲偏差在必要时进行丢包掩盖、舒适噪声生成和时间伸缩

所以,不宜画成“NetEq FIFO → 独立解码器 → PCM”,再让人误以为 NetEq 只存压缩包。沿 neteq_impl.cc 的 GetAudioInternal() 可以看到决策、解码和相应输出处理都参与其中。

6.3 缓冲越大,不等于对讲越好

缓冲多留一些时间,较晚到达的包就有更多机会赶上播放;代价是人说完一句话,对方更晚听到。

缓冲太小,网络稍有波动就可能来不及等到需要的内容;缓冲太大,音频可以很平稳,却不适合自然对话。NetEq 持续根据运行情况调整,而不是给所有会话固定塞入同一段延迟。

它也会对 PCM 做适当的时间调节,避免缓冲长期偏离目标。这不是简单把声卡配置从 48 kHz 改成另一个数值,也不能承诺自动修复错误的采样率声明、严重的设备时钟异常或无限增长的应用队列。

6.4 PLC、FEC、NACK 的恢复方式不同

机制用什么处理缺失内容时间与能力边界
PLC,丢包掩盖利用已有音频和解码器状态合成替代样本维持连续听感,不承诺恢复原始波形
FEC,前向纠错利用发送端实际附带的冗余信息相关信息必须存在,并在能使用的时间内到达
NACK 与重传接收端请求重发缺失数据需要相应配置和传输支持,重传还要赶上播放需求

Opus 带内 FEC 与视频里的其他 FEC 方案不能仅凭名字相同就混为一谈。开启相关能力,也不表示每一个丢包都必定有可用冗余。

本文版本的音频接收通道包含按配置工作的 NACK 跟踪与反馈。因此,“实时音频从不重传”和“只要重传就一定能恢复播放”都不准确。

AEC 处理的是本机声学回声,NetEq 处理的是接收和播放之间的音频恢复。回声、乱序和丢包可以同时存在,但需要分别定位。

7. 扬声器怎样持续拿到 PCM

7.1 播放由设备需求向上拉取

在默认 Native 路线中,设备适配层需要更多 PCM 时,通过 AudioDeviceBuffer 请求播放数据,进入 AudioTransportImpl::NeedMorePlayData()。

这条调用继续进入混音器。混音器向各音频源请求数据,接收流再让 ChannelReceive 从 NetEq 取出对应的 PCM。

这里有两条不同方向的关系:

  • 取数请求从播放设备向混音器、接收流和 NetEq 传递。
  • PCM 数据从 NetEq 经接收流、混音器与输出适配返回设备。

网络收包则在另一条处理路径上把数据插入 NetEq。不能把整个接收过程理解成“一个 RTP 包触发一次扬声器写入”。

7.2 混音器与重采样器分别做什么

混音器将需要播放的音频源合成为输出信号。即使当前只有一个远端音频源,统一的播放组织仍然可以通过混音接口完成。

重采样解决的是不同处理阶段所需采样率之间的转换。例如,解码输出采样率、混音率和最终设备输出率可能不同。声道适配则处理单声道、双声道等格式关系。

它们不会自动把任意格式的内存变成正确声音。若声道交错顺序、每声道样本数或数据格式声明错误,后面即使每个函数都返回成功,也可能出现变速、噪声或完全静音。

当前主线可以概括为:从 NetEq 取得音频,接收流按需要适配到混音率,混音完成后进入本机 APM 播放参考路径,最后适配到设备请求的输出格式。

默认远端播放链也不意味着自动把本机麦克风混进本机扬声器。需要监听或侧音时,应单独设计,避免无意间建立新的声学反馈环。

7.3 输出 PCM 之后,声音还没有立刻出现在空气里

把 PCM 写入平台接口,通常表示数据进入了某一级播放缓冲。之后仍可能经过系统音频队列、DMA FIFO、Codec 和功放,最后才从扬声器发声。

因此,NeedMorePlayData() 返回的时刻不能直接等同于其中第一个样本的实际声学输出时刻。设备排队长度和播放位置,是估算时延与处理回声时都要关注的信息。

如果播放链没有及时供数,设备可能发生欠载。即使网络完全没有丢包,也可能听到断续。此时继续加大 NetEq 的网络缓冲,未必能解决本机调度或驱动供数问题。

8. 两个声音方向怎样组成完整对讲

8.1 每个端点同时扮演发送者与接收者

A 发送给 B 时,A 在采集和编码,B 在接收和播放。反向讲话时,两端职责交换。全双工通话要求这两组工作能够同时持续运行。

两条媒体方向可以使用同一场会话协商出的传输资源,但仍各自维护媒体时间线、编码状态和接收缓冲。不能因为 A 到 B 能听见,就推断 B 到 A 的设备和媒体链也已经正常。

每一端还存在本机的 AEC 关系:它处理本机扬声器与本机麦克风之间的耦合。远端开启 AEC,不能代替本端处理本端的回声路径。

8.2 “按住说话”改变的是业务策略

有些设备采用按键控制发送。这里至少存在几种不同动作:禁止发送麦克风内容、给发送链提供静音、暂停某条媒体流,以及真正停止录音设备。

这些动作对编码器、DTX、设备启动开销和其他共享音频流的影响不同,不能全部叫“关闭麦克风”后就认为等价。恢复讲话时,还要避免把停顿期间积压的旧音频重新播放出来。

如果产品希望双方可以自然打断对方,就应按全双工场景验证;如果采用轮流讲话,则要明确按键状态、抢话规则和起止边界。ICE 的角色和网络是否直连,不决定谁可以开口。

8.3 音频对讲不要求把 PCM 塞进 DataChannel

标准音频媒体链通过音频轨、编解码与 RTP 组织发送。按钮、状态或设备控制消息可以另走业务信令或合适的数据通道。

若把 PCM 自行塞进 DataChannel,就需要另外处理媒体节拍、时间戳、缓冲、过期数据和播放等问题。那是自定义音频传输方案,不能把它当成启用 WebRTC 对讲的必要步骤。

对于嵌入式接入,首先要完成的是设备 PCM 与正确音频媒体路径的衔接。

9. 对讲延迟为什么会累积,音频时钟为什么不能忽略

9.1 先明确测量的是哪一段

“音频延迟 100 ms”如果没有起止点,工程意义很有限。至少要区分从 PCM 进入编码器到远端 PCM 输出,和从本端真实声音输入到远端扬声器发声。

后者通常称为口到耳延迟。按顺序拆分,可以观察下面这些部分:

采集与输入缓冲
+ 样本分块、音频处理和编码组包等待
+ 发送调度与网络传输
+ 接收等待、音频恢复与解码
+ 混音、输出缓冲和设备播放
= 对应测量口径下的端到端声音延迟

这是一种排查分解方式。具体测量时应让各段起止点互不重叠,避免把同一段等待同时计入“组包”和“编码”,或者同时计入“NetEq”和“接收缓冲”。

处理块是 10 ms,也不意味着每经过一个函数都固定再加 10 ms。拿到完整 PCM 块后,一些计算可以立即完成;真正增加多少时间,要看块形成过程、算法自身需要的上下文、任务等待和后端缓冲。

9.2 包时长与队列占用,要按时间换算

较长的编码包需要覆盖较长的声音片段,通常意味着更多组包等待。它可能减少单位时间的报文数量,但也改变丢失一个包影响的声音长度。具体选择需要结合带宽、时延目标和恢复策略。

队列也要按“里面装了多长时间的声音”观察。假设队列中确实积压了 6 个尚未消费的 10 ms PCM 块,就相当于积压了 60 ms 音频。这是计算示例,不是建议的队列配置。

缓冲分配了 60 ms 容量,却不代表平时一定等待 60 ms。容量上限、当前占用和目标缓冲时长应分别记录,不能只看申请了多少内存。

9.3 同样标称 48 kHz,两块设备也不会天然同步

设备 A 的采样时钟和设备 B 的播放时钟由各自硬件驱动。即使都配置成 48 kHz,实际速率也可能存在小偏差。

如果只把接收样本不断放入固定 FIFO,不处理长期速率差,缓冲可能缓慢增长,也可能反复接近耗尽。网络抖动主要表现为到达间隔波动;时钟偏差还会造成长期的样本供需差异。

WebRTC 的接收恢复、重采样和设备适配需要共同处理正常工作范围内的问题,但不能把“有 NetEq”理解成任何错误配置都能被掩盖。例如,实际送入 16 kHz 数据,却声明成 48 kHz,就已经改变了程序对这批样本时长的解释。

当前 Native 采集回调还保留一些历史参数,但实现并不使用其中的 clock_drift、volume 与 new_mic_volume 完成通用硬件补偿。传了这些参数,不等于板级时钟和麦克风模拟增益闭环已经自动接好。

9.4 AEC 的本机延迟参数,不是网络 RTT

APM 中与播放、采集对齐有关的延迟描述,关注的是本机播放参考到硬件输出,以及硬件采集到处理入口之间的设备和缓冲时序。

网络已经把远端声音送到了本机,AEC 要解决的是这段声音随后怎样从本机扬声器进入本机麦克风。把网络 RTT 填成本机设备延迟,会把两个不同问题混在一起。

同样,ICE 候选对的 RTT、NetEq 缓冲统计和 Native 接收延迟估计,都各有范围。它们可以帮助定位问题,但不能直接替代口到耳测量。

10. 对照官方源码,把整条音频链连起来

10.1 谁把设备、处理器和音频流接在一起

在本文版本中,WebRtcVoiceEngine 组织音频引擎的初始化和配置。它创建相应 AudioState,接入 ADM、APM 和混音器;设备初始化与音频选项处理以后,通过 RegisterAudioCallback() 把设备回调接到音频传输对象。

AudioState 进一步组织发送流列表与混音源。发送流加入后,采集处理结果才有相应的发送目标;接收流加入混音器后,播放拉取才能沿源接口取得远端音频。

这解释了为什么“ADM 对象已经创建”仍然不等于“对讲已经接好”。设备初始化、媒体流的创建与启停、回调注册和协商状态都需要衔接。

WebRTC 音频对讲总体架构:声音怎样从麦克风到达远端扬声器?
图 6:图中展示设备桥接、发送处理、网络接收与播放输出的主要数据方向。播放取数请求的方向另见图 3;这里不是所有对象都在同一线程里的完整同步调用栈。

10.2 发送方向:先跟住 PCM,再跟编码输出

入口主要作用本轮阅读应回答的问题
AudioTransportImpl::RecordedDataIsAvailable()接收设备交付的录音数据数据格式怎样检查,采样率和声道怎样适配
ProcessCaptureFrame()进入相应采集处理APM 在哪里处理 PCM,设备延迟怎样交入
SendProcessedData()向活动发送流交付处理结果哪些流会收到音频,缓冲怎样复制或转移
AudioSendStream::SendAudioData()交给音频发送通道流对象怎样接到 ChannelSend
ChannelSend::ProcessAndEncodeAudio()调度编码任务哪一步进入编码队列
音频编码模块的 Add10MsData()提交编码输入一次 10 ms 输入何时产生编码输出
ChannelSend::SendData()、SendRtpAudio()衔接编码结果与 RTP 音频发送编码负载怎样进入打包和发送路径

这里的表是阅读顺序,具体作用域要以文件中的定义为准。函数之间还可能存在回调、锁、任务投递和可选处理,不应仅按名字把它们拼成一条无条件同步调用链。

10.3 接收与播放:要沿两条入口一起读

当前接收主线先经过 SRTP 解除保护和 RTP 分流,再进入媒体接收通道与 Call::DeliverRtpPacket()。AudioReceiveStreamImpl 将自己的 ChannelReceive 注册为对应的接收对象,实际报文由 ChannelReceive 接收处理,再把负载交给 NetEq。

播放则从另一个入口开始:

播放入口接下来的关系
AudioTransportImpl::NeedMorePlayData()请求 Mixer 生成输出
AudioMixerImpl::Mix()向音频源拉取 PCM
AudioReceiveStreamImpl::GetAudioFrameWithInfo()转给对应 ChannelReceive
ChannelReceive::GetAudioFrameWithInfo()从 NetEq 取得音频,并做相应流级适配
NetEq::GetAudio()提供当前需要的 10 ms PCM
PCM 返回 Mixer 之后混音、APM 播放参考、设备输出适配

本文版本中,ChannelReceive 已直接持有并调用 NetEq。旧资料中的 ACMReceiver 不能继续作为这份源码里的必经层。

同样,模块职责也不等于操作系统线程划分。当前部分 network 与 worker 检查对应同一任务队列;有些计算又通过独立任务队列执行。需要看实际任务投递和序列检查,而不是为图中的每个框机械地创建一个线程。

10.4 第一轮阅读,可以从这些检索开始

在对应提交的源码根目录执行:

rg -n 'RegisterAudioCallback|CreateAudioState|CreateDefaultAudioOptions' \
  media/engine/webrtc_voice_engine.cc

rg -n 'RecordedDataIsAvailable|ProcessCaptureFrame|SendProcessedData|NeedMorePlayData|ProcessReverseAudioFrame' \
  audio/audio_transport_impl.cc

rg -n 'ProcessAndEncodeAudio|Add10MsData|SendData|SendRtpAudio' \
  audio/channel_send.cc

rg -n 'OnRtpPacket|OnReceivedPayloadData|InsertPacket|GetAudioFrameWithInfo|GetAudio' \
  audio/channel_receive.cc

rg -n 'GetAudioInternal|Decode|Accelerate|PreemptiveExpand|Expand' \
  modules/audio_coding/neteq/neteq_impl.cc

第一轮只跟 PCM 在哪里进入、编码包在哪里产生、网络负载在哪里插入、播放 PCM 在哪里取出。等这些入口能对应起来,再深入 AEC3、Opus 自适应和 NetEq 决策细节,会更容易判断每个算法解决的具体问题。

11. 怎样观察运行状态,避免只凭“能听见”判断

11.1 保留四类观察点

观察位置建议记录能帮助区分什么
设备采集入口格式、每声道样本数、回调间隔、电平与溢出没采到声音、格式错误、供数不稳
APM 与编码前后处理前后电平、处理耗时、编码包时长、输出数量处理抑制异常、组包等待、编码队列积压
RTP 与接收恢复收发包、丢失与迟到、恢复情况、NetEq 输出统计网络缺失与播放补偿之间的关系
设备播放出口请求节拍、输出电平、排队长度、欠载已有解码声音但设备没播出,或本机供数超时

诊断 PCM 可以帮助比较处理前后的变化,但应通过受控、非阻塞的方式采样,避免让诊断动作本身破坏音频回调节拍。

11.2 统计字段先看定义,再计算

浏览器统计里的 jitterBufferDelay 是以秒为单位的累计量,不是“此刻缓冲了多少秒”。对同一个接收统计对象,两个时间点之间的平均缓冲停留时间可以按下面方式计算:

区间平均缓冲停留时间(ms)
= (jitterBufferDelay(t2) - jitterBufferDelay(t1))
  / (jitterBufferEmittedCount(t2) - jitterBufferEmittedCount(t1))
  × 1000

这个计算要求分母大于零、统计对象相同,而且计数没有重置。音频中的输出计数按采样计,不是 RTP 包数。

concealedSamples 反映本地合成替代的样本,既可能与丢包有关,也可能与包太晚到达有关。它可以描述播放恢复的负担,但不能直接改名叫网络丢包率。

currentRoundTripTime 则是所选 ICE 候选对的检查往返时间。它没有包含完整的采集、编码和扬声器输出过程。浏览器字段使用驼峰命名,Native 统计成员还需要对照对应 C++ 定义。

11.3 典型现象要沿数据位置分段排查

现象优先核对的位置
网络连接正常,远端始终静音本机采集是否有有效 PCM,媒体发送方向与发送流是否工作,远端是否取到并播放 PCM
本地录音清楚,发送后声音很小或断字APM 配置、采集增益、VAD/DTX 相关策略及处理前后信号
自己能听到自己的延迟回声哪一端的扬声器声音回到其麦克风,参考内容是否完整,设备延迟与非线性是否异常
网络统计平稳,但仍有爆音或断续设备欠载/溢出、线程调度、样本格式及回调连续性
通话越久延迟越大实际队列占用、采样与播放速率、任务耗时和恢复后的旧数据积压
一端能听见,另一端没有声音分别检查反向采集、媒体方向、收包恢复和输出,不能只复用正向结论

这些是排查入口,不是仅凭现象就确定唯一原因。例如断字既可能来自错误的语音门限,也可能来自音频处理失配或接收恢复,最终还要回到对应位置的信号和时间记录。

12. 先建立一条可验证的音频闭环

总体架构理解之后,可以按下面的顺序接入实际设备。每一步都保留能说明结果的证据,再继续增加变量。

阶段做什么验证目标
本地设备单独采集并回放已知格式的 PCM,检查硬件路由格式、电平、采样时长和播放节拍正确
单向媒体A 采集并发送,B 接收并播放;明确实际配置PCM、编码包、RTP 和播放输出能逐段对应
反向媒体将相同检查反过来完成 B 到 A两个方向独立工作
双向连续运行同时发送和接收,先用耳机或受控声学环境区分问题双向调度稳定,没有持续积压
扬声器全双工使用实际扬声器,加入近端、远端分别讲话和同时讲话AEC 参考、双讲表现与音量变化得到验证
网络与负载变化受控加入抖动、丢包、CPU 负载或设备重启能解释恢复方式、状态变化和延迟是否回落

第一阶段也要注意避免扬声器回放直接造成声学反馈;使用耳机、隔离设备或受控音量更容易把格式问题与回声问题分开。

测量口到耳延迟时,应建立统一或经过校准的时间基准。例如,用同一采集设备记录发送侧的测试声音与接收扬声器输出,再根据波形对应关系估算时差,同时考虑采集位置和声传播影响。两个未同步终端各打一条系统时间日志,直接相减,不能得到可靠的单程声音延迟。

这些是实验方案和检查顺序,不是本文已经在某块板卡完成的性能测量。本篇也不提供一个脱离设备与网络条件的固定延迟承诺。

完成这条闭环后,再单独更换音频设备后端、调整 APM 或施加弱网条件,就能观察到明确的变化:声音在什么位置改变,哪一级开始等待,哪一种恢复机制实际参与了播放。后续的算法分析由此有了可重复验证的运行基础。

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

赞 (0)

相关推荐