8、AAudio性能调优:性能模式、Buffer大小与延时权衡、帧回调与数据泵模式
各位同学,欢迎来到第八章。这一章我们聊点硬核的——AAudio的性能调优。说实话,AAudio这个API从Android O开始引入,就是为了解决传统AudioTrack和AudioRecord在高性能场景下的痛点。我自己在调优一个实时K歌App时,被延时问题折磨了整整两周,最后发现是性能模式和Buffer大小没配好。今天我把这些经验掰开揉碎了讲给你听。
8.1 性能模式:SHARED vs EXCLUSIVE
AAudio提供了两种性能模式,说白了就是音频流和底层硬件之间的“亲密程度”。
- SHARED模式:多个音频流共享同一个混音器线程。系统帮你做混音,你只管往缓冲区里写数据。优点是省电、兼容性好;缺点是延时相对较高,因为中间多了一层混音调度。
- EXCLUSIVE模式:你的音频流独占一个底层音频通道,直接和硬件驱动对话。延时极低,但代价是功耗高、且不是所有设备都支持。
我个人的习惯是:先尝试EXCLUSIVE,如果创建失败再回退到SHARED。为什么?因为很多中低端设备根本不支持EXCLUSIVE模式,你硬要申请只会得到一个错误码。
核心原则:追求极致低延时选EXCLUSIVE,追求兼容性和省电选SHARED。没有绝对的好坏,只有适合不适合。
来看一段创建音频流的代码示例:
AAudioStreamBuilder *builder;
AAudio_createStreamBuilder(&builder);
// 先尝试EXCLUSIVE模式
AAudioStreamBuilder_setPerformanceMode(builder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStream *stream;
AAudioResult result = AAudioStreamBuilder_openStream(builder, &stream);
if (result != AAUDIO_OK) {
// 回退到SHARED模式
AAudioStreamBuilder_setPerformanceMode(builder, AAUDIO_PERFORMANCE_MODE_NONE);
result = AAudioStreamBuilder_openStream(builder, &stream);
}
AAudioStreamBuilder_delete(builder);
嗯,这里要注意:AAUDIO_PERFORMANCE_MODE_LOW_LATENCY 并不等于EXCLUSIVE模式,它只是告诉系统“我想要低延时”,系统会根据设备能力自动选择。如果你想强制EXCLUSIVE,需要配合 AAudioStreamBuilder_setSharingMode(builder, AAUDIO_SHARING_MODE_EXCLUSIVE)。
避坑指南:我曾经在一个项目里强制使用EXCLUSIVE模式,结果在华为某款手机上直接崩溃。后来发现是驱动不支持,但AAudio没有优雅地返回错误,而是直接挂了。所以一定要做回退处理,别偷懒。
8.2 Buffer大小与延时权衡
Buffer大小是延时和稳定性的天平。Buffer越小,延时越低,但CPU负载越高,容易产生XRun(欠载或过载)。Buffer越大,系统越稳,但延时感人。
AAudio提供了两个关键API来获取和设置Buffer大小:
AAudioStream_getFramesPerBurst():获取硬件一次burst传输的帧数。这是最小的有效Buffer单位。AAudioStream_setBufferSizeInFrames():动态调整Buffer大小。注意,只能在流启动后调用。
我建议的调优策略是:
- 先用默认Buffer大小跑起来,确保稳定。
- 然后逐步减小Buffer,每次减少一个burst的大小。
- 每次调整后跑一段测试音频,监听是否有爆音或卡顿。
- 找到临界点后,再往回加一个burst作为安全余量。
个人经验:我一般把Buffer大小设为2~3个burst。比如burst是192帧,采样率48000Hz,那么Buffer就是384~576帧,对应延时大约8~12ms。这个区间在大多数设备上都能兼顾稳定性和低延时。
下面这张图展示了Buffer大小和延时、稳定性的关系:
8.3 帧回调与数据泵模式
AAudio提供了两种数据读写方式:一种是传统的阻塞读写(AAudioStream_read()/AAudioStream_write()),另一种是帧回调(AAudioStreamBuilder_setDataCallback())。
帧回调模式,说白了就是系统在一个高优先级线程里定时调用你的回调函数,让你填充或读取音频数据。这种方式的好处是:
- 回调运行在音频线程,优先级高,延时低。
- 不需要你自己管理线程和循环。
- 系统会尽量保证回调的实时性。
而数据泵模式,指的是你在自己的线程里循环调用write(),像水泵一样把数据“泵”进去。这种方式更灵活,但需要你自己处理线程优先级和时序。
我个人更推荐帧回调模式,尤其是对延时敏感的场景。来看一个回调的完整实现:
aaudio_data_callback_result_t dataCallback(
AAudioStream *stream,
void *userData,
void *audioData,
int32_t numFrames) {
// 从环形缓冲区读取数据
int32_t framesRead = ringBuffer_read((RingBuffer*)userData,
audioData,
numFrames);
// 如果数据不够,填充静音
if (framesRead < numFrames) {
int32_t bytesToZero = (numFrames - framesRead) *
sizeof(int16_t) * 2; // 双声道16位
memset((int16_t*)audioData + framesRead * 2, 0, bytesToZero);
}
return AAUDIO_CALLBACK_RESULT_CONTINUE;
}
// 创建流时注册回调
AAudioStreamBuilder_setDataCallback(builder, dataCallback, &ringBuffer);
AAudioStreamBuilder_setCallbackResult(builder, AAUDIO_CALLBACK_RESULT_CONTINUE);
关键点:回调函数里绝对不能做耗时操作!不能加锁(如果非要加,用无锁队列)、不能分配内存、不能打日志。我见过有人直接在回调里写__android_log_print(),结果音频直接卡成PPT。
数据泵模式适合什么场景?举个例子,如果你需要从网络流读取音频数据,网络I/O本身就有不确定性,用回调反而容易阻塞音频线程。这时候在自己的线程里用write(),配合一个较大的Buffer,会更稳妥。
我的建议:能用回调就用回调。如果必须用数据泵,记得调用AAudioStream_setBufferSizeInFrames()把Buffer调大一点,给系统留足余量。我曾经在一个项目里用数据泵模式,Buffer设得太小,结果在低端设备上频繁XRun,后来改成回调+环形缓冲区,问题迎刃而解。
8.4 综合调优流程
最后,我把整个调优流程总结成一张图,方便你对照操作:
调优不是一蹴而就的。你想想看,不同设备的音频硬件差异巨大,同一套参数在骁龙8 Gen3上跑得飞起,到了联发科天玑上可能就爆音不断。所以我的建议是:建立一套自动化测试流程,在多个设备上跑一遍,收集XRun次数和实际延时,然后取一个折中值。
最后提醒一句:不要迷信“低延时”三个字。有些场景下,比如音乐播放器,10ms和20ms的延时用户根本听不出来。但如果你为了追求5ms延时导致频繁XRun,那体验反而更差。延时和稳定性,永远是跷跷板的两端。
好了,这一章的内容就到这里。记住:性能模式选对路,Buffer大小找平衡,回调模式保实时,测试验证不能少。把这些点吃透了,AAudio调优对你来说就是小菜一碟。
公众号:蓝海资料掘金营,微信deep3321