2. 音频基础

在进入 AudioFlinger 的源码世界之前,必须先建立一套关于数字音频的“度量衡”。AudioFlinger 的几乎所有设计决策——混音线程的周期、Track 的共享内存大小、FastMixer 的触发时机、时间戳的换算——都建立在本节这些基础概念之上。理解它们,才能在后续阅读 Threads.cpp、Tracks.cpp 时不被各种换算公式绕晕。

一、从模拟波形到数字采样:采样率与位深

声音本质是空气压强随时间连续变化的模拟信号。要让计算机处理,必须做两件事:在时间轴上离散化(采样),在幅度轴上离散化(量化)。

  • 采样率(Sample Rate):每秒采集多少个样本点,单位 Hz。根据奈奎斯特采样定理,采样率必须大于信号最高频率的 2 倍才能无失真还原。人耳可听范围约 20Hz~20kHz,因此 44.1kHz(CD)、48kHz(Android 默认、专业音频)、96kHz/192kHz(高解析)成为主流。Android 系统混音默认工作在 48kHz。
  • 位深(Bit Depth):每个样本用多少比特表示幅度,决定动态范围与量化噪声。16bit 提供约 96dB 动态范围,24bit 约 144dB,32bit 浮点则用于混音中间态以避免累加溢出。AudioFlinger 内部混音普遍使用 32bit 定点或浮点累加,最终再降到设备支持的位深输出。
采样率决定“时间分辨率”,位深决定“幅度分辨率”。二者共同决定单位时间的原始数据量:
数据率 = 采样率 × 位深 × 声道数(bit/s)

二、声道与 PCM:数据的组织方式

声道(Channel)描述空间布局:单声道(Mono)、立体声(Stereo,L/R)、5.1、7.1 等。Android 用 audio_channel_mask 表示,如 AUDIO_CHANNEL_OUT_STEREO、AUDIO_CHANNEL_OUT_5POINT1。声道数直接影响缓冲区大小与混音计算量。

PCM(Pulse Code Modulation,脉冲编码调制)是最基础、最通用的未压缩音频表示:按时间顺序排列的采样点序列。AudioFlinger 的 Track 共享内存中流转的正是 PCM 数据。PCM 数据本身不带采样率、位深、声道信息,这些必须由外部格式描述(如 audio_config)配套给出,否则就是“裸数据”。

// Android 中描述 PCM 格式的关键字段
audio_format_t   format;      // AUDIO_FORMAT_PCM_16_BIT / PCM_FLOAT ...
uint32_t         sample_rate; // 48000
audio_channel_mask_t channel_mask; // AUDIO_CHANNEL_OUT_STEREO
// 一帧立体声 16bit PCM 的字节数 = 2 声道 × 2 字节 = 4 字节

三、音频帧与缓冲区:AudioFlinger 的调度单位

音频帧(Audio Frame)是“同一时刻所有声道的一个采样点集合”。这是音频领域最容易混淆的概念:一帧 ≠ 一个采样点。立体声 16bit 下,1 帧 = 2 声道 × 2 字节 = 4 字节。AudioFlinger 中大量 API 以“帧”为单位,例如 getMinBufferSize()、AudioTrack.write() 的 size 参数、时间戳换算等。

缓冲区(Buffer)是生产者(App/AudioTrack)与消费者(AudioFlinger 混音线程)之间的共享内存环形队列。它的大小直接决定延迟下限。Android 中常见的几个缓冲区概念:

  • Track Buffer:每个 AudioTrack 的共享内存,App 写入、AudioFlinger 读取。
  • Mixer Buffer / HAL Buffer:混音输出到 HAL 的周期缓冲,通常对应一个“混音周期”。
  • FastMixer Buffer:Fast Track 路径下的极小缓冲,用于低延迟播放。

四、延迟与抖动:实时音频的两大敌人

延迟(Latency)是声音从产生到被听到的总时间,由多段组成:App 写入延迟 + Track 缓冲延迟 + 混音周期延迟 + HAL/驱动缓冲延迟 + 硬件 DAC 延迟。Android 官方给出的“冷启动”延迟指标就是这条链路的端到端总和。

