Chapter 108: Capture, Playback, Mixing, Resampling, Latency
手机音频系统要解决的问题是:把连续的物理声波变成应用可处理的数字缓冲区,再把应用产生的数字缓冲区稳定送回扬声器、耳机或蓝牙设备。读完本章后,读者应能追踪一次录音和播放请求,计算 PCM buffer 的时间长度,判断混音、重采样、buffer 调度和端到端延迟分别位于哪一层。
本章用一个实时语音 App 作为贯穿材料。这个 App 同时录制麦克风、播放远端语音、偶尔播放提示音,并允许用户切到蓝牙耳机。用户可见问题可能是听到爆音、录音断续、耳返延迟变长,或者切换设备后音色和延迟同时变化。这个现象表面是“声音不稳定”,系统路径里包含采集、播放、混音、格式转换、调度和路由策略。
本章的核心结论是:音频问题很少归属于单个 API。可复用的判断顺序是先确定数据方向,再检查 PCM 格式,再看 buffer 周期和 callback deadline,随后定位 mixer、resampler、HAL、driver、硬件和无线编码是否加入额外阶段。Android 的 AAudio 文档把低延迟音频描述为通过 stream 读写数据,并强调 callback、buffer 和性能模式对低延迟的影响;Apple 的 Core Audio 文档也把 iOS 音频放在低延迟、移动电源约束和系统托管行为中解释。这些公开材料支持本章采用“数据流 + buffer 时序 + 策略边界”的阅读方式:Android AAudio、Android Audio Latency、Apple Core Audio Overview、Apple Audio Session Programming Guide。
108.1 Audio Capture 与 Playback 的双向数据流
Audio Capture 指输入路径,系统把麦克风或外接输入设备采到的声音转换成应用可读的 buffer。Audio Playback 指输出路径,系统把应用写入的 buffer 送到扬声器、耳机或无线音频设备。两条路径方向相反,但都经过同一组基本角色:硬件设备、codec 或无线端点、driver / HAL、系统音频服务、framework API、应用 buffer 和调度线程。
采集路径的所有权从硬件开始。麦克风产生模拟信号,codec 中的 ADC 将它变成数字 PCM,driver 和 HAL 把硬件周期内产生的 frame 放入内核或用户态可见的队列,系统音频服务按权限、source、route 和前台状态决定哪些数据可交给应用。应用最终看到的是一次 read、一次 callback 参数,或一个由 framework 填充的 buffer。实时语音 App 的录音线程如果在 10 ms 周期里没有及时读取,输入侧 buffer 会继续累积,达到容量后新的采样数据会覆盖、丢弃或触发 overrun 计数。
播放路径的所有权从应用开始。应用生成远端语音解码后的 PCM,把数据写入播放 buffer。framework 把这段数据提交给系统音频服务,服务根据音量、音频焦点、session category、route、格式和设备状态决定它进入普通 mixer、低延迟路径、offload 路径或无线编码路径。HAL 和 driver 按硬件周期取走 frame,codec 中的 DAC 或蓝牙编码器把数字数据转换成用户能听到的声音。
下图把贯穿材料拆成输入和输出两条相反路径。图只表达运行期数据流,权限请求、音频焦点、session 激活和 route 变化属于系统策略层,会在每一次打开 stream、切换设备或恢复播放时重新参与决策。
这个路径说明了一个定位原则:录不到音先沿输入方向查,听不到声音先沿输出方向查,耳返延迟和实时通话要把输入、应用处理和输出合并成 round-trip path。声音从麦克风到应用需要权限和采集队列,声音从应用到扬声器需要播放队列和路由策略。两条路径共享设备状态、时钟、buffer 周期和功耗策略,所以一个 route change 可能同时改变采样率、声道数、buffer duration 和延迟。
108.2 PCM Frame、Sample Rate、Channel、Bit Depth
PCM 是移动音频路径里最常见的未压缩数字表示。它把连续声波按固定时间间隔采样,把每个采样点保存成数值。第一次分析音频 buffer 时,应先确定四个量:sample rate 表示每秒采样次数,channel 表示同一时刻有几路声音,bit depth 表示每个 sample 使用多少 bit,frame 表示同一时刻所有 channel 的 sample 组合。
frame 是系统调度音频最常用的时间单位。单声道 PCM 中,一个 frame 等于一个 sample;双声道 PCM 中,一个 frame 包含左声道 sample 和右声道 sample。Apple 的 Core Audio Overview 使用同样的区分:sample 是单个 channel 的一个数值,frame 是同一时间点上的一组 sample。这个定义能解释为什么音频 API 常使用 frame count 表示 buffer 长度,而字节数需要再乘以 channel 和 sample size。
计算 buffer 时间长度时,先看 frame count 和 sample rate。公式是 duration_ms = frame_count / sample_rate * 1000。在 48 kHz 设备上,480 frames 对应 10 ms;在 44.1 kHz 设备上,441 frames 对应 10 ms。实时语音 App 如果把 callback period 设成 10 ms,就意味着系统每 10 ms 交给它一批 frame,应用必须在下一个周期前处理完输入或填满输出。
计算 buffer 字节数时,使用 bytes = frame_count * channel_count * bit_depth / 8。48 kHz、stereo、16-bit 的 480 frames 占用 480 * 2 * 2 = 1920 bytes;16 kHz、mono、16-bit 的 160 frames 占用 320 bytes。这个换算帮助读者把 API 里的 bufferSizeInBytes、framesPerBuffer、preferredIOBufferDuration 和实际时间联系起来。
音频路径中的格式信息还有一个工程含义:所有相邻阶段都要对 frame 的解释达成一致。应用用 float 处理、HAL 要 int16,或者应用按 mono 写入、route 期望 stereo,系统就需要格式转换。格式转换可以正常工作,但它会增加处理阶段和调度成本。低延迟路径通常倾向使用设备原生 sample rate、原生 channel layout 和较少的格式转换,因为每一个额外阶段都占用 callback 周期的一部分。
108.3 Mixing:多 App 音频流的系统混音
Mixing 是系统把多个播放源合成到同一个输出设备的过程。手机上的输出设备数量有限,用户可能同时打开音乐、导航语音、通知铃声、游戏音效和通话。系统 mixer 的职责是把允许同时发声的 PCM 流按音量、增益、路由和时间对齐后合成到目标输出;策略层的职责是先决定哪些流参与本次混音。
实时语音 App 播放远端语音时,系统可能还要处理通知音。通知音进入 mixer 之前,音频焦点或 audio session 先判断它和通话、媒体、闹钟、导航播报之间的关系。Android 常用 usage、focus、stream / attributes 和 AudioPolicy 来表达播放意图;Apple 通过 AVAudioSession category、mode、option 和 active state 表达应用角色。两类平台都把“应用想发声”转成系统可仲裁的音频角色,再决定混音、duck、暂停、独占或改路由。
混音器处理的是已经对齐到同一输出时间线的 PCM frame。每条 track 会先经过音量、静音、左右平衡、可能的 effect chain 和格式适配。然后 mixer 按 frame 把各 track 的 sample 加权合成。合成结果超出表示范围时,系统可能使用 limiter、clamp 或内部浮点表示降低削波风险。这个阶段的核心风险是 CPU 周期和调度稳定性:track 越多、效果越多、格式越不一致,mixer 在一个周期内完成工作的压力越高。
多 App 音频问题要按“策略 → mixer → route”三层看。策略层回答“这条流是否可以发声”;mixer 回答“它如何和其它流合成”;route 回答“合成后的数据送到哪个设备”。如果用户说通知音把语音压低,责任多半在焦点或 session 策略;如果多个声音同时播放后出现爆音,责任更可能在增益、效果链、mixer 负载或输出 buffer;如果切到蓝牙后延迟变长,责任通常转向 route、codec 编码和无线链路。
| 判断维度 | 系统要做的事 | 可见结果 |
|---|---|---|
| 音频角色 | 把媒体、通话、闹钟、提示音、语音识别等意图映射成策略 | 允许混音、duck、暂停或独占 |
| 格式对齐 | 把不同 sample rate、channel、sample format 转成 mixer 工作格式 | CPU 开销增加,低延迟路径可能改变 |
| 时间对齐 | 在同一输出时间线取 frame 并合成 | callback 抖动会放大成 glitch |
| 路由选择 | 把 mixer 输出交给扬声器、耳机、蓝牙或 USB | 延迟、声道和编码方式变化 |
108.4 Resampling 与格式转换
Resampling 是把一种 sample rate 的 PCM 转成另一种 sample rate。它发生在应用格式、mixer 工作格式、HAL 设备格式或无线设备格式不一致时。格式转换还包括 channel conversion、bit depth conversion、float / integer conversion、endian / packing 调整,以及蓝牙或投送路径上的编码转换。
最常见的例子是 44.1 kHz 音乐和 48 kHz 设备输出。音乐文件以 44.1 kHz 解码成 PCM,设备输出时钟以 48 kHz 工作,系统需要在播放前做 sample rate conversion。语音通话也常遇到转换:语音编解码器可能使用 16 kHz 或 48 kHz,麦克风 route 可能暴露 48 kHz,播放设备又可能在蓝牙模式下要求另一组参数。转换的位置可能在应用、framework、mixer、HAL、DSP 或无线芯片侧。
重采样的难点来自时钟。Android 音频延迟文档明确提醒:名义 sample rate 和真实音频时钟可能有细微差异,采集和播放端点也可能使用独立时钟;这会带来 asynchronous sample rate conversion 的需求。实时语音 App 做耳返时,如果采集端每秒实际产生的 frame 数和播放端每秒实际消耗的 frame 数略有差异,ring buffer 会逐渐变满或变空。此时需要缓慢调整重采样比例、丢弃/插入极少量样本,或让 jitter buffer 吸收差异。
格式转换的系统边界可以按成本排序。channel conversion 通常成本较低,例如 mono 复制到 stereo,或者 stereo downmix 成 mono。bit depth 和 float / integer conversion 成本也可控,但要处理 clipping、rounding 和 dither。高质量 sample rate conversion 成本更高,因为它需要滤波并控制 aliasing。蓝牙编码还会引入 codec frame、无线调度、重传和接收端 buffer,这些阶段会把普通播放路径变成更长的分段 pipeline。
工程上应把转换集中到明确边界。应用内部使用稳定格式处理,例如 48 kHz float mono 或 stereo;打开系统 stream 时查询设备推荐 sample rate 和 frames per buffer;接入文件、网络语音、蓝牙 route 或 USB 设备时,在入口或出口做一次清晰的转换。Android 文档建议使用设备 optimal sample rate 和 buffer size,并指出 HAL buffer size 会随设备和系统版本变化;这一点说明硬编码 256 frames 或 44.1 kHz 会降低路径可迁移性。
108.5 Audio Buffer、Ring Buffer、Callback 与实时性
Audio Buffer 是一次读写或 callback 交付的 frame 集合。Ring Buffer 是生产者和消费者之间的循环队列,用来吸收短时间调度抖动。Callback 是系统按固定或近似固定周期调用应用的实时入口。实时性来自 deadline:在一个 10 ms callback period 中,应用必须在 10 ms 内完成读取、解码、混音、效果处理、写入和状态更新中属于当前周期的工作。
播放侧的生产者通常是应用,消费者是系统音频线程和硬件。应用写得太慢,输出 buffer 变空,硬件没有足够 frame 可取,就出现 underrun。采集侧的生产者是硬件,消费者是应用。应用读得太慢,输入 buffer 被新数据追上,就出现 overrun。xrun 是 underrun 和 overrun 的统称,表示实时音频数据流跨过了 buffer 安全边界。
buffer 大小决定延迟和稳定性的取舍。较大的 buffer 可以吸收线程调度、GC、锁等待、解码波动和短时 CPU 竞争,但每个额外 frame 都会增加等待时间。较小的 buffer 可以降低端到端延迟,但要求 callback 更稳定、处理更短、线程优先级更高。Android AAudio 文档建议通过 buffer size 和 burst size 调整低延迟路径,并说明高优先级 callback 可降低普通线程抢占和 timing jitter 带来的 glitch 风险。
callback 内的代码应保持可预测。稳定做法是只执行定长 DSP、简单 copy、轻量状态读取和无阻塞队列操作。内存分配、文件 I/O、网络 I/O、长锁、同步 IPC、日志刷盘、复杂 JSON 解析和 UI 更新都应放到非实时线程。实时语音 App 如果在 output callback 中等待网络包,就把网络 jitter 直接变成音频 underrun;更稳的结构是网络线程写入 jitter buffer,callback 只从 jitter buffer 取当前周期所需的 frame。
ring buffer 的容量应对应具体风险。耳返和乐器 App 追求低 latency,ring buffer 只保留少量 burst,重点是高优先级 callback 和原生格式。语音通话要对抗网络 jitter,jitter buffer 可能保留几十毫秒甚至更多 frame,重点是稳定播放和丢包隐藏。媒体播放器追求省电和稳定,buffer 可以更大,系统也可能选择 power saving path 或 offload path。不同路径的 buffer 目标不同,统一写成“越小越好”会把稳定性、功耗和用户体验同时拉进风险区。
108.6 Latency 组成:Hardware、HAL、Mixer、Framework、App
Audio Latency 是音频信号穿过系统所需的时间。输出延迟从应用生成 sample 到用户听见声音;输入延迟从麦克风收到声音到应用读到 PCM;round-trip latency 是输入延迟、应用处理时间和输出延迟的总和。Android 音频延迟文档使用类似划分,并指出 round-trip latency 会随设备型号和系统构建变化,所以音频延迟应作为路径属性测量和判断。
端到端延迟由多个阶段叠加。硬件阶段包括麦克风、speaker、codec、ADC / DAC、DSP、蓝牙编码和无线传输。driver / HAL 阶段包括硬件 period、DMA、kernel buffer、HAL buffer 和设备唤醒。mixer 阶段包括 track 合成、volume、effect、resampling 和 route 选择。framework 阶段包括 API 封装、IPC、权限检查、session / focus 状态和 copy。App 阶段包括解码、编码、DSP、降噪、回声消除、jitter buffer、业务逻辑和 callback 实际耗时。
用贯穿材料估算 round-trip path,可以先写成:输入硬件 + 输入 HAL buffer + App 处理 + 输出 mixer / HAL buffer + 输出硬件 / route。如果输入 callback period 是 10 ms,应用处理平均 3 ms、峰值 12 ms,输出 buffer 是 20 ms,蓝牙 route 还加入编码和无线接收 buffer,那么用户感知到的耳返延迟会明显高于单个 callback period。一个 10 ms 数字只表示某个局部周期,不能直接代表端到端体验。
低延迟路径的条件通常跨越多层。App 需要使用设备推荐 sample rate、推荐 frames per buffer、短 callback、轻量处理和合适的音频角色;系统服务需要把流放进低延迟 mixer 或专用路径;HAL 和 driver 需要支持较小 period;硬件 route 需要较短的 codec 和传输路径。Android 文档中的 android.hardware.audio.low_latency 和 android.hardware.audio.pro 是设备能力标记,能帮助应用判断平台是否承诺低延迟能力;实际路径仍要结合 route、buffer、格式和测量结果判断。
延迟优化有明确代价。保持音频设备常开可以减少 warmup latency,但会增加功耗。降低 buffer 可以改善交互手感,但会提高 underrun 风险。关闭部分信号处理可以减少处理时间,但可能降低麦克风降噪、回声控制或外放音质。移动 OS 在这里做的是策略折中:前台乐器、语音通话、媒体播放、后台播客和导航提示音有不同的 latency、power 和 quality 目标。
108.7 音频路径中的 Dropout、Glitch、Underrun、Overrun
Dropout 表示音频片段缺失或被静音填补;glitch 表示用户听到爆音、咔哒声、短暂毛刺或不连续;underrun 表示播放消费者取数据时输出 buffer 已空;overrun 表示采集生产者写入新数据时输入 buffer 已满。它们的共同根因是实时数据流没有在规定时间内完成生产、转换、传递或消费。
播放爆音常从 underrun 查起。Android AudioTrack.getUnderrunCount() 文档说明,应用写入不够快会造成 buffer underflow,并可能带来 glitch 或 pop。这个计数位于应用级播放 buffer 视角,能说明应用到系统之间的数据供应问题。它不能覆盖所有硬件、蓝牙或 mixer 故障,但它是判断播放侧调度是否稳定的一个直接证据。
录音断续常从 overrun 和读取周期查起。采集路径里,硬件按固定时钟持续产生 frame,应用读取间隔如果被主线程任务、锁等待、CPU 降频或后台限制拉长,输入 ring buffer 会失去余量。用户听到的是远端语音断续,应用看到的可能是 read 返回 frame 数不足、timestamp 跳变、音频能量突然归零,或者上层编码器收到不连续 PCM。
重采样和 route change 会制造另一类不连续。切到蓝牙耳机时,系统可能重新打开 stream,sample rate、channel、buffer duration 和 codec path 都可能变化。旧 stream 的 timestamp、position 和 buffer 语义可能失效,新的 stream 需要重新查询参数并重建环形队列。Apple Audio Session 文档强调 route change notification 和 hardware setting request 的角色;Android AAudio 也说明 audio stream 可能因设备断开或 primary device 变化进入 disconnected 状态。两类平台都要求应用把 route 变化看作音频路径重建点。
定位 dropout 和 glitch 时,应使用稳定顺序:先判定输入、输出还是 round-trip;再核对 sample rate、channel、bit depth 和 route;随后检查 buffer size、frames per callback、callback 耗时和 xrun 计数;再看 mixer 负载、effect、resampling、Bluetooth / USB / speaker route;最后把焦点、session、权限、后台状态和温控策略加入判断。这个顺序能把“声音坏了”还原成可验证的责任边界。
回到本章贯穿材料:实时语音 App 出现蓝牙耳返延迟变长时,首要路径是 round-trip。输入端仍然从麦克风到 App capture buffer,App 处理后再到 output buffer,输出端切到蓝牙后多了编码、无线传输和耳机端 buffer。若同时出现短促爆音,就继续看 output underrun、callback 峰值、jitter buffer 和 resampling drift。若录音发给远端也断续,就转向 input overrun、录音 permission / route、采集线程优先级和后台状态。
最小自检任务
一个实时语音 App 在手机上使用 48 kHz、mono、16-bit PCM 录音。它每 10 ms 从麦克风读取一批 frame,做降噪和编码,同时把远端语音解码后写入播放 buffer。用户切到蓝牙耳机后,耳返延迟明显变长;偶尔有爆音;统计发现应用音频 callback 峰值耗时达到 12 ms。请写出这次问题的系统路径、10 ms buffer 的 frame 数和字节数、最可能的责任边界,以及下一步应优先检查的证据。
答案要点
10 ms 在 48 kHz 下对应 480 frames。mono、16-bit 每个 frame 是 2 bytes,所以一批输入 buffer 是 960 bytes。这个计算只覆盖应用一次采集周期,不代表完整 round-trip latency。
系统路径应写成:麦克风 → ADC / codec → input driver / HAL → 系统音频服务 → App capture buffer → App 降噪和编码 / 解码 → App playback buffer → 系统 mixer / route policy → output HAL / driver → 蓝牙编码和无线链路 → 蓝牙耳机播放。蓝牙 route 加入了编码、无线调度和耳机端 buffer,耳返延迟变长的责任首先落在输出 route 和 round-trip path,而爆音还要继续检查播放 buffer 是否发生 underrun。
callback 峰值 12 ms 已经超过 10 ms 周期,说明应用实时线程有 deadline 风险。下一步应优先检查 callback 内是否存在阻塞操作、锁等待、分配、日志、网络等待或复杂处理;检查 output underrun / xrun 计数;检查 route change 后 sample rate、frames per buffer、channel 和 buffer duration 是否被重新读取;检查蓝牙路径下是否启用了不同 codec、mode 或更大的系统 buffer。
结论应区分三类边界:格式边界负责确认 48 kHz、mono、16-bit 是否在各阶段保持一致;调度边界负责解释 12 ms callback 峰值如何导致 underrun 或 overrun;route 边界负责解释蓝牙输出为什么增加端到端延迟。修复顺序应先让 callback 在周期内稳定完成,再按设备参数重建 buffer,最后根据蓝牙 route 的实际延迟决定是否关闭耳返、增大 jitter buffer 或提示用户使用有线 / 内置路径。
本章知识点总结
- 双向路径:采集从硬件流向应用,播放从应用流向硬件,两条路径共享设备状态、时钟、buffer 和系统策略。
- 责任链:一次音频请求要经过 App、framework、系统服务、HAL / driver、codec / route 和硬件端点。
- PCM Frame:frame 是同一时刻所有 channel 的 sample 组合,音频 buffer 时间长度通常由 frame count 和 sample rate 决定。
- 时间换算:
duration_ms = frame_count / sample_rate * 1000能把 API buffer 参数转成用户可感知的时间。 - 字节换算:
bytes = frame_count * channel_count * bit_depth / 8能检查 bufferSizeInBytes 是否匹配 PCM 格式。 - 系统混音:mixer 合成 PCM 流,策略层先按音频角色、焦点、session、route 和用户期望决定哪些流进入混音。
- 格式转换:sample rate、channel、bit depth、sample format 和无线编码不一致时,系统需要在应用、mixer、HAL 或设备边界转换。
- 时钟差异:采集端和播放端可能使用不同真实时钟,长时间实时链路需要用异步重采样或 jitter buffer 吸收 drift。
- Buffer 取舍:大 buffer 提高抗抖动能力并增加延迟,小 buffer 降低延迟并提高 callback deadline 压力。
- 实时 Callback:callback 内应只放定长、轻量、无阻塞的音频工作,网络、文件、UI 和复杂业务应放到非实时线程。
- Latency 叠加:端到端延迟由硬件、HAL、mixer、framework、App 处理和 route 编码共同组成,单个 callback period 只代表局部周期。
- 故障归因:dropout、glitch、underrun 和 overrun 都应按方向、格式、buffer、callback、mixer、route 和策略顺序定位。