Android音频数据流 笔记

7 阅读7分钟

Android音频数据流深度解析

从应用程序写入一段音频数据,到声音从扬声器或耳机发出,这段看似简单的旅程,在Android系统中要经过应用进程 → Binder → AudioFlinger → HAL → Kernel → 硬件多个关卡,期间经历共享内存传递、采样率转换、多路混音、格式重排等一系列处理。本文将专注于数据流动的细节,剥离策略与配置,只追踪音频数据在内存与设备间的每一次迁移。


一、 播放数据流全景

核心路径

App [PCM] 
    ↓ 共享内存 (匿名共享内存/Ashmem)
AudioFlinger::PlaybackThread 
    ↓ 重采样 + 混音 + 格式转换
Audio HAL :: out_write() 
    ↓ ioctl
Kernel ALSA DMA Buffer 
    ↓ I2S/SLIMbus/USB
物理Codec/DAC → 扬声器/耳机

整个流动是生产者-消费者模型

  • 生产者:App 的 AudioTrack/AAudio 写入数据到共享内存。
  • 消费者:AudioFlinger 的播放线程(MixerThread/DirectOutputThread)从共享内存取走数据,处理后交给 HAL。

二、 播放数据流:从 App 到 AudioFlinger

1. 建立通道

App 创建 AudioTrack 时,通过 IAudioFlinger::createTrack() 发起 Binder 调用,AudioFlinger 执行:

  • 分配一个 Track 对象(位于 AudioFlinger 进程空间)。
  • 通过 MemoryDealer 分配一块匿名共享内存,用于存放音频数据和控制块(Cblk)
  • 将共享内存的文件描述符(FD)通过 Binder 传回 App 进程。
  • App 端的 AudioTrack 将该 FD mmap 到自己的进程空间,获得直接读写权限。

关键数据结构:audio_track_cblk_t
位于共享内存头部,包含:

  • user / server 读写指针
  • 缓冲区大小、帧计数
  • 等待标志、下溢计数等
    App 和 AudioFlinger 通过原子操作更新这些指针,无锁通信。

2. App 写入数据

App 调用 AudioTrack::write() 或通过 obtainBuffer() + releaseBuffer() 将 PCM 数据拷贝到共享内存的环形缓冲区(Ring Buffer)。

写入位置由 user 指针指示,AudioFlinger 侧的读取位置由 server 指针指示。当缓冲区满时,App 进入等待(由 Cblk 中的 futex 机制唤醒)。


三、 AudioFlinger 内部处理循环

每个输出设备都对应一个 PlaybackThread 实例(继承关系见下),其核心是 threadLoop() 方法。

1. 线程类型与选择

  • MixerThread:默认线程,支持混音、重采样、音量调节。大多数播放走此路。
  • DirectOutputThread:绕过混音器,直接将单一 Track 的数据送入 HAL,要求格式与 HAL 能力严格匹配,用于低延迟场景。
  • OffloadThread:用于压缩音频(如 MP3、AAC)硬件直通,App 写入压缩数据,AudioFlinger 原样转发给 DSP 解码。
  • DuplicatingThread:将一个输入复制到多个输出(如同时扬声器+蓝牙耳机)。

2. 线程循环(MixerThread 为例)

threadLoop()
 ├─ prepareTracks_l()   // 遍历 mActiveTracks,收集有数据的 Track,计算混音权重
 ├─ threadLoop_mix()    // 混音核心:从每个 Track 的共享内存取出数据,重采样,累加至 mMixBuffer
 ├─ threadLoop_write()  // 将 mMixBuffer 写入 HAL,必要时进行位深度/通道转换
 └─ 进入下一次循环等待
(1)数据提取

通过 Track::getNextBuffer() 从共享内存环形缓冲区中获取数据块。该函数根据 server 指针和 user 指针的距离计算出可读数据量,返回指向共享内存某区域的指针。

(2)重采样

若 Track 的采样率与线程输出采样率(通常是 48kHz)不一致,则调用 libresample(或更高性能的 libspeexdsp)执行重采样。重采样后的数据存入临时缓冲区。

(3)混音

将所有激活 Track 的数据叠加到 mMixBuffer(32位定点格式,Q4.27,避免溢出)。混音公式:

out = Σ (in_i * volume_i)  (饱和至 24bit/16bit 范围)
(4)格式转换与写入

mMixBuffer 是固定位深(通常 32bit),需转换为 HAL 要求的格式(16bit、24bit、32bit float),并通过 HAL 的 out_write() 函数写入。


四、 HAL 与内核驱动层

