AAudio API:AAudio的优势、与AEC的结合使用

说实话,Android音频开发这么多年,我一直在等一个真正低延迟的API。OpenSL ES?嗯,能用,但配置起来太繁琐了。直到AAudio出现,我才觉得Android音频终于有了点「专业」的样子。

这一章,我们就来聊聊AAudio到底强在哪,以及怎么把它和AEC(回声消除)结合起来用。

AAudio是什么?

AAudio是Android O(8.0)引入的原生音频API。它的目标很明确:低延迟、高性能、易使用

你想想看,以前用OpenSL ES写一个音频采集播放的循环,得写多少样板代码?AAudio把这些都简化了。我个人习惯用AAudio做实时音频处理,因为它对开发者更友好,性能也足够硬核。

AAudio的核心优势

我总结了AAudio的几个关键优势,你在项目中一定会感受到:

  • 低延迟:AAudio直接与ALSA(Advanced Linux Sound Architecture)交互,绕过了中间层。延迟可以低至5-10ms,这对AEC来说太重要了。
  • 独占模式(Exclusive Mode):AAudio支持独占模式,说白了就是直接接管音频硬件,不经过混音器。延迟更低,但要注意,不是所有设备都支持。
  • 共享模式(Shared Mode):如果独占模式不可用,AAudio会自动降级到共享模式。兼容性很好。
  • 自动重连:音频设备发生变化(比如插拔耳机),AAudio会自动处理。我在项目中遇到过,用OpenSL ES时设备切换经常导致崩溃,AAudio就稳多了。
  • 性能模式(Performance Mode):你可以指定AAudio以最低延迟或省电模式运行。AEC场景下,我建议用AAUDIO_PERFORMANCE_MODE_LOW_LATENCY

重要:AAudio的延迟优势在AEC中体现得淋漓尽致。回声消除需要参考信号和采集信号高度同步,延迟越低,同步越容易。

AAudio与AEC的结合思路

AEC的核心逻辑是:播放参考信号 + 采集麦克风信号 → 算法消除回声。AAudio正好提供了两个独立的音频流:AAudioStream用于播放,AAudioStream用于采集。

我一般这样设计架构:

  1. 创建两个AAudioStream:一个播放(输出),一个采集(输入)。
  2. 播放流负责输出参考信号(比如远端说话人的声音)。
  3. 采集流负责拾取麦克风信号(包含近端说话人声音 + 回声)。
  4. 在回调函数中,将播放数据和采集数据一起送入AEC引擎。

嗯,这里要注意:播放流和采集流必须使用相同的采样率和声道数,否则AEC算法会出问题。

代码示例:创建AAudio流

下面是我在实际项目中用过的代码片段,展示了如何创建播放流和采集流:

// 创建播放流(输出)
AAudioStreamBuilder *builder;
AAudio_createStreamBuilder(&builder);
AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_OUTPUT);
AAudioStreamBuilder_setSampleRate(builder, 48000);
AAudioStreamBuilder_setChannelCount(builder, 1);
AAudioStreamBuilder_setPerformanceMode(builder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_setDataCallback(builder, myPlaybackCallback, nullptr);

AAudioStream *playStream;
AAudioStreamBuilder_openStream(builder, &playStream);

// 创建采集流(输入)
AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_INPUT);
AAudioStreamBuilder_setInputPreset(builder, AAUDIO_INPUT_PRESET_VOICE_COMMUNICATION);
AAudioStream *recordStream;
AAudioStreamBuilder_openStream(builder, &recordStream);

AAudioStreamBuilder_delete(builder);

提示:采集流建议使用AAUDIO_INPUT_PRESET_VOICE_COMMUNICATION,这个预设会启用麦克风的回声消除硬件(如果有的话),可以和软件AEC互补。

回调函数中处理AEC

AAudio的回调函数是实时音频处理的关键。我在回调中做了三件事:

  • 从播放回调中获取参考数据(远端信号)。
  • 从采集回调中获取麦克风数据(近端信号 + 回声)。
  • 将两者送入AEC引擎,输出消除回声后的信号。

代码逻辑大致如下:

aaudio_data_callback_result_t myPlaybackCallback(
    AAudioStream *stream,
    void *userData,
    void *audioData,
    int32_t numFrames) {
    // 这里audioData是即将播放的参考信号
    // 将其复制到AEC引擎的参考缓冲区
    memcpy(aec_ref_buffer, audioData, numFrames * sizeof(float));
    return AAUDIO_CALLBACK_RESULT_CONTINUE;
}

aaudio_data_callback_result_t myRecordCallback(
    AAudioStream *stream,
    void *userData,
    void *audioData,
    int32_t numFrames) {
    // audioData是麦克风采集的信号
    // 调用AEC处理
    aec_process(aec_ref_buffer, audioData, numFrames);
    // 处理后的audioData就是消除回声后的信号
    return AAUDIO_CALLBACK_RESULT_CONTINUE;
}

警告:回调函数中不要做任何阻塞操作(比如内存分配、文件读写、锁操作)。AAudio的回调运行在实时线程中,阻塞会导致音频卡顿甚至崩溃。我曾经在这里栽过跟头,后来改用预分配缓冲区才解决。

AAudio vs OpenSL ES:AEC场景对比

我整理了一个表格,方便你对比:

特性 AAudio OpenSL ES
延迟 低(5-10ms) 中(10-30ms)
API复杂度 简单 复杂
独占模式 支持 不支持
自动重连 支持 需手动处理
AEC适用性 非常适合 一般

说白了,如果你现在开始一个新项目,我建议直接上AAudio。除非你需要兼容Android 8.0以下的设备,那才考虑OpenSL ES。

AAudio与AEC结合的核心流程图

下面这张图展示了AAudio与AEC结合的整体数据流:

AAudio + AEC 数据流架构 AAudio 播放流 参考信号(远端) AAudio 采集流 麦克风信号(近端+回声) AEC 引擎 自适应滤波器 + NLP 消除回声后的信号 参考数据 采集数据 注:播放流和采集流必须使用相同的采样率(如48000Hz)和声道数(如单声道)

实际项目中的避坑指南

我在多个项目中用过AAudio做AEC,踩过一些坑,分享给你:

  • 采样率不匹配:播放流和采集流的采样率必须一致。我曾经因为一个设了44100,一个设了48000,导致AEC完全失效。嗯,这个错误很低级,但很容易犯。
  • 缓冲区大小:AAudio的缓冲区大小会影响延迟和稳定性。我建议从AAudioStream_getFramesPerBurst()开始,然后根据实际表现调整。
  • 设备兼容性:不是所有Android设备都支持AAudio的独占模式。你可以在运行时检查AAudioStream_getPerformanceMode(),如果不支持,降级到共享模式。
  • 回调线程优先级:AAudio的回调线程优先级很高,但如果你在回调中做了耗时操作,系统可能会降低它的优先级。保持回调轻量。

个人经验:我习惯在AAudio回调中使用环形缓冲区(ring buffer)来传递数据。这样即使AEC处理稍微慢一点,也不会阻塞音频流。你可以试试std::atomic实现的lock-free ring buffer。

总结

AAudio为Android音频开发带来了真正的低延迟体验。结合AEC使用时,它的优势更加明显:API简洁、延迟可控、自动处理设备切换。我个人认为,AAudio是当前Android平台上做实时音频处理的最佳选择。

如果你还在用OpenSL ES,不妨试试迁移到AAudio。你会发现,代码量减少了,性能反而提升了。嗯,这就是技术进步带来的红利。

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