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用于采集。
我一般这样设计架构:
- 创建两个AAudioStream:一个播放(输出),一个采集(输入)。
- 播放流负责输出参考信号(比如远端说话人的声音)。
- 采集流负责拾取麦克风信号(包含近端说话人声音 + 回声)。
- 在回调函数中,将播放数据和采集数据一起送入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,踩过一些坑,分享给你:
- 采样率不匹配:播放流和采集流的采样率必须一致。我曾经因为一个设了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。你会发现,代码量减少了,性能反而提升了。嗯,这就是技术进步带来的红利。