1. HAL 接口

HAL 模块(如 audio.primary.xxx.so)实现 out_write(),该函数将用户空间的音频数据拷贝到内核 ALSA DMA 缓冲区。根据硬件能力,可能触发 DMA 传输,将数据通过 I2S 等总线送往音频编解码器(Codec)。

2. 内核 ALSA

Android 普遍使用 tinyalsa(用户态)而非完整 alsa-lib,通过 pcm_write() 最终调用 ioctl(..., SNDRV_PCM_IOCTL_WRITEI_FRAMES) 将数据送入内核。
内核驱动将数据放入 DMA 缓冲区,由硬件周期性中断触发传输。

关键点:内核缓冲区大小
该缓冲区决定了整个路径的最低延迟。通常为 4 个 Period,每个 Period 长度由 HAL 报告。


五、 录制数据流

录制是播放的逆过程,同样经过共享内存,但方向相反。

1. 建立通道

App 创建 AudioRecord,通过 IAudioFlinger::openRecord() 获取共享内存,AudioFlinger 创建 RecordThreadRecordTrack

2. 数据采集

  • HAL 的 in_read() 从麦克风硬件读取数据(阻塞或非阻塞)。
  • RecordThread 轮询 HAL,获得 PCM 数据后写入每个 RecordTrack 的共享内存环形缓冲区。
  • App 通过 AudioRecord::read()obtainBuffer() 从共享内存拷贝数据。

六、 低延迟数据流:AAudio / Oboe 与 MMAP

传统路径经过两次拷贝(App→共享内存,共享内存→AudioFlinger内部缓冲区→HAL缓冲区),且经过两次调度(App线程、AudioFlinger线程),延迟通常在 20ms 以上。

AAudio 在 Android 8.1 引入,Oboe 是其 C++ 封装,核心优化是 MMAP(内存映射)

  • AudioFlinger 不再介入数据搬运,直接将 HAL 的内核 DMA 缓冲区映射到 App 进程。
  • App 通过 write() 直接将数据写入映射的内存,完全绕过 AudioFlinger 的线程循环。
  • 同步由内核直接完成(通过 poll() 等待可用空间)。

数据流变为

App [PCM] 
    ↓ 内存映射 (mmap of DMA buffer)
HAL/Kernel DMA Buffer
    ↓ DMA
硬件

延迟可低至 5ms 以下。


七、 数据格式的流转

阶段格式(以常见场景为例)
App 写入PCM 16bit, 44.1kHz, 2ch
AudioFlinger 内部混音PCM 32bit, 48kHz, 2ch (重采样并扩展位深)
HAL out_write 输入PCM 16bit, 48kHz, 2ch (或 24bit/32bit)
内核 DMA 缓冲区与 HAL 要求一致
Codec 输入I2S 信号,PCM 格式
扬声器输出模拟信号

重采样损耗:当 App 提供 44.1kHz 而系统输出固定 48kHz 时,必须重采样。重采样不是无损的,高频信息会有衰减。


八、 数据流调试与观测

1. 查看活跃音频流

adb shell dumpsys media.audio_flinger

输出中包含所有 PlaybackThread 及其挂载的 Track 信息,可查看:

  • 采样率、通道数、格式
  • 共享缓冲区大小、当前填充帧数
  • 下溢(underrun)计数

2. 分析延迟

使用 systrace 抓取音频渲染周期:

python systrace.py audio hal input sched freq

可看到 App 写入与 AudioFlinger 读取的时间差、HAL 写入与中断发生的间隔。

3. 模拟数据丢失

通过 tinyplay 直接向 HAL 写入数据,绕过整个框架,可对比延迟差异。


九、 总结:数据流动的核心要点

阶段关键机制瓶颈/优化点
App → AudioFlinger共享内存环形缓冲 + 无锁控制块缓冲区大小影响延迟和吞吐量
AudioFlinger 内部重采样 + 混音重采样算法质量;混音算法是否 SIMD 优化
AudioFlinger → HALout_write() 阻塞调用HAL 实现质量;内核调度
HAL → 硬件DMA 传输硬件缓冲区大小(决定中断频率)
完整路径生产者-消费者同步唤醒延迟、CPU 频率、电源管理策略

Android 音频数据流始终围绕“如何高效、可靠地将 PCM 样本从用户态搬移到硬件”这一核心命题。传统路径追求通用性(多路混音、重采样),但付出了延迟与保真度的代价;现代路径(MMAP、Offload)试图在特定场景下绕过通用层,以换回性能与音质。理解数据流,是优化音频体验和调试音质问题的基本功。