AIDL Audio HAL:厂商实现,关键接口与核心机制
基于 Android 16 / NXP i.MX 平台音频栈源码(
hardware/interfaces/audio/aidl+vendor/nxp-opensource/imx/audio/default)
从 Android 13 起,Audio HAL 从 HIDL 迁移到 AIDL。这不只是 IDL 语法的替换,而是 数据面与控制面彻底分离 的一次重构:控制走 Binder,PCM 数据走 FMQ(Fast Message Queue)。本文从厂商实现的视角,讲清三件事------AIDL 的关键接口、PCM 数据流转的核心机制,以及一个真实厂商(NXP)是怎么把这套接口落地的。
一、整体架构:接口树与进程模型
AIDL Audio HAL 的接口定义在 android/hardware/audio/core/ 下,核心是一棵以 IModule 为根的树:
scss
IConfig // 全局配置 / 效果反射(独立 service)
IModule // 一个音频模块(= 一块声卡 / 一类设备)
├── IStreamOut ── IStreamCommon // 播放流
├── IStreamIn ── IStreamCommon // 录制流
├── ITelephony // 通话
├── IBluetooth / IBluetoothA2dp / IBluetoothLe
└── (AudioPort / AudioRoute / AudioPatch 作为数据结构)
一个 IModule 实例对应 audio policy 里的一个 module,按设备类型拆分。NXP 实际注册的实例:
bash
android.hardware.audio.core.IModule/default -> 板载 codec(ModulePrimary)
android.hardware.audio.core.IModule/r_submix -> Remote Submix
android.hardware.audio.core.IModule/usb -> USB Audio
android.hardware.audio.core.IModule/bluetooth -> BT
android.hardware.audio.core.IConfig/default -> 全局配置
整个 HAL 跑在一个进程 vendor.audio-hal-aidl 里(打包进 audio APEX),实时优先级运行(rlimit rtprio 10 10、ioprio rt 4)。
二、厂商落地的范式:fork + 继承,而非从零实现
这是 AIDL HAL 和 HIDL 时代最大的工程差异。AOSP 在 hardware/interfaces/audio/aidl/default 提供了一份功能完整的参考实现 (Module、Stream、状态机、FMQ 框架全都写好了)。AOSP 推荐的厂商模式是:把这份参考实现 fork 进 vendor 库,然后用继承去覆写硬件相关的钩子方法 ,而不是直接实现 BnModule。
NXP 正是这么做的。它把源码 fork 成 libaudioserviceimximpl,继承链如下:
scss
BnModule (由 IModule.aidl 自动生成)
└── Module (AOSP 参考实现,含全部通用逻辑)
└── ModulePrimary / ModuleUsb / ModuleBluetooth / ModuleRemoteSubmix / ModuleStub
基类 Module.h 里把这个扩展模型写得很明白:
"The vendor extension done via inheritance can override interface methods and augment a call to the base implementation."
也就是说,厂商绝大多数接口(getAudioPorts、setAudioPatch、openOutputStream 的骨架......)直接复用基类,只覆写真正和硬件强相关的几个 protected 虚函数。对 NXP 来说,硬件特性几乎全部集中在:
createOutputStream/createInputStream------根据 flag/format 选择具体 Stream 子类populateConnectedDevicePort------HDMI 热插拔、动态 profile 协商getMmapPolicyInfos/getNominalLatencyMs/calculateBufferSizeFrames------平台参数
加上一个 AudioCardManager 做 ALSA 声卡发现的胶水代码。这就是「厂商如何实现 AIDL」的答案核心:选择性覆写,而非全盘重写。
service 注册:数据驱动 + 普通 binder service
main.cpp 没有用 lazy service,而是建 16 线程的 binder 池、addService 后 join 阻塞:
cpp
ABinderProcess_setThreadPoolMaxThreadCount(16);
ABinderProcess_startThreadPool();
// ... 解析 audio_policy_configuration.xml,按 (name, config) 创建 Module ...
ABinderProcess_joinThreadPool();
关键点是 module 实例不是硬编码的,而是由 XML 驱动 。main() 解析 audio_policy_configuration.xml,对每个 module 名字调用工厂:
cpp
// name("default"/"usb"/...) -> Type -> 具体子类
std::shared_ptr<Module> Module::createInstance(Type type, ...) {
switch (type) {
case Type::DEFAULT: return SharedRefBase::make<ModulePrimary>(...);
case Type::USB: return SharedRefBase::make<ModuleUsb>(...);
case Type::BLUETOOTH: return SharedRefBase::make<ModuleBluetooth>(...);
// ...
}
}
// 实例名 = descriptor + "/" + name
const std::string moduleFqn = std::string(Module::descriptor).append("/").append(name);
AServiceManager_addService(result.getBinder(), moduleFqn.c_str());
VINTF manifest 声明 android.hardware.audio.core version 3,列出所有可能的实例(是个超集,实际注册哪些取决于 XML 配置)。
三、关键接口逐个看
IModule ------ 模块的中枢
IModule 管理一块声卡的拓扑、路由和流的生命周期。最重要的几组方法:
拓扑与路由(静态)
aidl
AudioPort[] getAudioPorts(); // 所有端口(device port + mix port)
AudioRoute[] getAudioRoutes(); // 端口间可达关系
AudioPort connectExternalDevice(in AudioPort templateIdAndAdditionalData);
void disconnectExternalDevice(int portId);
connectExternalDevice 是动态设备(HDMI、USB headset)接入的入口:它以一个 template device port 为模板,填入客户端给的设备地址,分配新的 portId,再调用可覆写的 populateConnectedDevicePort 去探测动态 profile(采样率/格式/声道)。NXP 在这里打开 ALSA mixer 读 HDMI EDID。
配置与连接(动态)
aidl
boolean setAudioPortConfig(in AudioPortConfig requested, out AudioPortConfig suggested);
AudioPatch setAudioPatch(in AudioPatch requested); // 建立 source->sink 的数据通路
void resetAudioPatch(int patchId);
AudioPatch 是 AIDL 路由模型的核心:它把 source port config 和 sink port config 连起来,等价于「把这条流接到这个设备上」。
开流(控制面与数据面的交汇点)
aidl
OpenOutputStreamReturn openOutputStream(in OpenOutputStreamArguments args);
OpenInputStreamReturn openInputStream(in OpenInputStreamArguments args);
返回值里既有 IStreamOut/IStreamIn binder 句柄,也有 StreamDescriptor------后者携带了三个 FMQ 的描述符。开流这一刻,控制走 binder,数据通道(FMQ)一次性建好,后续 PCM 传输就不再经过 binder 了。
全局控制 :setMasterVolume/Mute、setMicMute、updateAudioMode(通话/铃声/普通)、updateScreenRotation、generateHwAvSyncId(隧道模式音视频同步)、setVendorParameters(厂商私有 KV 参数)。
IStreamOut / IStreamIn / IStreamCommon
开流后真正用于流控的接口。IStreamCommon 是共享部分:
aidl
interface IStreamCommon {
void close(); // 释放 FMQ 与共享内存
void prepareToClose();
void updateHwAvSyncId(int hwAvSyncId);
VendorParameter[] getVendorParameters(in String[] ids);
void setVendorParameters(in VendorParameter[] params, boolean async);
void addEffect(in IEffect effect); // offload 效果
void removeEffect(in IEffect effect);
}
注意一个约定:IStreamOut.getStreamCommon() 必须每次返回同一个实例。
IStreamOut 额外有 updateMetadata(用途元数据)、updateOffloadMetadata(压缩 offload)、get/setHwVolume(按声道,[0.0, 1.0],不支持就返回 EX_UNSUPPORTED_OPERATION)、dual-mono、playback rate、selectPresentation(NGA 多节目流)。
但真正的 PCM 读写不在这些方法里------它在 FMQ 上。这就引出核心机制。
四、核心机制:StreamDescriptor 与三条 FMQ
为什么要 FMQ
HIDL 时代每次 write()/read() 都是一次 binder 调用,传一个 buffer。对音频这种高频、低延迟、周期性的负载,binder 的开销和不确定性是硬伤。AIDL 的解法:开流时建立一组 Fast Message Queue(基于共享内存的无锁环形队列),之后 PCM 数据和流控命令都走 FMQ,binder 线程只负责开关流这种低频操作。
StreamDescriptor 这个 parcelable 就是数据通道的「钥匙」,在 openOutputStream 时随 binder 句柄一起交给客户端:
aidl
parcelable StreamDescriptor {
MQDescriptor<Command, SynchronizedReadWrite> command; // client 写, HAL 读
MQDescriptor<Reply, SynchronizedReadWrite> reply; // HAL 写, client 读
// AudioBuffer audio:
MQDescriptor<byte, SynchronizedReadWrite> fmq; // PCM 数据队列
MmapBufferDescriptor mmap; // MMAP NOIRQ 模式的共享 buffer
int frameSizeBytes;
long bufferSizeFrames;
}
三条队列:
- command:客户端 → HAL 的命令队列(容量 1,严格 lockstep)
- reply:HAL → 客户端的回复队列(容量 1)
- audio.fmq:真正搬运 PCM 的数据队列
三条都是 SynchronizedReadWrite(单读单写、阻塞)。command/reply 各只存 1 个元素,强制请求/响应一一对应。
状态机:流在 HAL I/O 线程里跑
StreamDescriptor.State(@Backing(type="int"))定义了流的全部状态:
| 值 | 状态 | 含义 |
|---|---|---|
| 1 | STANDBY |
开流初始态,硬件可掉电 |
| 2 | IDLE |
硬件已激活。输出:缓冲空,硬件输出静音;输入:缓冲填充,绕回则覆盖。不计 xrun |
| 3 | ACTIVE |
正常周期性 I/O,客户端迟到计为 xrun |
| 4 | PAUSED |
消费方停止;输入缓冲满则停(不覆盖),输出发静音 |
| 5 | DRAINING |
生产方停,消费方排空缓冲;瞬时态,排空后自动跳转 |
| 6 | DRAIN_PAUSED |
仅输出,暂停排空 |
| 7 | TRANSFERRING |
仅异步模式,回复 burst 后提前解阻塞客户端;瞬时态 |
| 8 | TRANSFER_PAUSED |
仅输出异步,TRANSFERRING 中 pause |
| 100 | ERROR |
不可恢复错误,只能 close |
关键原则(写在 StreamDescriptor.aidl 头部):HAL 起一个专用高优先级 I/O 线程,独占状态机和所有队列访问,把整个模型抽象成「生产者 ↔ 环形缓冲 ↔ 消费者」(输出:client 生产 / 硬件消费;输入:反过来)。
Command 与 Reply 的字段
aidl
@FixedSize union Command {
int halReservedExit; // HAL 内部专用,解阻塞 I/O 线程退出,client 不能发
Void getStatus; // 任意状态可用,只读位置/状态
Void start;
int burst; // 参数=请求字节数;输出=要从 fmq 消费的字节,输入=要读入的字节
DrainMode drain; // DRAIN_ALL / DRAIN_EARLY_NOTIFY(gapless)
Void standby;
Void pause;
Void flush;
}
@FixedSize parcelable Reply {
int status; // OK / BAD_VALUE / INVALID_OPERATION / NOT_ENOUGH_DATA
int fmqByteCount; // 实际消费/提供的字节,不超过命令请求值
Position observable; // 呈现/采集位置,HAL 永不重置(连续流抽象)
Position hardware; // 仅 MMAP:硬件读写位置
int latencyMs; // 当前硬件延迟,-1 = 未知
int xrunFrames; // 上一次传输丢失的帧
State state; // 写回复时流所处的状态
}
burst 握手:整个机制的心跳
以阻塞模式输出为例,一次数据传输是 command → audio.fmq → reply 的乒乓:
- 客户端把 PCM 写进
audio.fmq - 客户端往
command写一个burst,然后阻塞在reply上 - HAL I/O 线程被唤醒,从
audio.fmq读数据交给硬件,把位置/延迟/状态写进reply,然后阻塞回command - 客户端被
reply唤醒,读到本次进度
输入 对称:客户端发 burst 阻塞在 reply,HAL 填 audio.fmq 并把字节数写进 reply.fmqByteCount,客户端醒来后必须把 FMQ 里的数据全部取走。
异步(非阻塞)模式 打破这个 lockstep:HAL 回复 burst 时进入 TRANSFERRING 而非 ACTIVE,提前放客户端走;客户端要等 IStreamCallback.onTransferReady 这个 oneway 回调才能发下一个 burst。
MMAP NOIRQ 模式 则完全不走数据队列:PCM 通过 mmap 共享 buffer 直接和 DSP 交换,burst 参数恒为 0,只用来取位置和延迟。
五、厂商侧的数据流转:从 FMQ 到 tinyalsa
AOSP 参考实现把上面这套 FMQ + 状态机做成了通用框架,厂商只需要填「真正碰硬件」的那一层。NXP 的实现分两层线程:
arduino
AudioFlinger (client) --command/data/reply FMQ--> StreamWorker 线程 (cycle())
│
DriverInterface (init/start/transfer/
drain/pause/flush/standby/shutdown)
│
StreamPrimary (设备仲裁 + 重采样 + 声道转换)
│
StreamAlsa::transfer --> alsa::DeviceProxy
│
tinyalsa pcm_writei / pcm_readi
第一层:每条流一个 worker 线程
每条流有一个专用 worker 线程(输出名 "writer",输入名 "reader"),循环执行 cycle():阻塞读一条命令 → 按 tag 分发 → 阻塞写一条回复。所有状态转换只在这个线程上发生,binder 线程只是把命令塞进队列。
cpp
StreamDescriptor::Command command{};
mContext->getCommandMQ()->readBlocking(&command, 1);
switch (command.getTag()) {
case Tag::start: mDriver->start(); break; // STANDBY->IDLE
case Tag::burst: isMmap() ? readMmap(&reply) : read(fmqByteCount, &reply); break;
case Tag::drain: mDriver->drain(mode); break;
case Tag::standby: mDriver->standby(); break; // IDLE->STANDBY
case Tag::pause: mDriver->pause(); break;
case Tag::flush: mDriver->flush(); break;
}
reply.state = mState;
mContext->getReplyMQ()->writeBlocking(&reply, 1);
burst 是 PCM 真正流动的地方。输出方向:从 data FMQ 读进 mDataBuffer → 交给 mDriver->transfer() → 推进帧计数:
cpp
const size_t readByteCount = dataMQ->availableToRead();
dataMQ->read(&mDataBuffer[0], readByteCount);
size_t byteCount = std::min({clientSize, readByteCount, mDataBufferSize});
mDriver->transfer(mDataBuffer.get(), byteCount / frameSize, &actualFrameCount, &latency);
reply->fmqByteCount += actualFrameCount * frameSize;
mContext->advanceFrameCount(actualFrameCount);
设备断开时,两个方向都用 usleep 模拟时序(输入额外清零),让状态机和帧计数器照常推进------这保证了拔设备不会卡死流。
第二层:DriverInterface 是硬件抽象的缝
worker 线程不直接碰硬件,而是调用 DriverInterface 这个纯虚接口:
This interface provides operations of the stream which are executed on the worker thread.
cpp
init(DriverCallbackInterface*) / start() / transfer(buf, frames, *actualFrames, *latencyMs)
/ drain(mode) / pause() / flush() / standby() / shutdown()
/ refinePosition() // 填 reply 里的 observable 位置
StreamAlsa 是它的 ALSA 实现(class StreamAlsa : public StreamCommonImpl, public DriverInterface),transfer() 最终落到 tinyalsa:
cpp
const size_t bytesToTransfer = frameCount * mFrameSizeBytes;
if (mIsInput) {
proxy_read_with_retries(mAlsaDeviceProxies[0].get(), buffer, bytesToTransfer, ...);
} else {
alsa::applyGain(buffer, mGain, bytesToTransfer, mConfig->format, mConfig->channels);
for (auto& proxy : mAlsaDeviceProxies) { // 一份数据扇出到多个输出设备
proxy_write_with_retries(proxy.get(), buffer, bytesToTransfer, ...);
}
}
proxy_write/read_with_retries 包的就是 tinyalsa 的 pcm_writei / pcm_readi,对应内核 ASoC 的 DMA ioctl。
StreamPrimary 夹在中间做平台适配:设备仲裁、mono↔stereo 声道转换、mResampler 软件重采样,再调 StreamAlsa::transfer。NXP 的 ModulePrimary::createOutputStream 在这里做关键路由决策:mmap flag → StreamOutMmap;压缩 offload → StreamOutOffloadStub;MP3 且声卡名含 "sof"(SOF DSP 能解码)→ StreamOutCompress;否则 → StreamOutPrimary。
六、总结:理解 AIDL Audio HAL 的三把钥匙
-
控制面/数据面分离:binder 只管开关流和全局控制;PCM 走 FMQ 共享内存。这是相对 HIDL 的根本变化,延迟和确定性都大幅改善。
-
StreamDescriptor 三队列 + 状态机 :command/reply lockstep 跑控制,audio.fmq 搬数据,一个 HAL 专用高优先级线程独占状态机。
burst握手是整个机制的心跳,异步模式用TRANSFERRING+onTransferReady解耦。 -
厂商靠继承而非重写 :AOSP 给了完整参考实现,厂商 fork 后只覆写
createOutputStream、populateConnectedDevicePort等少数硬件钩子,再用一层DriverInterface把通用 FMQ 框架桥到 tinyalsa/内核 ASoC。NXP 的Module → ModulePrimary、DriverInterface → StreamAlsa就是这个范式的标准样板。
理解了这三点,再看任何一家厂商的 AIDL Audio HAL 源码,都能很快定位「哪些是 AOSP 通用逻辑,哪些是这块板子自己的事」。