来源:飞一样的成长
原文: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 变成设备需要的输出 | 按播放节奏供数,适配输出格式,最终推动扬声器 |

有两个分界很重要:编码器之前主要处理 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 必须发送一个音频包”。

硬件可能每次交出更小或更大的缓冲。适配层需要先积累、拆分或缓存,再以内部接口需要的大小交接。编码器也可能连续接收几个 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 先进行适合后续处理的声道和采样率适配,再进入配置好的采集处理路径,最后把处理结果交给发送流。
如果配置了异步音频处理,结果还可能经过异步处理完成回调后再进入发送。因此图中的“处理后交给发送流”表达的是数据依赖,不能理解为所有工作都必然在一个函数栈中一次做完。

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

参考信号提供了已知播放内容,算法据此估计它经过扬声器、房间和麦克风以后会怎样出现在采集信号中。这个关系会受摆放、音量、设备延迟和非线性失真影响,不是把两段 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。

内部大致需要协调四件事:
| 工作 | 为什么需要 |
|---|---|
| 管理包、时间戳与冗余信息 | 按媒体时间关系使用数据,处理乱序、重复和可用冗余 |
| 估计合适的缓冲目标 | 为网络波动留余量,同时限制额外等待 |
| 调用解码器并管理已解码 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 对象已经创建”仍然不等于“对讲已经接好”。设备初始化、媒体流的创建与启停、回调注册和协商状态都需要衔接。

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 举报,一经查实,本站将立刻删除。