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里默认开着”。这就很坑——上层以为没开,底层其实开了,调试的时候查了半天。

核心要点:HAL层的AEC接口,本质上是给Framework一个“开关”和“状态查询”的能力。真正的算法处理,不在这一层。

5.2 ADSP侧:真正的战场

ADSP,全称是Audio Digital Signal Processor。说白了,就是一颗专门处理音频的芯片。它跟主芯片(AP,Application Processor)是分开的。

为什么要把AEC放在ADSP上?两个原因:

  • 延迟敏感:AEC需要参考信号(扬声器输出)和采集信号(麦克风输入)严格同步。如果走AP侧,经过Linux调度、音频路径切换,延迟动不动就几十毫秒,AEC根本没法做。
  • 功耗考虑:ADSP功耗极低,可以一直开着。AP侧跑AEC,CPU要一直唤醒,电池扛不住。

在ADSP上实现AEC,通常是一个叫做“音频预处理模块”的东西。它接收两路输入:

  1. 参考信号(Reference):从扬声器播放的PCM数据,直接路由一份到ADSP。
  2. 采集信号(Capture):从麦克风进来的PCM数据。

ADSP内部的AEC算法,就是拿这两路信号做自适应滤波。输出的是“干净”的采集信号,再送回AP侧。

避坑指南:我曾经调试过一个项目,ADSP侧的AEC效果一直不好。后来发现,参考信号的路由路径上,有一个软件混音器把音量调低了。ADSP拿到的参考信号跟实际扬声器发出的声音不匹配,AEC自然失效。所以,参考信号必须是从扬声器输出之前“偷”出来的,不能经过任何增益调整。

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的典型数据流。你一看就明白了:

ADSP侧AEC数据流架构 AP侧(应用处理器) ADSP侧(音频DSP) AudioFlinger 播放线程 录音线程 采集数据 参考信号 来自扬声器输出 采集信号 来自麦克风 AEC核心算法 自适应滤波 + 双讲检测 + NLP 干净采集信号 → 返回AP侧 路由一份 路由一份 处理后数据返回

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毫秒再确认。

注意:不同厂商的HAL实现差异很大。有的厂商把AEC开关放在audio_stream_inset_parameters里,有的放在audio_hw_deviceset_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内部的算法实现细节,看看自适应滤波到底是怎么跑的。

一句话总结:AEC的HAL层是“开关”,ADSP是“引擎”,AP是“调度员”。三者缺一不可。
公众号:蓝海资料掘金营,微信deep3321