3、音频驱动接口:ALSA架构简介、tinyalsa库使用、PCM设备节点操作、Mixer控制接口
好,咱们正式开始啃音频驱动这块硬骨头。
说实话,很多做上层应用的朋友一听到「驱动」两个字就头大。其实没那么可怕。你想想看,音频驱动说白了就是让CPU和音频编解码芯片(Codec)能正常对话。我做了这么多年移植,最深的体会就是:搞懂ALSA,你就掌握了音频底层的一半。
3.1 ALSA架构——音频子系统的骨架
ALSA,全称是Advanced Linux Sound Architecture。名字挺唬人,但核心思想很简单:把音频硬件抽象成文件。
在Linux世界里,一切皆文件。音频设备也不例外。ALSA在用户空间暴露了两类接口:
- PCM设备节点:负责音频数据的读写,也就是播放和录音
- Mixer控制接口:负责音量调节、通路切换、静音控制等
这两者分工明确。我刚开始接触ALSA时,总把PCM和Mixer混为一谈,结果调试音量时老找不到对应的控制项。后来才明白:PCM管数据流,Mixer管控制流,两条线互不干扰。
ALSA在内核空间的层次结构大致是这样的:
用户空间 (tinyalsa / alsa-lib)
↓
ALSA 内核框架 (snd_pcm / snd_control)
↓
硬件驱动层 (soc-core / codec driver)
↓
物理硬件 (I2S / PCM 总线 + Codec 芯片)
嗯,这里要注意:AOSP里默认用的是tinyalsa,而不是标准的alsa-lib。为什么?因为tinyalsa更轻量,没有那么多依赖,适合嵌入式场景。我见过不少新手在移植时直接拿alsa-lib的代码去编译,结果链接一堆符号找不到,折腾半天。
核心要点:AOSP音频HAL层通过tinyalsa与内核ALSA驱动交互。你写的HAL代码,本质上就是在封装tinyalsa的调用。
3.2 tinyalsa库——轻量级的ALSA用户空间接口
tinyalsa是Google为Android量身定做的。它只保留了ALSA最核心的功能:PCM读写和Mixer控制。
我个人习惯把tinyalsa的API分成三组:
| 功能组 | 核心函数 | 用途 |
|---|---|---|
| PCM打开/关闭 | pcm_open() / pcm_close() |
建立与PCM设备的连接 |
| PCM读写 | pcm_read() / pcm_write() |
传输音频数据 |
| Mixer控制 | mixer_open() / mixer_get_ctl_by_name() |
获取并设置混音器控制项 |
你看,就这么几个函数,覆盖了音频驱动接口的全部需求。我在项目中遇到过有人试图用ioctl直接操作设备节点,绕开tinyalsa。结果呢?代码又长又难维护,而且容易踩坑。我建议你老老实实用tinyalsa,Google已经帮你封装好了。
3.3 PCM设备节点操作——音频数据的读写通道
PCM设备节点通常位于/dev/snd/目录下。你执行ls /dev/snd/会看到类似这样的输出:
controlC0 pcmC0D0c pcmC0D0p pcmC0D1c pcmC0D1p timer
命名规则其实很直观:
pcmC0D0p:Card 0, Device 0, playback(播放)pcmC0D0c:Card 0, Device 0, capture(录音)controlC0:Card 0的Mixer控制接口
打开PCM设备的代码示例:
#include <tinyalsa/asoundlib.h>
struct pcm *pcm;
struct pcm_config config = {
.channels = 2,
.rate = 48000,
.period_size = 1024,
.period_count = 4,
.format = PCM_FORMAT_S16_LE,
};
pcm = pcm_open(0, 0, PCM_OUT, &config);
if (!pcm || !pcm_is_ready(pcm)) {
// 处理错误
}
这里有个坑,我曾经踩过:period_size和period_count的搭配。period_size决定了一次中断传输的数据量,period_count决定了缓冲区能容纳多少个period。如果设置不当,会出现Xrun(缓冲区溢出或欠载)。我建议你从1024/4开始试,大部分Codec都能稳定工作。
注意:pcm_open()的第一个参数是card编号,第二个参数是device编号。在多声卡设备上,一定要确认你打开的是正确的设备。我见过有人把麦克风数据写到了喇叭设备上,结果调试了一整天。
3.4 Mixer控制接口——音量与通路的遥控器
Mixer接口负责控制音频通路。比如调节音量、切换输入源、静音等。这些操作不涉及音频数据流,只改变硬件的状态。
使用Mixer的典型流程:
struct mixer *mixer;
struct mixer_ctl *ctl;
// 打开Mixer
mixer = mixer_open(0);
if (!mixer) {
// 处理错误
}
// 通过名字获取控制项
ctl = mixer_get_ctl_by_name(mixer, "Master Volume");
if (!ctl) {
// 处理错误
}
// 设置值(假设是单声道音量,范围0-100)
mixer_ctl_set_value(ctl, 0, 80);
控制项的名字因Codec而异。同一颗芯片在不同厂商的板子上,名字可能不一样。我建议你先通过tinymix工具把所有控制项列出来,确认名字后再写代码。
执行tinymix命令,你会看到类似这样的输出:
Number of controls: 12
ctl type num name value
0 ENUM 1 "AIF1 Playback Mixer Source" DAC
1 INT 2 "Master Volume" 80 80
2 BOOL 1 "Speaker Switch" On
...
嗯,这里要注意:控制项的类型有INT(整型)、BOOL(布尔)、ENUM(枚举)三种。设置方法不一样。INT用mixer_ctl_set_value(),BOOL用mixer_ctl_set_value()传0或1,ENUM用mixer_ctl_set_enum_by_name()。
个人经验:调试Mixer时,我习惯先用tinymix把所有控制项的值dump出来,然后手动修改一个值,再dump一次,对比差异。这样能快速定位哪个控制项对应哪个功能。比看datasheet快多了。
3.5 避坑指南——我这些年踩过的雷
最后,分享几个实战中容易出问题的地方:
- 设备节点权限:Android系统里,/dev/snd/*的权限通常是0660,属于audio组。你的HAL进程必须在audio组里,否则打不开设备。我曾经在userdebug版本上一切正常,切到user版本就报错,查了半天发现是SELinux策略没放行。
- PCM参数匹配:你设置的采样率、位深、通道数,必须和Codec硬件支持的一致。不一致时,pcm_open()可能成功,但读写时会静默失败。我建议你在初始化后调用
pcm_get_params()确认实际参数。 - Mixer控制项的持久化:Mixer设置不会自动保存。每次系统重启后,音量、通路等状态都会恢复成默认值。你需要在HAL的
open_output_stream()或set_parameters()里重新设置。
好了,这一章的内容就到这里。ALSA架构、tinyalsa库、PCM节点、Mixer接口,这四个知识点是音频驱动移植的基石。下一章我们会深入HAL层的具体实现,看看这些接口是怎么被封装成Android标准的音频策略的。