抖动(Jitter)是延迟的时间波动,即每个音频帧到达播放端的时间间隔不均匀。抖动比固定延迟更危险:固定延迟可以补偿,抖动会造成欠载(underrun,爆音/卡顿)或过载。AudioFlinger 通过固定周期的混音线程、时间戳(timestamp)反馈、以及 FastMixer 的 SCHED_FIFO 实时调度来抑制抖动。

常见误区:认为“缓冲区越大越安全”。缓冲区越大确实越不容易欠载,但延迟也线性增大,交互式场景(游戏、通话、乐器)会明显感到“声音滞后”。低延迟与抗抖动是一对需要权衡的矛盾。

五、核心参数关系总览

下面这张图把本节所有概念串成一条从“模拟声音”到“AudioFlinger 输出”的信号链,并标注了各环节的关键参数与换算关系,便于建立整体心智模型。

数字音频信号链与 AudioFlinger 关键参数 模拟声音 连续压强波形 采样 + 量化 采样率 48kHz 位深 16/24/32bit PCM 数据 声道布局 L/R 未压缩样本序列 音频帧 Frame 1帧 = 声道数×位深/8 立体声16bit = 4字节 缓冲区 Buffer Track / Mixer / HAL 环形共享内存队列 AudioFlinger 混音 固定周期 MixerThread 32bit 累加后降位深 HAL / 驱动 / DAC 周期写入硬件缓冲 DMA 搬运至 DAC 扬声器 / 耳机 模拟声音输出 端到端延迟 Latency = App写入 + Track缓冲 + 混音周期 + HAL缓冲 + DAC 缓冲区越大 → 抗欠载能力越强,但延迟线性增大 抖动 Jitter = 延迟的时间波动,靠固定周期 + 实时调度 + 时间戳抑制 稳定周期(低抖动) 抖动周期(易欠载/爆音)

六、案例:一次“爆音”问题的参数复盘

某音乐 App 在低端机上播放 48kHz 立体声时偶发爆音。排查发现:App 侧 AudioTrack 缓冲区设为 minBufferSize 的 1 倍,而该机型 HAL 周期为 20ms,混音线程负载较高。当 App 主线程因 GC 卡顿 30ms 时,Track 缓冲被读空,产生 underrun。

// 排查关键日志(简化)
AudioFlinger: MixerThread(0x...) underrun, framesReady=0
AudioTrack: obtainBuffer timed out (is the CPU pegged?)
// 参数:sampleRate=48000, channels=2, format=PCM_16_BIT
// minBufferSize ≈ 3840 字节 ≈ 960 帧 ≈ 20ms
// 实际 bufferSizeInFrames = 960(仅 1 个 HAL 周期)

解决方案:将 App 缓冲提升到 minBufferSize 的 2~3 倍,同时把写入线程优先级提升,避免主线程 GC 影响。这体现了本节的核心权衡:延迟、抗抖动、CPU 余量三者不可兼得。

七、关键参数速查清单

概念 单位/取值 在 AudioFlinger 中的体现
采样率 Hz,常见 44100/48000 audio_config.sample_rate,混音线程重采样依据
位深 16/24/32bit AUDIO_FORMAT_PCM_*,内部 32bit 累加
声道 Mono/Stereo/5.1 audio_channel_mask_t,决定帧大小
音频帧 声道数 × 位深/8 字节 getMinBufferSize()、write() 的 size 单位
缓冲区 字节或帧 Track 共享内存、MixerBuffer、FastMixer Buffer
延迟 ms 端到端链路总和,Fast Track 可降至 ~10ms
抖动 ms(波动量) 由固定周期混音、SCHED_FIFO、时间戳共同抑制
记忆口诀:采样率管时间、位深管幅度、声道管空间、帧是打包单位、缓冲区是队列、延迟是总和、抖动是波动。后续阅读 AudioFlinger 源码时,凡是看到 frames、bufferSize、latency、timestamp 这些词,都可以回到本节这张信号链图上定位它属于哪一环。