第1章:音频硬件抽象层(HAL)基础
各位同学,咱们今天聊聊HAL。说实话,HAL是音频子系统的地基,地基不稳,上层再花哨也没用。我见过太多移植项目,上来就改上层策略,结果底层HAL接口没对齐,折腾半天白费功夫。所以这一章,咱们把HAL的底裤扒干净。
1.1 HAL模块加载机制
Android的HAL设计,说白了就是一套插件化架构。系统运行时,通过动态库加载的方式,把硬件厂商的so库拉进来。你想想看,Google不可能知道每家芯片厂的硬件细节,所以定好接口规范,厂商自己实现。
加载流程其实不复杂,核心就三步:
- 查找模块:根据硬件ID,在指定路径下找对应的so文件
- 打开模块:调用
dlopen加载动态库 - 获取接口:通过
dlsym拿到HAL_MODULE_INFO_SYM符号
嗯,这里要注意。Android 8.0之后引入了Treble架构,HAL被分成了两种形态:
| 类型 | 加载方式 | 典型路径 |
|---|---|---|
| 传统HAL(同进程) | 直接dlopen | /vendor/lib/hw/ |
| 绑定式HAL(跨进程) | 通过HIDL/Binder | /vendor/lib/hw/ + .rc服务 |
我个人习惯,在调试阶段先用传统HAL,简单粗暴。等基本功能通了,再切到绑定式HAL做正式发布。为什么?因为传统HAL出问题可以直接打log,绑定式HAL还要过一层IPC,排查起来多一道弯。
核心数据结构:每个HAL模块都必须暴露一个hw_module_t结构体,名字固定为HAL_MODULE_INFO_SYM。这是Android HAL的约定,改了就加载失败。
// 每个HAL模块必须定义这个符号
struct hw_module_t HAL_MODULE_INFO_SYM = {
.tag = HARDWARE_MODULE_TAG,
.module_api_version = AUDIO_MODULE_API_VERSION_2_0,
.hal_api_version = HARDWARE_HAL_API_VERSION,
.id = AUDIO_HARDWARE_MODULE_ID,
.name = "My Audio HAL",
.author = "Your Name",
.methods = &audio_module_methods,
};
我曾经踩过一个坑:module_api_version写错了,结果上层死活认不到我这个模块。查了两天才发现,版本号不匹配,框架直接跳过。所以版本号这东西,看着不起眼,错了真要命。
1.2 audio_hw_device_t结构体详解
模块加载完了,接下来要拿到设备操作接口。这个接口就是audio_hw_device_t。你可以把它理解成「音频硬件设备的遥控器」,所有操作都通过它来发号施令。
这个结构体很大,我挑几个关键的讲:
- common:继承自
hw_device_t,包含打开/关闭设备的基础方法 - create_stream:创建音频流,这是最核心的接口
- set_master_volume:设置主音量
- get_master_mute:获取静音状态
- dump:调试信息输出,我经常用它来查状态
我的小技巧:在dump函数里把内部状态全部打印出来,包括采样率、通道数、缓冲区大小。调试时直接adb shell dumpsys media.audio_flinger,就能看到你的HAL状态,非常方便。
struct audio_hw_device {
struct hw_device_t common;
// 创建音频输出流
int (*create_stream)(struct audio_hw_device *dev,
audio_stream_type_t type,
audio_stream_out_t **stream_out);
// 创建音频输入流
int (*create_stream_in)(struct audio_hw_device *dev,
audio_stream_type_t type,
audio_stream_in_t **stream_in);
// 音量控制
int (*set_master_volume)(struct audio_hw_device *dev, float volume);
int (*set_master_mute)(struct audio_hw_device *dev, bool muted);
// 调试接口
int (*dump)(struct audio_hw_device *dev, int fd);
};
你想想看,上层Framework调用create_stream时,传进来一个audio_stream_type_t,比如AUDIO_STREAM_MUSIC。你的HAL要根据这个类型,决定走哪个硬件通道。是走喇叭还是耳机?要不要开效果器?这些决策都在这里做。
1.3 stream相关结构体
stream结构体,说白了就是一条音频管道。输出用audio_stream_out_t,输入用audio_stream_in_t。它们都继承自audio_stream_t。
咱们重点看输出流:
struct audio_stream_out {
struct audio_stream common;
// 写入音频数据
ssize_t (*write)(struct audio_stream_out *stream,
const void *buffer, size_t bytes);
// 获取渲染位置
int (*get_render_position)(const struct audio_stream_out *stream,
uint32_t *dsp_frames);
// 设置音量
int (*set_volume)(struct audio_stream_out *stream,
float left, float right);
// 获取延迟
uint32_t (*get_latency)(const struct audio_stream_out *stream);
};
这里有个关键点:write函数是同步还是异步?我建议做成同步阻塞的。为什么?因为上层AudioFlinger期望write返回时,数据已经写入硬件缓冲区。如果你做成异步,上层无法准确知道数据何时被消费,容易导致underrun。
避坑指南:我曾经把write做成非阻塞的,结果播放音乐时频繁出现卡顿。后来发现是上层以为数据写完了,实际上硬件还没开始传输。改成同步阻塞后,问题解决。记住:音频是实时系统,时序一致性比吞吐量更重要。
输入流类似,核心是read函数:
struct audio_stream_in {
struct audio_stream common;
ssize_t (*read)(struct audio_stream_in *stream,
void *buffer, size_t bytes);
uint32_t (*get_input_frames_lost)(struct audio_stream_in *stream);
int (*set_gain)(struct audio_stream_in *stream, float gain);
};
嗯,get_input_frames_lost这个接口容易被忽略。它返回丢失的输入帧数。我在做语音唤醒项目时,就靠这个接口判断麦克风数据是否稳定。如果丢失帧数持续增长,说明硬件有问题或者驱动有bug。
1.4 HAL版本演进
Android音频HAL从诞生到现在,经历了几个大版本。我列个表,大家一目了然:
| Android版本 | HAL版本 | 主要变化 |
|---|---|---|
| 4.x - 7.x | V1.0 - V2.0 | 传统HAL,同进程调用 |
| 8.0 - 8.1 | V3.0 - V4.0 | 引入Treble,HAL可运行在独立进程 |
| 9.0 - 10.0 | V5.0 - V6.0 | 支持多声道、USB音频增强 |
| 11.0+ | V7.0+ | AAudio集成、低延迟优化 |
我个人建议,新项目直接基于V7.0起步。为什么?因为V7.0支持AAudio,延迟可以做到10ms以内。我做过一个K歌项目,用V6.0延迟在30ms左右,用户反馈有回声。升级到V7.0后,延迟降到8ms,体验完全不一样。
但要注意,版本越高,接口越复杂。V7.0新增了AAudioStream相关的回调,如果你的硬件不支持MMAP,这些接口实现起来会比较痛苦。所以选版本时,要结合硬件能力来定。
总结一下:HAL版本演进的核心逻辑,就是「解耦」和「低延迟」。从同进程到跨进程,从传统pcm到AAudio,每一步都是为了更好的性能和更灵活的架构。做移植时,先搞清楚你的目标版本,再去看对应的接口规范。
好了,这一章的内容就到这里。HAL基础打牢了,后面讲具体移植时,你才能理解每一步在做什么。下一章咱们聊音频策略管理器,那个东西决定了音频路由怎么走,也是移植中的重头戏。