5、Android AEC HAL层:HAL接口定义、ADSP与AP侧的AEC实现
好,咱们继续往下走。上一章我们把AEC的算法原理聊透了,这一章要落地了——说说Android系统里,AEC到底是怎么跟硬件打交道的。
说白了,AEC不是纯软件就能搞定的事。你想想看,麦克风采集到的信号,跟扬声器播放的信号,这两条路在硬件上就是分开的。谁来做这个减法?谁来保证延迟足够低?这就引出了我们今天的主角——HAL层和ADSP。
5.1 HAL接口:承上启下的关键一层
Android的音频架构,我习惯把它看成三层:上层是Framework,中间是HAL,下层是Kernel和硬件。AEC的实现,就卡在HAL这一层。
为什么?因为Framework只管“我要录音”、“我要播放”,它不关心回声消除这种细节。真正干活的是HAL——它把上层抽象的音频流,映射到底层具体的硬件通路。
在Android的audio_hw.h里,有几个关键的结构体,我挑重点说:
struct audio_stream_in {
// 标准输入流接口
ssize_t (*read)(struct audio_stream_in *stream, void *buffer, size_t bytes);
// 获取麦克风方向、增益等参数
status_t (*set_parameters)(struct audio_stream_in *stream, const char *kvpairs);
// 重点:获取AEC是否启用
bool (*get_aec_enabled)(const struct audio_stream_in *stream);
};
嗯,这里要注意:get_aec_enabled这个函数,不是所有厂商都实现的。我在项目中遇到过,某款芯片的HAL里直接返回false,然后告诉你“我们AEC在ADSP里默认开着”。这就很坑——上层以为没开,底层其实开了,调试的时候查了半天。
5.2 ADSP侧:真正的战场
ADSP,全称是Audio Digital Signal Processor。说白了,就是一颗专门处理音频的芯片。它跟主芯片(AP,Application Processor)是分开的。
为什么要把AEC放在ADSP上?两个原因:
- 延迟敏感:AEC需要参考信号(扬声器输出)和采集信号(麦克风输入)严格同步。如果走AP侧,经过Linux调度、音频路径切换,延迟动不动就几十毫秒,AEC根本没法做。
- 功耗考虑:ADSP功耗极低,可以一直开着。AP侧跑AEC,CPU要一直唤醒,电池扛不住。
在ADSP上实现AEC,通常是一个叫做“音频预处理模块”的东西。它接收两路输入:
- 参考信号(Reference):从扬声器播放的PCM数据,直接路由一份到ADSP。
- 采集信号(Capture):从麦克风进来的PCM数据。
ADSP内部的AEC算法,就是拿这两路信号做自适应滤波。输出的是“干净”的采集信号,再送回AP侧。
5.3 AP侧:兜底方案
不是所有手机都有ADSP。低端芯片、或者某些平板电脑,可能就把AEC放在AP侧跑。
AP侧实现AEC,通常有两种方式:
- 纯软件AEC:在AudioFlinger或者录音线程里,直接调用一个AEC库(比如Speex、WebRTC的AEC模块)。这种方式实现简单,但延迟高、效果一般。
- 硬件加速AEC:AP侧有一个DSP核心(比如Hexagon),可以分担一部分计算。但本质上还是AP在管。
我个人建议,如果条件允许,尽量用ADSP方案。AP侧做AEC,有个绕不开的问题——你没法保证参考信号和采集信号的时间戳完全对齐。Android的音频路径里,播放和录音是两个独立的线程,时间戳漂移是常态。
5.4 整体架构图
下面这张图,是我自己画的ADSP侧AEC的典型数据流。你一看就明白了:
5.5 HAL接口定义实战
说了这么多理论,咱们看看实际代码。在Android 12之后的版本里,HAL接口定义在hardware/libhardware/include/hardware/audio.h中。我摘一段跟AEC相关的:
// 音频输入流的AEC控制
#define AUDIO_PARAMETER_KEY_AEC_ENABLE "aec_enable"
#define AUDIO_PARAMETER_KEY_AEC_REFERENCE "aec_reference"
// 设置AEC开关
static status_t set_aec_enable(struct audio_stream_in *stream, bool enable) {
char value[32];
snprintf(value, sizeof(value), "%s=%d",
AUDIO_PARAMETER_KEY_AEC_ENABLE, enable ? 1 : 0);
return stream->set_parameters(stream, value);
}
// 设置参考信号源(通常就是扬声器输出)
static status_t set_aec_reference(struct audio_stream_in *stream,
audio_devices_t device) {
char value[64];
snprintf(value, sizeof(value), "%s=%d",
AUDIO_PARAMETER_KEY_AEC_REFERENCE, device);
return stream->set_parameters(stream, value);
}
这段代码看着简单,但坑不少。我遇到过最典型的问题:set_parameters是异步的,你调用完立刻去读状态,可能还没生效。所以,我习惯在设置之后加一个usleep(10000),等10毫秒再确认。
audio_stream_in的set_parameters里,有的放在audio_hw_device的set_parameters里。调试前,先看厂商的HAL源码,别想当然。
5.6 ADSP与AP的协作模式
最后,说说ADSP和AP是怎么配合的。我把它总结成三步:
| 步骤 | AP侧动作 | ADSP侧动作 |
|---|---|---|
| 1. 初始化 | 打开录音流,设置AEC参数 | 加载AEC算法库,分配内存 |
| 2. 运行时 | 将播放数据路由一份到ADSP | 接收参考+采集信号,实时处理 |
| 3. 结束 | 关闭录音流,释放资源 | 卸载算法,释放内存 |
这里有个细节:ADSP处理完的数据,怎么送回AP?通常是通过一个共享内存区域(ION buffer)。AP侧录音线程从这块内存里读数据,而不是直接从麦克风硬件读。这样,上层拿到的就是已经消除回声的干净信号。
嗯,这一章的内容就到这。HAL层和ADSP的配合,说白了就是“分工明确、各司其职”。AP负责调度和策略,ADSP负责实时计算。下一章我们会深入ADSP内部的算法实现细节,看看自适应滤波到底是怎么跑的。