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大小。注意,只能在流启动后调用。

我建议的调优策略是:

  1. 先用默认Buffer大小跑起来,确保稳定。
  2. 然后逐步减小Buffer,每次减少一个burst的大小。
  3. 每次调整后跑一段测试音频,监听是否有爆音或卡顿。
  4. 找到临界点后,再往回加一个burst作为安全余量。

个人经验:我一般把Buffer大小设为2~3个burst。比如burst是192帧,采样率48000Hz,那么Buffer就是384~576帧,对应延时大约8~12ms。这个区间在大多数设备上都能兼顾稳定性和低延时。

下面这张图展示了Buffer大小和延时、稳定性的关系:

Buffer大小与延时、稳定性权衡 Buffer大小(帧数) 指标值 延时(越低越好) 稳定性(越高越好) 推荐区域 2~3个burst 0 1 burst 2 burst 3 burst 4 burst

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 综合调优流程

最后,我把整个调优流程总结成一张图,方便你对照操作:

AAudio性能调优流程图 1. 选择性能模式 EXCLUSIVE创建成功? 2. 设置Buffer大小 2. 回退到SHARED模式 3. 选择数据模式 4. 测试并微调Buffer

调优不是一蹴而就的。你想想看,不同设备的音频硬件差异巨大,同一套参数在骁龙8 Gen3上跑得飞起,到了联发科天玑上可能就爆音不断。所以我的建议是:建立一套自动化测试流程,在多个设备上跑一遍,收集XRun次数和实际延时,然后取一个折中值。

最后提醒一句:不要迷信“低延时”三个字。有些场景下,比如音乐播放器,10ms和20ms的延时用户根本听不出来。但如果你为了追求5ms延时导致频繁XRun,那体验反而更差。延时和稳定性,永远是跷跷板的两端。

好了,这一章的内容就到这里。记住:性能模式选对路,Buffer大小找平衡,回调模式保实时,测试验证不能少。把这些点吃透了,AAudio调优对你来说就是小菜一碟。


公众号:蓝海资料掘金营,微信deep3321