麦克风把声音变成 PCM,扬声器把 PCM 变回声音。对一个实时通话系统来说,这两句话之间还有很多事情:选择设备、协商格式、启动音频流、适应设备回调的节奏,以及在耳机拔出、来电中断后恢复工作。
WebRTC 把这些工作集中在 Audio Device Module,简称 ADM。理解它,最好先建立整体位置,再沿着音频数据走一遍,最后进入代码和平台实现。否则很容易把“设备缓冲”“网络抖动缓冲”和“回声消除”混成一件事。
本文以本地
webrtc_src的提交3dec7d05377fb690e84f503db865edde982ab2a5为源码基线,提交日期为 2026-10-23。正文主要讨论使用内置设备模块的 native 音频路径;浏览器或业务方可以注入自己的 ADM,复用这里的接口而采用不同的数据通路。文中的源码链接固定到该提交,代码片段会标明是否经过简化。
一、整体视角:ADM 是 WebRTC 与操作系统音频 I/O 的边界
1.1 它在整个系统的什么位置
可以把 WebRTC 的音频部分分成三个层次:最下面连接操作系统和设备,中间处理 PCM,上面处理编码后的数据和网络传输。
这张图表达的是职责边界,不代表所有对象都按这个顺序相互持有。
从上向下看,ADM 提供“准备并启停输入输出、获取和交付 PCM”的能力;从下向上看,它把不同系统的音频 API 适配成 WebRTC 能使用的接口。上层不必分别理解 WASAPI、CoreAudio、AudioRecord 或 AudioUnit。
1.2 ADM 管理哪些事情
| 职责 | 解决的问题 | 典型例子 |
|---|---|---|
| 设备与能力管理 | 用哪个输入输出,支持什么能力 | 枚举设备、选择默认设备、查询立体声和音量能力 |
| 生命周期管理 | 什么时候准备、开始、停止 I/O | 初始化模块,准备录音流,启动采集,停止播放 |
| PCM 交接与适配 | 如何把系统缓冲交给 WebRTC | 格式转换、缓冲拼接、交付采集 PCM、请求播放 PCM |
| 设备侧时序与状态 | 音频在设备侧停留多久,流是否正常 | 播放/采集延迟、可选采集时间戳、欠载或中断处理 |
这里的“ADM”有两种常见含义:狭义上指公共的 AudioDeviceModule 接口;广义上指接口实现、平台后端和缓冲适配组成的整个设备模块。后面说“ADM 适配设备回调”时,通常采用广义含义。
ADM 交付的是 PCM。降噪、回声消除等算法主要属于 APM(Audio Processing Module);网络包的乱序、抖动与丢包补偿属于接收侧 NetEq;编码器负责把 PCM 压缩成待发送的音频。某些平台后端可以启用系统语音处理,但这不改变这些职责边界。
二、数据流视角:一条向上推,一条向下拉
这一节暂时忽略平台差异,只看各平台共同需要完成的事情。
2.1 采集数据:由麦克风驱动,向发送链路推送
用户说话时,操作系统不断提供新采集的音频。设备模块整理这些数据,向上层报告“这一段录音已经可用”,上层再进行处理和发送。
这是一种 push 模型:采集端产生数据,主动交付给上层。设备不会等编码器询问“你有没有新录音”。
在本文的默认路径中,同一份采集数据先经过公共 APM 处理,再向已注册的发送流分发;各发送流拥有自己的编码与发送过程。因此,“有多个发送流”不意味着每个流都重新打开一个麦克风。
2.2 播放数据:由设备需求驱动,向接收链路拉取
播放的起点容易理解反了:网络包到达时,并不直接写进扬声器。网络到包是有抖动的,而扬声器需要按设备时钟持续消费音频。
接收链路先把到来的压缩音频放入 NetEq;当设备需要下一段播放数据时,再由播放调用链拉取音频、完成接收处理并混音。
实线表示数据的交付方向,虚线表示播放需求的传播方向。播放数据向设备流动,播放请求向接收链路传播。
这里的 pull 模型 是逻辑关系:有的平台由系统回调直接触发拉取,有的平台由后端工作线程提前填充本地缓冲。共同点是播放节奏最终受设备消费速度约束,而不是受网络到包时刻支配。
2.3 为什么采集与播放还会在 APM 相遇
假设扬声器播放远端声音,麦克风又把它录了进去。若原样发送,远端就会听到自己的回声。AEC(Acoustic Echo Cancellation,声学回声消除)需要同时观察两类信号:
- 采集信号:麦克风录下的近端语音、噪声和回声。
- 播放参考信号:本机准备播放的远端音频。
因此,播放链路除了把音频送到设备,还会把混音结果送给 APM 的 reverse stream。采集侧再把设备侧延迟传给 APM,辅助对齐参考与录音。
这里的参考是设备输出之前的数字音频,并非从扬声器重新录回来的声音;ADM 提供的延迟也只是播放和采集的设备侧估计,不包含全部网络、NetEq 和编解码延迟。
2.4 共同的数据契约:16-bit PCM 与 10 ms
在本文所讨论的 AudioDeviceBuffer → AudioTransportImpl 路径中,上下层主要交换 16-bit、交错存储的 PCM,每次处理 10 ms。交错双声道的排列是 L0, R0, L1, R1, ...。
以 48 kHz 为例:
| 概念 | 单声道的 10 ms | 双声道的 10 ms |
|---|---|---|
| 每声道样本数 / PCM frame 数 | 480 | 480 |
| 所有声道合计的标量样本数 | 480 | 960 |
| 数据字节数,S16 | 960 | 1920 |
这里一个 PCM frame 包含同一时刻所有声道的样本;它与源码中表示一整段音频的 AudioFrame 对象是两个概念。换成 16 kHz,10 ms 就是每声道 160 个样本,因此 10 ms 也不等于固定的 480 个样本。
10 ms 是逻辑处理粒度,设备回调周期和编码包时长可以不同。 设备可能每次给 128、192 或其他数量的 PCM frame;编码器也可能积累两个 10 ms 输入,才产出一个 20 ms 的编码包。
以“48 kHz、单声道、每次设备回调 192 frame”为例,采集适配的过程可以是:
播放侧执行相反操作:先按 10 ms 向上层取数,再按设备本次需要的大小切出,保留余量。FineAudioBuffer 就是完成这种块长适配的工具之一;另一些后端自己实现累计、拆分或环形缓冲。
三、源码视角:对象如何连接,方法如何调用
3.1 先分清六个核心角色
| 对象 / 接口 | 主要责任 | 需要避免的误解 |
|---|---|---|
AudioDeviceModule | 面向上层的设备公共接口 | 它不等于某一个平台实现类 |
| 平台后端 | 调用 OS API、操作设备流、完成本地缓冲适配 | 各平台的线程和缓冲结构可以不同 |
AudioDeviceBuffer,ADB | 保存 PCM、采样率、声道、延迟等,调用运输回调 | 它不会自动把任意输入拼成 10 ms |
FineAudioBuffer | 在任意设备块长与 10 ms 之间拆分、拼接 | 它既不做网络抖动控制,也不负责重采样 |
AudioTransport / AudioTransportImpl | 前者定义双向 PCM 回调,后者连接 APM、发送流和混音器 | 这里的 Transport 不是网络传输 |
AudioState | 协调 ADM、APM、混音器和收发流的公共状态 | 音频 PCM 回调实际由其内部的 AudioTransportImpl 承接 |
以通用 AudioDeviceModuleImpl 路径为例,对象关系如下。节点中合并标出接口与实现,箭头表示持有或调用关系,不表示单向音频流。
这张图只展开一种 ADM 的内部组织。Android 的组合式 ADM、iOS SDK 的 AudioDeviceModuleIOS、Windows 的另一套 Core Audio ADM,都可以直接实现同一个公共接口,不需要继承 AudioDeviceModuleImpl。
对应源码:ADM 公共接口、通用 ADM 实现、AudioState。
3.2 建立连接:后端接入 ADB,再注册运输回调
第一处连接发生在设备模块内部。通用工厂创建后端后,调用:
| |
这让平台后端知道应该向哪个 ADB 交付录音、从哪个 ADB 获取播放数据。
第二处连接发生在设备模块与音频引擎之间。WebRtcVoiceEngine::Init() 完成设备初始化和选项配置后,执行:
| |
audio_transport() 返回内部 AudioTransportImpl 的接口指针;通用 ADM 再把这个指针交给 AudioDeviceBuffer::RegisterAudioCallback()。至此,后端 ↔ ADB ↔ AudioTransportImpl 才连通。
AudioTransport 是上层实现、下层调用的接口。尤其要注意:NeedMorePlayData() 虽然是“回调”,行为却是同步请求,上层要在返回前填好输出 PCM。
初始化路径见 WebRtcVoiceEngine::Init 和 adm_helpers::Init。允许内部设备的构建中,未注入 ADM 时会尝试创建平台默认实现;调用方也可以提供自己的 ADM。
3.3 控制调用链:初始化与真正运行分开
Init()、InitMicrophone() 和 StartRecording() 不是同一件事。公共接口把模块初始化、设备控制能力准备、I/O 流准备与运行分开;各平台实际创建资源的时机可能有所不同。
| 阶段 | 录音侧的代表方法 | 播放侧的代表方法 |
|---|---|---|
| 模块初始化 | Init() | 同一个 Init() |
| 选择设备、准备控制能力 | SetRecordingDevice()、InitMicrophone() | SetPlayoutDevice()、InitSpeaker() |
| 准备 I/O 流 | InitRecording() | InitPlayout() |
| 开始 / 停止数据流 | StartRecording() / StopRecording() | StartPlayout() / StopPlayout() |
在 AudioState 中,运行还与收发流是否存在、录音/播放开关是否启用有关:
发送流注册时,AudioState::UpdateAudioTransportWithSendingStreams() 还会更新 AudioTransportImpl 中的发送者列表及所需采样率、声道数;接收流注册时,则通过 AudioMixer::AddSource(stream->source()) 加入混音器。生命周期控制和 PCM 流动由此衔接起来。
另一个细节是:当接收流仍存在、只是 AudioState 的 playout 开关被关闭时,UpdateNullAudioPollerState() 会启用 10 ms 定时拉取,让接收音频继续被消费。这和把接收流自身停止是不同的操作,不能把所有“静音”都理解成同一种状态。
3.4 采集调用链上半段:后端 → ADB → APM
无论系统通过回调、事件还是读线程提供录音,后端最终需要把合适大小的 PCM 交给 ADB。以下为调用顺序示意,块长适配已由后端或 FineAudioBuffer 完成:
| |
这三步的作用分别是:复制 PCM 到 rec_buffer_;保存播放与采集延迟;通过已注册的 AudioTransport 把 PCM 和元数据交给上层。SetRecordedBuffer() 本身只保存本次给它的数据,不负责累计不足 10 ms 的输入。
DeliverRecordedData() 的关键调用可概括为:
| |
接口中的参数名 nBytesPerSample 容易误导:在这条 ADB 调用路径里,传入的是每个 PCM frame 的字节数,即 声道数 × sizeof(int16_t)。双声道为 4,不能直接按“一个标量样本 2 字节”去填。AudioTransportImpl 中也有相应的检查。
进入 AudioTransportImpl::RecordedDataIsAvailable() 后,主要顺序是:
- 检查格式和长度,包括
number_of_frames * 100 == sample_rate,明确要求本次为 10 ms。 InitializeCaptureFrame()选择处理格式,voe::RemixAndResample()调整声道与采样率。ProcessCaptureFrame()传入设备延迟和按键状态,并调用ProcessAudioFrame()。ProcessAudioFrame()转到AudioProcessing::ProcessStream(),执行已配置的采集处理。- 设置可选的绝对采集时间,之后直接或经可选异步处理器进入
SendProcessedData()。
如果后端提供采集时间戳,ADB 会利用 TimestampAligner 将它映射到 WebRTC 使用的时钟域;没有时间戳时,接口允许传入 std::nullopt。不要因为参数中还有 clockDrift、currentMicLevel 等历史字段,就假定当前 ADB 会提供有效数值:上面的实现对它们传入的是 0。
实现见 AudioDeviceBuffer、AudioTransportImpl 和 APM 的 AudioFrame 适配函数。
3.5 采集调用链下半段:发送流 → 编码队列 → RTP
AudioTransportImpl::SendProcessedData() 遍历已注册的 AudioSender,调用各发送流的 SendAudioData()。多个发送流需要数据时,为其中的流复制 AudioFrame,最后把原始对象移动给一个流。
图中 Transport 指 AudioTransportImpl,“发送链路”合并表示 AudioSendStream 和 ChannelSend。源码中的主链可按下面的顺序阅读:
| |
明确的线程交接发生在 ChannelSend 投递编码任务时。 默认同步路径中的格式调整、APM 以及发送流分发,会先在调用采集交付函数的线程上执行;启用额外异步处理时,中间还会增加交接点。
另外,Add10MsData() 每次接受 10 ms,并不保证每次都回调 SendData()。编码器是否已经攒够一个包、是否启用 DTX 等都会影响输出。把设备帧、APM 处理帧和 RTP 包时长分开看,才能正确分析延迟。
对应源码:AudioSendStream、ChannelSend、AudioCodingModuleImpl。
3.6 播放调用链:设备请求 → 混音器 → 接收流 → NetEq
先看网络到包这一侧。ChannelReceive::OnRtpPacket() 解析和处理 RTP,在接收流处于播放状态时,经过 OnReceivedPayloadData() 把载荷插入 NetEq::InsertPacket()。这一步提供的是压缩音频,不会直接触发设备播放。
真正消费音频的入口在设备侧。后端执行:
| |
RequestPlayoutData() 同步调用 AudioTransport::NeedMorePlayData()。进入 AudioTransportImpl 后,它先调用混音器;混音器逐个向接收流请求音频;各流最终向自己的 NetEq 取出下一段 10 ms PCM。
图中 Transport 指 AudioTransportImpl,Mixer 指 AudioMixer,“接收链路”合并表示接收流和 ChannelReceive。展开方法名之后,主链是:
| |
这里有三个关键点:
- NetEq 是由播放需求拉动的。 它结合已有包和当前状态生成音频,可能解码新包、使用已解码的缓存,也可能执行丢包补偿;不是每次
GetAudio()都恰好解码一个网络包。 - 混音之后才进入 APM 播放参考路径。 AEC 需要观察本机准备输出的合成声音,而不只是某一条远端流。
- 重采样可能不止一次。 接收流先适配混音器要求的格式,混音结果再适配设备要求的采样率;设备侧仍可能存在原生格式转换。
对应源码:混音器、AudioReceiveStreamImpl、ChannelReceive。
AudioTransport 还定义了 PullRenderData()。本基线中的注释说明,Chromium 使用这个入口并拥有自己的 APM,因此该方法没有在内部调用 ProcessReverseStream();采集侧 RecordedDataIsAvailable() 的默认处理路径也注明不用于 Chromium。分析 Chrome 时,应继续追踪浏览器自己的采集与处理装配,不能把上面的 native 调用链直接套过去。
3.7 ADB、FineAudioBuffer 与 NetEq:三种缓冲分别解决什么
| 组件 | 缓冲的是什么 | 面对哪种差异 | 典型行为 |
|---|---|---|---|
AudioDeviceBuffer | 本次采集 / 播放的 PCM | 后端与运输回调的交接 | 保存 PCM、格式和元数据,调用双向接口 |
FineAudioBuffer | 设备块与 10 ms 块之间的 PCM 余量 | 本地设备回调块长不一致 | 采集攒够 10 ms 才交付,播放取 10 ms 后按需切出 |
NetEq | 接收包及解码、时伸缩等所需的音频状态 | 网络到包与稳定播放之间的时间差 | 抖动控制、按需解码、丢包补偿 |
FineAudioBuffer::DeliverRecordedData() 中的循环体现了这个区别:追加本次设备数据,只要缓存达到一个 10 ms 块,就调用 ADB 的 SetRecordedBuffer()、SetVQEData()、DeliverRecordedData(),剩余数据保留到下次。
反向的 GetPlayoutData() 则不断请求 10 ms,直到足够满足设备这次的需求,然后复制所需部分并保留余量。它会缓存后端传入的播放延迟,供后续采集交付时一起上报。完整实现见 fine_audio_buffer.cc。
四、平台视角:共用 PCM 边界,底层采用不同的节奏
4.1 差异集中在哪几层
上层的 AudioTransport 契约可以复用,但设备 API、后端组织、回调线程、设备路由和系统音效能力都存在差异。
| 平台 / 路径 | 系统 I/O 方式 | 块长适配的主要位置 | 设备选择 / 路由 |
|---|---|---|---|
| Windows 传统 Core Audio 后端 | WASAPI 共享模式,事件唤醒采集/播放线程 | 后端的同步缓冲与 10 ms 循环 | endpoint 枚举、设备 ID、默认设备角色 |
| macOS | CoreAudio HAL 的 IOProc | 环形缓冲、工作线程与 AudioConverter | HAL 设备及属性监听 |
| Linux PulseAudio | 服务端音频流、threaded mainloop 与后端线程 | 后端按 10 ms 整理,与服务端缓冲衔接 | Pulse server 的 source / sink |
| Linux ALSA | PCM 读写与等待 | 后端读写线程、本地缓冲 | ALSA PCM 设备 |
| Android Java | AudioRecord / AudioTrack 高优先级线程 | Java/JNI 按 10 ms 交接 | Android 系统与应用的路由管理 |
| Android OpenSL ES / AAudio | buffer queue / data callback | FineAudioBuffer | 受系统路由和具体后端能力影响 |
| iOS | VoiceProcessingIO AudioUnit 回调 | FineAudioBuffer | AVAudioSession |
以下“默认”都指这份源码及对应构建条件下的选择。SDK 的创建入口、应用注入的 ADM 和构建开关,都会影响真正使用的后端。
4.2 Windows:WASAPI 事件驱动,注意两套 ADM 入口
通用 AudioDeviceModuleImpl 的 Windows 默认路径使用 AudioDeviceWindowsCore。它主要通过以下对象工作:
IMMDevice等接口枚举和取得输入/输出 endpoint。IAudioClient协商格式并创建共享模式流。IAudioCaptureClient读取采集包,IAudioRenderClient写入输出缓冲。- 采集与播放各有事件等待和工作线程,并使用 MMCSS 改善调度。
播放线程的主过程是等待事件,通过 GetCurrentPadding() 确定输出缓冲还能容纳多少 frame,再按 10 ms 向 ADB 请求 PCM,最后提交到 WASAPI。采集线程从 GetBuffer() 取得数据,放入同步缓冲,凑足 10 ms 后交给 ADB。
因此,即使一次系统事件对应的可读写数据不是 10 ms 的整倍数,上层仍可按 10 ms 工作。格式也由设备探测与协商确定,不能假定任何端点都接受同一个采样率和声道组合。
本基线还保留另一套 kWindowsCoreAudio2 路径,通过 CoreAudioInput / CoreAudioOutput 组合成 ADM。它需要调用专用工厂 CreateWindowsCoreAudioAudioDeviceModule();直接把这个枚举值传给通用 ADM 工厂会被拒绝。专用入口要求正确初始化 COM MTA,并提供 automatic_restart 参数控制断连后的自动重启。
源码入口:audio_device_core_win.cc、专用工厂与约束、组合式 Windows ADM。
4.3 macOS:IOProc 与 WebRTC 处理之间隔着环形缓冲
AudioDeviceMac 直接使用 CoreAudio HAL。系统在 IOProc 中提供输入、请求输出,而 WebRTC 上层的调用主要发生在后端工作线程中。环形缓冲把两边连接起来。
这样做可以把上层处理与 IOProc 的实时节奏隔开。播放 worker 观察环形缓冲的可写空间,持续补充音频;采集 worker 从输入缓冲取数并做格式转换,再交付 ADB。它增加了本地缓冲,也需要在延迟估计中考虑这些缓冲。
当前后端把 ADB 的默认格式设为采集 48 kHz 单声道、播放 48 kHz 双声道,并结合设备能力和配置调整声道;设备原生格式与内部格式之间由 AudioConverter 等逻辑适配。后端还监听设备、格式等属性变化。
macOS 和 iOS 都属于 Apple 平台,但这份 macOS 实现采用 HAL、IOProc 与 worker 的组织方式,不能照搬下一节 iOS 的 AudioUnit 模型。源码见 AudioDeviceMac。
4.4 Linux:PulseAudio 服务端路径与 ALSA PCM 路径
PulseAudio 路径。 当构建启用 WEBRTC_ENABLE_LINUX_PULSE 时,通用 ADM 的 kPlatformDefaultAudio 选择 AudioDeviceLinuxPulse。后端加载 libpulse,创建 pa_threaded_mainloop、pa_context,连接服务端,再建立采集与播放的 pa_stream。读写回调与后端音频线程协作,完成本地 10 ms PCM 和服务端缓冲之间的交换。
设备列表与默认 source / sink 来自服务端,因此系统混音和路由也是时序与设备行为的一部分。
ALSA 路径。 未启用 PulseAudio 时,平台默认走 ALSA;启用 PulseAudio 的构建也可显式选择 kLinuxAlsaAudio。AudioDeviceLinuxALSA 通过 snd_pcm_readi()、snd_pcm_writei() 读写 PCM,配合 snd_pcm_wait() 等待,以及 snd_pcm_delay() 获取延迟信息。
两条路径都要处理“暂时没有足够数据”和异常恢复。ALSA 的 ErrorRecovery() 会针对 underrun 等错误恢复 PCM 状态;PulseAudio 后端通过服务端流状态和缓冲回调协调。它们共同向 ADB 提供 10 ms 音频,但缓冲层次、设备命名和路由来源不同。
源码见 PulseAudio 后端、ALSA 后端。这里列的是 WebRTC 后端使用的 API,不能仅凭桌面系统采用了什么音频服务,就推断源码中存在同名的直接后端。
4.5 Android:输入与输出可以采用不同 API
Android 需要先问清楚“通过哪个工厂、选择哪种 AudioLayer 创建 ADM”。native API 的 CreateAndroidAudioDeviceModule() 支持这些组合:
| AudioLayer | 采集端 | 播放端 | 设计考虑 |
|---|---|---|---|
kAndroidJavaAudio | AudioRecord | AudioTrack | Java API 路径 |
kAndroidOpenSLESAudio | OpenSL ES | OpenSL ES | native buffer queue 路径 |
kAndroidJavaInputAndOpenSLESOutputAudio | AudioRecord | OpenSL ES | Java 输入音效能力与 native 输出结合 |
kAndroidAAudioAudio | AAudio | AAudio | AAudio data callback 路径,需构建支持 |
kAndroidJavaInputAndAAudioOutputAudio | AudioRecord | AAudio | Java 输入与 AAudio 输出组合,需构建支持 |
在这个工厂中,请求 kPlatformDefaultAudio 时,若定义了 WEBRTC_AUDIO_DEVICE_INCLUDE_ANDROID_AAUDIO,选择 AAudio 双向路径;否则根据系统报告的低延迟输入/输出能力,在 OpenSL ES、混合路径和 Java 路径之间选择。这是该工厂的选择逻辑,不代表所有 Android SDK 接入都采用同样的默认值。
Java 路径的数据交接很适合作为一个具体例子:
| |
direct ByteBuffer 让 JNI 可以访问已缓存的内存地址,减少 Java 与 C++ 交接的额外复制;不过 ADB 自身的保存和取数仍有复制,不能把整条链路理解为零拷贝。
OpenSL ES 使用缓冲队列回调,AAudio 使用带 num_frames 的 data callback。它们通过 FineAudioBuffer 衔接设备块长与 10 ms。AAudio 播放端还会观察 xrun:欠载次数增加时,扩大一个 burst 的输出缓冲,以延迟换稳定性;断连时把重启工作投递到另一线程,而不是在错误回调中直接关闭、重开流。
另一项差异是设备管理:这份组合式 Android ADM 的枚举接口返回一个逻辑输入和一个逻辑输出,不能据此得到全部手机麦克风、蓝牙和扬声器列表。路由需要结合 Android 系统音频管理与应用配置处理。Java 输入还可能使用系统 AEC/NS/AGC,启用时应与 APM 选项协调。
源码入口:Android 工厂与选择逻辑、AndroidAudioDeviceModule、AudioRecordJni、AudioTrackJni、AAudioPlayer。
4.6 iOS:音频会话决定环境,一个 AudioUnit 承担双向 I/O
iOS 的核心设备实现是 AudioDeviceIOS,SDK 可以通过 AudioDeviceModuleIOS 包装它。控制环境主要来自 AVAudioSession,实际 I/O 使用 Voice-Processing I/O AudioUnit。
它与前面的独立输入/输出线程模型有一个明显区别:同一个全双工 AudioUnit 组织录音与播放,系统语音处理可以同时观察两侧信号。默认创建路径不旁路 voice processing;SDK 也提供 bypass_voice_processing 配置,因此应以实际创建选项判断系统处理是否启用。
初始化时要区分偏好参数与真实参数:Init() 先保存偏好采样率和声道;真正准备 I/O、激活 session 后,SetupAudioBuffersForActiveAudioSession() 再读取实际 sampleRate 和 IOBufferDuration,更新 ADB,并重建 FineAudioBuffer。
具体的采集入口是 AudioDeviceIOS::OnDeliverRecordedData():它调用 AudioUnit 的 render 取得输入,再通过 FineAudioBuffer::DeliverRecordedData() 累积交付。播放入口是 OnGetPlayoutData():它通过 FineAudioBuffer::GetPlayoutData() 取得当前回调需要的 PCM,不播放时填零。
耳机路由变化、电话中断和采样率变化,都可能要求重新配置。后端把相关事件转交到拥有对象的线程,在需要时停止 AudioUnit、反初始化、读取新的 session 参数、重建缓冲并恢复运行。这里的设备选择主要属于 session 路由管理,不能套用桌面 ADM 的 index/GUID 操作方式。
源码入口:iOS SDK 工厂、AudioDeviceIOS、VoiceProcessingAudioUnit。
4.7 哪些能力不能假定跨平台一致
| 问题 | 共同接口背后的实际差异 |
|---|---|
| 能否枚举并选择任意设备? | 桌面后端一般支持设备枚举;移动端更多依赖系统路由,部分 ADM 方法只提供逻辑设备或未实现 |
| 能否设置音量、静音、立体声? | 要查询相应 IsAvailable 能力并检查返回值,不能由接口存在推断设备支持 |
| 是否启用系统 AEC? | 取决于平台、后端与创建配置;Android Java 输入和 iOS voice processing 需要与软件 APM 配合 |
| 延迟是不是精确测量? | 后端可能使用系统时间戳、缓冲查询或固定估计,精度不同 |
| 换设备能否自动恢复? | 取决于后端、选项和应用策略,公共接口并不保证所有平台自动恢复 |
例如,ALSA 可以从 snd_pcm_delay() 换算延迟;AAudio 可以根据流的时间与帧位置估计;iOS 的 PlayoutDelay() 在此基线中返回固定估计。这些值可以辅助音频处理和诊断,但不能当成同等精度的端到端延迟测量。
真实设备之外还可以使用 dummy、文件或测试 ADM。它们证明了这个边界的可替换性:上层需要的是符合契约的音频源和输出端,不一定是 WebRTC 亲自打开的物理麦克风与扬声器。
五、带着问题回到源码
5.1 建议的阅读顺序
| 要回答的问题 | 优先阅读的文件与方法 |
|---|---|
| 上层能要求设备做什么? | api/audio/audio_device.h、audio_device_defines.h |
| 设备与上层在哪里接通? | webrtc_voice_engine.cc 的 Init(),ADM 的 RegisterAudioCallback() |
| 谁控制什么时候开麦、播放? | audio/audio_state.cc 的 Add*Stream()、SetRecording()、SetPlayout() |
| PCM 怎样过设备边界? | audio_device_buffer.cc 的两组交付/请求方法 |
| 为什么上层都是 10 ms? | fine_audio_buffer.cc 与各后端的缓冲循环 |
| APM 和收发流在哪里连接? | audio/audio_transport_impl.cc 的两个主回调 |
| 编码在哪里切换线程? | audio/channel_send.cc 的 ProcessAndEncodeAudio() |
| 播放怎样到达 NetEq? | audio_mixer_impl.cc、audio_receive_stream.cc、channel_receive.cc |
| 某个平台为什么有额外延迟或重启? | 实际选中的平台后端、工厂和构建开关 |
5.2 用数据断点定位“有设备,但没有声音”
比起一开始钻进驱动,更有效的办法是沿公共边界逐段检查:
- 录音没有进入发送链路:先确认录音流运行且有非零 PCM,再看
DeliverRecordedData()是否调用到正确的RecordedDataIsAvailable(),随后检查 APM 输出、发送者列表与编码队列状态。 - 网络有包但扬声器没声音:先确认接收流已进入播放状态且载荷插入 NetEq,再看设备侧是否持续请求、混音器是否注册了接收源、
NeedMorePlayData()是否返回有效 PCM。 - 只有某个平台周期性卡音:同时观察设备回调间隔、处理耗时、缓冲余量和 underrun/xrun;10 ms 逻辑帧并不保证操作系统线程每 10 ms 准时运行。
- 切换耳机后失声或变速:检查实际路由、采样率、声道和设备块长是否变化,ADB 参数及块长适配器是否随流重建而更新。
- 声音正常但回声明显:检查播放参考是否进入对应的 APM、设备延迟是否合理,以及系统语音处理与软件 APM 的启用配置是否匹配。
这些检查贯穿全文的四个层次:先确认 ADM 是否接入并运行,再确认数据流是否连续,然后定位到具体的方法与线程交接,最后分析平台设备与路由行为。这样读代码,平台细节才有清晰的落点。
