8、音频HAL层接口:HAL架构与标准接口、HAL加载与调用流程、HAL与AudioFlinger的交互、常见HAL实现
好,我们进入音频HAL层。这一层,说白了就是AudioFlinger和硬件之间的翻译官。你想想看,AudioFlinger是个纯软件框架,它不懂怎么操作具体的音频芯片。而硬件厂商的驱动,又各有各的脾气。HAL就是中间那个标准化的适配层。
我当年刚接触Android音频时,有个很深的体会:搞懂了HAL,你就搞懂了音频系统的边界。哪里是软件的事,哪里是硬件的事,全看HAL怎么划分。
8.1 HAL架构与标准接口
Android的音频HAL架构,其实经历过一次大升级。早期是legacy HAL,后来演进到了HIDL HAL,再到现在的AIDL HAL。但不管怎么变,核心思想没变:定义一套标准接口,让AudioFlinger能统一调用。
我们先看最核心的接口定义。在hardware/libhardware/include/hardware/audio.h中,定义了audio_hw_device_t这个结构体。它就是HAL设备的"身份证"。
struct audio_hw_device {
struct hw_device_t common;
// 核心操作函数指针
int (*init_check)(struct audio_hw_device *dev);
int (*set_voice_volume)(struct audio_hw_device *dev, float volume);
int (*get_microphones)(struct audio_hw_device *dev,
struct audio_microphone_characteristic_t *mic_array,
size_t *mic_count);
// 创建音频流
int (*open_output_stream)(struct audio_hw_device *dev,
audio_io_handle_t handle,
audio_devices_t devices,
audio_output_flags_t flags,
struct audio_config *config,
struct audio_stream_out **stream_out,
const char *address);
int (*open_input_stream)(struct audio_hw_device *dev,
audio_io_handle_t handle,
audio_devices_t devices,
struct audio_config *config,
struct audio_stream_in **stream_in,
audio_input_flags_t flags,
const char *address,
audio_source_t source);
// 关闭流
void (*close_output_stream)(struct audio_hw_device *dev,
struct audio_stream_out *stream_out);
void (*close_input_stream)(struct audio_hw_device *dev,
struct audio_stream_in *stream_in);
};
这里有个关键点:HAL接口是函数指针表。AudioFlinger拿到这个结构体后,直接调用里面的函数指针。这种设计的好处是——厂商可以自由实现内部逻辑,只要把函数指针填对就行。
核心接口分类:
- 设备级接口:init_check、set_voice_volume、set_master_volume等,控制整个音频设备
- 流级接口:open_output_stream、open_input_stream,创建具体的音频流
- 流操作接口:write、read、get_presentation_position等,在audio_stream_out/audio_stream_in结构体中定义
我个人习惯把HAL接口想象成"三明治结构"。上层是AudioFlinger,下层是硬件驱动,中间这层HAL就是那个肉饼。肉饼怎么做,全看厂商的配方。
8.2 HAL加载与调用流程
HAL是怎么被加载进来的?这个过程,我建议你记住一个关键函数:load_audio_interface()。
它位于AudioFlinger.cpp中,大致流程是这样的:
- 获取HAL模块:通过
hw_get_module()加载AUDIO_HARDWARE_MODULE_ID对应的so库 - 打开设备:调用
audio_hw_device_open(),拿到audio_hw_device_t指针 - 初始化检查:调用
init_check()确认硬件是否就绪 - 设置参数:配置采样率、通道数等初始参数
我曾经在调试一个项目时,发现设备启动后音频一直没声音。查了半天,原来是hw_get_module()加载的so库路径不对。Android 8.0之后,HAL库的路径从/system/lib/hw/移到了/vendor/lib/hw/,厂商没更新配置文件,导致加载失败。嗯,这种坑,踩过一次就记住了。
我们来看加载的核心代码片段:
// AudioFlinger.cpp 中加载HAL的简化逻辑
status_t AudioFlinger::load_audio_interface() {
const hw_module_t *mod;
int rc;
// 1. 获取HAL模块
rc = hw_get_module(AUDIO_HARDWARE_MODULE_ID, &mod);
if (rc != 0) {
ALOGE("Failed to get audio hardware module");
return NO_INIT;
}
// 2. 打开设备
audio_hw_device_t *dev;
rc = audio_hw_device_open(mod, &dev);
if (rc != 0) {
ALOGE("Failed to open audio hardware device");
return NO_INIT;
}
// 3. 初始化检查
rc = dev->init_check(dev);
if (rc != 0) {
ALOGE("Audio hardware not ready");
audio_hw_device_close(dev);
return NO_INIT;
}
// 4. 保存设备指针
mPrimaryHardwareDev = dev;
return OK;
}
避坑指南:我曾经遇到过一个问题——init_check()返回了非零值,但硬件明明已经就绪了。后来发现是厂商的HAL实现里,init_check()检查了一个不存在的设备节点。所以,HAL的健壮性很大程度上取决于厂商的实现质量。
8.3 HAL与AudioFlinger的交互
AudioFlinger和HAL的交互,本质上就是函数调用。但这里有个设计模式值得注意——回调机制。
AudioFlinger不会主动去轮询HAL的状态。相反,HAL通过回调函数,把事件通知给AudioFlinger。比如:
- 音频设备插拔:耳机插入时,HAL调用回调通知AudioFlinger
- 音频焦点变化:某些HAL实现会通知上层音频焦点变化
- 参数变化:比如采样率变化、音量变化等
这个回调机制,是通过audio_hw_device_t中的set_parameters和get_parameters实现的。但更重要的,是create_audio_patch和release_audio_patch这些新接口。
我画了一张图,帮你理解这个交互流程:
你看这张图,数据流是双向的。播放时,AudioFlinger把PCM数据往下推;录音时,数据从硬件往上送。HAL在中间做格式转换、音量控制、路由切换这些脏活累活。
8.4 常见HAL实现
不同的硬件平台,HAL实现差异很大。我接触过的几种常见实现:
| 实现类型 | 典型平台 | 特点 |
|---|---|---|
| ALSA HAL | 高通、MTK | 基于Linux ALSA驱动,通过tinyalsa库操作 |
| QCOM HAL | 高通平台 | 深度定制,支持DSP卸载、低功耗音频 |
| MTK HAL | 联发科平台 | 私有接口多,与硬件耦合度高 |
| USB HAL | 通用 | 基于USB Audio Class,支持外接设备 |
| 模拟HAL | 模拟器 | 基于文件或内存模拟,用于测试 |
以ALSA HAL为例,它的核心实现思路是:
// 简化的ALSA HAL实现思路
static int alsa_open_output_stream(struct audio_hw_device *dev,
audio_io_handle_t handle,
audio_devices_t devices,
audio_output_flags_t flags,
struct audio_config *config,
struct audio_stream_out **stream_out,
const char *address) {
struct alsa_stream_out *out;
// 1. 分配流对象
out = (struct alsa_stream_out *)calloc(1, sizeof(*out));
if (!out) return -ENOMEM;
// 2. 打开ALSA设备
out->pcm = pcm_open(0, 0, PCM_OUT, &config->format);
if (!out->pcm || !pcm_is_ready(out->pcm)) {
free(out);
return -EIO;
}
// 3. 设置函数指针
out->stream.write = alsa_write;
out->stream.get_presentation_position = alsa_get_presentation_position;
*stream_out = &out->stream;
return 0;
}
注意事项:不同平台的HAL实现,对音频路由的处理差异很大。高通平台有audio_route机制,通过Mixer Paths配置文件控制音频路径。而MTK平台则使用自己的AudioPolicyManager。如果你做跨平台移植,这部分是最容易出问题的。
我记得有一次,在MTK平台上调试录音功能。明明HAL的open_input_stream返回成功了,但读出来的数据全是静音。后来发现,MTK的HAL要求先设置input_source参数,才能正确路由到麦克风。这个参数在标准HAL接口里没有强制要求,但MTK的实现依赖它。嗯,这就是厂商定制的"坑"。
最后说一句,HAL层的调试,最常用的手段就是加日志。在audio_hw_device_t的每个函数入口加ALOGD,基本能定位90%的问题。剩下的10%,可能是硬件时序问题,那就得用示波器了。
公众号:蓝海资料掘金营,微信deep3321