8. 音频输出与渲染:AudioSink接口、AudioTrack与FastMixer、音频时间戳与同步
音频输出,说白了就是让声音从手机喇叭里发出来。但Android系统里这条路径并不简单。我刚开始接触Stagefright时,以为音频输出就是调个API写数据,结果发现底层藏着AudioSink、AudioTrack、FastMixer这一整套机制。今天我们就把它拆开看看。
8.1 AudioSink接口:音频输出的抽象层
AudioSink是一个接口,定义在frameworks/av/media/libmedia/include/media/AudioSink.h里。它把音频输出的具体实现藏了起来。上层播放器只管调用它的方法,不用关心底层是走AudioTrack还是别的什么。
核心方法就几个:
open():打开音频输出,设置采样率、通道数、格式write():写入音频数据start()/stop():启停播放pause()/resume():暂停恢复getPlaybackRate():获取当前播放速率getTimestamp():获取音频时间戳
我个人习惯把AudioSink看作一个「音频输出插座」。你插上不同的设备,背后走不同的线路,但插头接口是一样的。
关键点:AudioSink是NuPlayer和AudioTrack之间的桥梁。NuPlayer通过它把解码后的PCM数据送出去,至于数据怎么送到硬件,AudioSink不管。
8.2 AudioTrack:真正的音频渲染器
AudioTrack是Android音频输出的核心类。它运行在AudioFlinger服务进程中,负责把PCM数据写入音频混音器,最终送到音频硬件。
AudioTrack有两种模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| MODE_STATIC | 一次性写入所有数据,循环播放 | 短音效、铃声 |
| MODE_STREAM | 持续写入数据流 | 音乐、视频、游戏 |
Stagefright用的是MODE_STREAM模式。数据通过共享内存从客户端传到AudioFlinger,再由AudioFlinger混音后输出。
嗯,这里要注意:AudioTrack的缓冲区大小很关键。太小了容易欠载(underrun),声音会断断续续;太大了延迟又高。我记得在项目中调一个直播场景,缓冲区设成两倍采样周期才搞定,既没卡顿延迟也还能接受。
经验之谈:AudioTrack的缓冲区大小建议用getMinBufferSize()获取最小值,然后根据场景适当放大。直播场景建议1.5-2倍,本地播放可以3-4倍。
8.3 FastMixer:低延迟的秘密武器
FastMixer是Android 4.1引入的。它解决了音频延迟过大的问题。你想想看,传统的AudioFlinger混音器跑在正常线程里,调度延迟可能几十毫秒。FastMixer跑在专用高优先级线程上,延迟能降到10ms以内。
FastMixer的工作原理:
- 它维护一个「快速混音缓冲区」
- 多个AudioTrack的数据先写入这个缓冲区
- FastMixer线程以固定周期(比如5ms)把数据送到音频硬件
- 它绕过了AudioFlinger的主混音器,直接操作音频驱动
我曾经在调试一个蓝牙耳机的低延迟模式时,发现FastMixer没启用。查了半天,原来是采样率不匹配——FastMixer只支持44100Hz和48000Hz。换成48000Hz后,延迟从80ms降到了25ms。
避坑指南:FastMixer不是所有场景都能用。它要求音频路径必须走primary输出设备,而且采样率必须是44100Hz或48000Hz。如果你用USB声卡或蓝牙A2DP,FastMixer会自动降级。
8.4 音频时间戳与同步
音频时间戳是音画同步的基础。Stagefright通过AudioSink::getTimestamp()获取当前播放的音频帧位置,然后换算成时间。
时间戳结构体定义:
struct AudioTimestamp {
int64_t mPosition; // 已播放的音频帧数
int64_t mTimeNs; // 对应的CLOCK_MONOTONIC时间(纳秒)
};
为什么需要这个?因为音频播放是硬件驱动的。你调用write()写入数据,但数据什么时候真正从喇叭出来,你并不知道。时间戳告诉你:在某个时刻,硬件已经播放了多少帧。
音画同步的逻辑:
- 视频解码器输出一帧画面,记录它的PTS(显示时间戳)
- 音频解码器输出音频数据,也带PTS
- 播放器定期查询音频时间戳,得到当前音频播放位置
- 比较视频PTS和音频时间戳,决定是丢帧还是等待
我遇到过一个问题:某些设备上getTimestamp()返回的位置不准确,导致音画不同步。后来发现是音频驱动的时间戳实现有bug。解决方案是改用getPlaybackRate()中的位置信息做辅助校准。
核心要点:音频时间戳是音画同步的「时钟源」。视频要跟着音频走,因为人耳对音频抖动的敏感度远高于视频。这就是为什么Stagefright里音频是主时钟。
8.5 整体流程梳理
把上面这些串起来,音频输出的完整路径是这样的:
- NuPlayer解码出PCM数据
- 通过AudioSink接口写入
- AudioSink内部调用AudioTrack的write()
- 数据通过共享内存传到AudioFlinger
- AudioFlinger判断是否启用FastMixer
- 数据进入混音器,最终送到音频硬件
- 硬件播放的同时,时间戳信息返回给上层
下面这张图展示了核心流程:
从图里能看出来,时间戳反馈是反向的。硬件播放完数据后,把位置信息一层层传回给NuPlayer。NuPlayer拿这个信息去校准视频播放速度,保证音画同步。
8.6 实战中的几个坑
最后分享几个我踩过的坑:
- 缓冲区大小:别直接用
getMinBufferSize(),那是最小值。实际项目中要根据场景放大,否则低端设备容易欠载。 - 采样率匹配:FastMixer只支持44100和48000。如果你用96000,它会降级到普通混音器,延迟飙升。
- 时间戳精度:某些设备的
getTimestamp()实现有bug,返回的位置跳跃。建议做平滑滤波,或者用多个采样点取平均。 - 线程优先级:AudioTrack的回调线程优先级不够高时,音频会卡顿。可以在
AudioTrack::setCallback()里设置线程优先级。
音频输出这块,说白了就是「数据怎么从解码器流到喇叭」。理解了AudioSink、AudioTrack、FastMixer这三层,你就掌握了Android音频渲染的骨架。时间戳是灵魂,没有它音画同步就是空谈。
一句话总结:AudioSink是接口,AudioTrack是实现,FastMixer是加速器,时间戳是同步的锚点。四者配合,才能让声音流畅、同步地出来。
公众号:蓝海资料掘金营,微信deep3321