目录
一、先准备一台 Linux 机器
二、先看看系统到底有没有声卡
三、什么是 Card?
四、什么是 Device?
五、hw:0,0 到底是什么意思?
六、为什么叫 hw?
七、plughw:0,0 是什么?
八、aplay -l 和 aplay -L 到底有什么区别?
九、举个例子就明白了
十、default 又是什么?
十一、最简单的播放方式
十二、指定具体声卡播放
十三、为什么调试音频时经常使用 aplay?
十四、arecord 是干什么的?
十五、播放和录音其实就是两个方向
Playback
Capture
十六、ALSA PCM API 长什么样?
十七、第一个 ALSA 程序
十八、第一步:snd_pcm_open()
十九、第二步:设置 PCM 参数
二十、第三步:真正把参数交给硬件
二十一、第四步:prepare
二十二、第五步:snd_pcm_writei()
二十三、为什么这里不是 write()?
二十四、Frame 和 snd_pcm_writei() 的关系
二十五、ALSA 播放流程总结
二十六、那 aplay 和我们写的程序有什么关系?
二十七、为什么 aplay 能播放 WAV,却不能直接播放 MP3?
二十八、这一篇最重要的几张图
1. ALSA 播放
2. Card 和 Device
3. hw:0,0
4. PCM API
二十九、遇到“没有声音”,先别急着改代码
三十、下一篇开始进入 ALSA Kernel
系列说明:
这是《从零理解 Linux ALSA、ASoC 到 Qualcomm Audio》系列的第二篇。第一篇我们认识了 PCM、ALSA、Codec 和 ASoC。
这一篇不继续“背概念”,直接上手 Linux 音频。
我们重点搞清楚几个问题:
aplay -l到底在看什么?
aplay -L和aplay -l有什么区别?
hw:0,0到底是什么意思?
plughw:0,0又是什么?ALSA 播放 PCM 的基本流程是什么?
snd_pcm_open()、snd_pcm_hw_params()、snd_pcm_writei()又在干什么?把这些搞明白,后面再看 ALSA Kernel 和 ASoC 就不会那么容易迷路。
一、先准备一台 Linux 机器
本文使用 Linux 环境进行演示。
Ubuntu、Debian、开发板 Linux 都可以。
先检查系统有没有 ALSA 工具:
aplay --version如果能看到版本信息,说明基本没问题。
如果没有安装,可以根据发行版安装 ALSA 工具。
Ubuntu / Debian 通常可以安装:
sudo apt install alsa-utils安装完成后,可以检查:
aplay --version arecord --version amixer --version如果都能正常输出版本信息,就可以继续了。
二、先看看系统到底有没有声卡
执行:
aplay -l注意这里是小写字母:
-l不是数字1。
如果系统识别到了播放设备,可能看到类似:
**** List of PLAYBACK Hardware Devices **** card 0: Device [USB Audio], device 0: USB Audio [USB Audio] Subdevices: 1/1 Subdevice #0: subdevice #0不同机器的结果当然不一样。
比如开发板可能看到:
card 0: xxxPC 可能看到:
card 0: PCH card 1: HDMIUSB 声卡可能又是另外一种名字。
这里最重要的不是记住具体名字。
而是先搞明白:
card device这两个东西。
三、什么是 Card?
card可以简单理解为:
ALSA 眼中的一张声卡。
例如:
card 0表示:
第 0 张声卡如果机器上有两张声卡:
card 0 card 1也就很好理解了。
比如一台电脑可能同时存在:
card 0 → 主板声卡 card 1 → HDMI 音频具体是什么设备,需要看实际系统。
四、什么是 Device?
再看:
device 0它表示这张声卡上的一个音频设备。
可以先粗略理解成:
Card 0 ├── Device 0 ├── Device 1 └── Device 2所以:
card 0和:
device 0不是一回事。
这一点非常重要。
以后你在日志里看到:
card 0, device 0不要把它理解成:
“这是一个设备,名字叫 0。”
实际上是:
Card 0 ↓ Device 0五、hw:0,0到底是什么意思?
如果你以后学习 ALSA,十有八九会看到:
hw:0,0第一次看到可能会想:
这是什么神秘字符串?
其实非常简单。
拆开:
hw:0,0可以理解为:
hw: card 0 device 0也就是:
硬件声卡 0 硬件设备 0所以:
hw:0,0对应:
Card 0 Device 0如果是:
hw:1,0就是:
Card 1 Device 0如果是:
hw:0,1就是:
Card 0 Device 1这下是不是简单多了?
六、为什么叫 hw?
hw可以理解成:
Hardware也就是:
直接访问硬件 PCM 设备。
例如:
aplay -D hw:0,0 test.wav意思就是:
使用 Card 0、Device 0 这个硬件 PCM 设备播放
test.wav。
但是这里有一个坑。
hw对格式要求比较严格。
例如硬件只支持:
48000 Hz 2 Channels 16 bit你却要求:
44100 Hz 2 Channels 16 bit那么可能直接失败。
这时候就轮到:
plughw登场了。
七、plughw:0,0是什么?
先看:
plughw:0,0它和:
hw:0,0最大的区别,可以简单理解成:
hw ↓ 尽量直接使用硬件而:
plughw ↓ 允许 ALSA 做一些格式转换 ↓ 再交给硬件例如:
应用 ↓ 44100 Hz ↓ plughw ↓ 转换 ↓ 48000 Hz ↓ 硬件具体能转换什么,取决于 ALSA 的插件和硬件能力。
所以:
hw更接近:
“我就按你的硬件要求来,别给我乱改。”
而:
plughw更像:
“你这个格式和硬件不完全匹配?行,我帮你转换一下。”
当然,这只是帮助理解的比喻。
八、aplay -l和aplay -L到底有什么区别?
这是 ALSA 初学者非常容易搞混的一对命令。
先执行:
aplay -l它主要查看:
硬件播放设备。
再执行:
aplay -L它查看的是:
ALSA 提供的 PCM 设备名称/PCM 定义。
两者不是一个维度。
可以简单记:
aplay -l ↓ 我机器上有哪些真实的播放硬件?而:
aplay -L ↓ ALSA 给我提供了哪些可以使用的 PCM 名称?九、举个例子就明白了
执行:
aplay -l可能看到:
card 0: PCH device 0: Analog device 1: HDMI这说明:
Card 0 ├── Device 0 → Analog └── Device 1 → HDMI但是:
aplay -L可能出现更多东西:
default sysdefault front surround21 surround40 hw:CARD=PCH,DEV=0 plughw:CARD=PCH,DEV=0 ...为什么-L看起来比-l复杂很多?
因为:
-l主要是在看:
实际硬件设备。
而:
-L是在看:
ALSA 用户空间可以使用的 PCM 名称和插件。
因此不要看到aplay -L输出一大堆名字就开始怀疑人生。
它们很多并不是一张张新的声卡。
十、default又是什么?
你可能还会看到:
default例如:
aplay -D default test.wav这里的default不是:
默认声卡就是 Card 0。
它实际上是 ALSA 提供的一个默认 PCM 配置。
它可能通过 ALSA 配置,把音频最终路由到某个实际设备。
所以:
default和:
hw:0,0不是一回事。
可以先简单理解:
hw:0,0 ↓ 我明确指定硬件设备 default ↓ 你按照系统默认配置帮我找设备后面学习 ALSA 配置文件时,会再深入讲default。
十一、最简单的播放方式
假设当前目录有:
test.wav直接执行:
aplay test.wav如果系统默认音频设备配置正确,那么:
🎵应该就出来了。
这里发生了什么?
可以简单理解为:
test.wav ↓ aplay ↓ ALSA ↓ default PCM ↓ 真实硬件 PCM ↓ 声卡 ↓ Speaker所以aplay其实就是一个非常方便的 ALSA PCM 播放程序。
十二、指定具体声卡播放
假设:
Card 0 Device 0那么可以:
aplay -D hw:0,0 test.wav如果希望使用 ALSA 的格式转换能力:
aplay -D plughw:0,0 test.wav所以:
hw:0,0和:
plughw:0,0在实际调试中非常常见。
尤其当你怀疑:
到底是应用的问题,还是硬件 PCM 的问题?
可以直接绕过很多上层组件,用aplay测一下。
十三、为什么调试音频时经常使用 aplay?
因为它非常简单。
假设 Android Audio 播放不了声音。
你可能要排查:
AudioTrack ↓ AudioFlinger ↓ Audio Policy ↓ Audio HAL ↓ DSP ↓ ALSA ↓ Codec ↓ Speaker这一长串东西。
看得脑壳疼。
但是如果你能直接:
aplay -D hw:0,0 test.wav并且成功播放。
那么至少可以说明:
底层 ALSA PCM 播放链路具备正常工作的可能。
如果aplay都无法播放,那就没有必要第一时间怀疑 AudioTrack。
这就是为什么:
ALSA 命令行工具是音频工程师排查问题时非常重要的工具。
十四、arecord 是干什么的?
既然:
aplay负责播放。
那么:
arecord就是:
录音。
例如:
arecord -l查看录音设备。
如果找到:
card 0 device 0可以尝试:
arecord -D hw:0,0 test.wav然后对着麦克风说:
你好,ALSA。再按:
Ctrl + C结束录音。
然后:
aplay test.wav如果能听到刚才录下来的声音:
🎤 → 💾 → 🔊恭喜。
你已经完成了一次完整的:
Capture → Playback十五、播放和录音其实就是两个方向
现在可以把 ALSA PCM 理解成两个方向。
Playback
Application ↓ ALSA ↓ PCM Buffer ↓ Hardware ↓ SpeakerCapture
Microphone ↓ Hardware ↓ PCM Buffer ↓ ALSA ↓ Application所以:
Playback就是:
数据往硬件走。
而:
Capture就是:
数据从硬件回来。
十六、ALSA PCM API 长什么样?
前面一直在使用:
aplay但是aplay本身也是一个程序。
那么我们自己写程序,能不能直接使用 ALSA?
当然可以。
ALSA 提供了 PCM API。
最简单的播放流程可以理解成:
打开设备 ↓ 设置参数 ↓ 准备设备 ↓ 写入 PCM 数据 ↓ 关闭设备对应的 API 大概是:
snd_pcm_open() snd_pcm_hw_params() snd_pcm_prepare() snd_pcm_writei() snd_pcm_close()这几个函数先记住。
后面看aplay源码时,就会发现:
原来
aplay也是通过这些 API 在干活。
十七、第一个 ALSA 程序
下面写一个非常简单的 ALSA PCM 播放程序。
先说明一下:
这个例子为了学习 ALSA API,代码进行了简化。真正工程中的错误处理、参数协商、XRUN 恢复等内容会复杂一些。
代码如下:
#include <stdio.h> #include <stdlib.h> #include <alsa/asoundlib.h> int main(void) { snd_pcm_t *pcm; snd_pcm_hw_params_t *params; int ret; unsigned int rate = 48000; int channels = 2; ret = snd_pcm_open( &pcm, "hw:0,0", SND_PCM_STREAM_PLAYBACK, 0 ); if (ret < 0) { fprintf(stderr, "无法打开 PCM 设备: %s\n", snd_strerror(ret)); return 1; } snd_pcm_hw_params_alloca(¶ms); snd_pcm_hw_params_any(pcm, params); snd_pcm_hw_params_set_access( pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED ); snd_pcm_hw_params_set_format( pcm, params, SND_PCM_FORMAT_S16_LE ); snd_pcm_hw_params_set_channels( pcm, params, channels ); snd_pcm_hw_params_set_rate_near( pcm, params, &rate, 0 ); ret = snd_pcm_hw_params(pcm, params); if (ret < 0) { fprintf(stderr, "无法设置硬件参数: %s\n", snd_strerror(ret)); snd_pcm_close(pcm); return 1; } printf("ALSA PCM 设备打开成功\n"); printf("Rate: %u Hz\n", rate); printf("Channels: %d\n", channels); snd_pcm_prepare(pcm); /* * 这里应该不断向 PCM 设备写入音频数据。 */ snd_pcm_close(pcm); return 0; }先不要急着研究每一行。
我们只看整个流程。
十八、第一步:snd_pcm_open()
代码:
snd_pcm_open( &pcm, "hw:0,0", SND_PCM_STREAM_PLAYBACK, 0 );这个函数的任务很直白:
打开一个 PCM 设备。
这里最重要的参数是:
"hw:0,0"也就是:
Card 0 Device 0以及:
SND_PCM_STREAM_PLAYBACK表示:
我要播放。
如果是录音,则会使用:
SND_PCM_STREAM_CAPTURE所以:
snd_pcm_open()可以先理解成:
“ALSA,我要打开这个 PCM 设备。”十九、第二步:设置 PCM 参数
接下来:
snd_pcm_hw_params_any(pcm, params);可以理解成:
先获取一份这个 PCM 设备的参数配置。
然后设置:
访问方式 格式 声道 采样率例如:
snd_pcm_hw_params_set_format( pcm, params, SND_PCM_FORMAT_S16_LE );表示:
16 bit Little Endian再比如:
snd_pcm_hw_params_set_channels( pcm, params, 2 );表示:
2 Channels采样率:
snd_pcm_hw_params_set_rate_near( pcm, params, &rate, 0 );表示我们希望使用:
48000 Hz二十、第三步:真正把参数交给硬件
前面只是:
“我想要这些参数。”真正提交参数:
snd_pcm_hw_params(pcm, params);可以简单理解为:
应用 ↓ 我想要: 48000 Hz 16 bit 2 Channels ↓ ALSA ↓ 检查硬件是否支持 ↓ 设置 PCM如果硬件完全不支持这些参数,就可能返回错误。
这就是为什么 ALSA 调试时经常会看到:
Invalid argument或者:
Sample format not available之类的错误。
很多时候就是:
你要求的参数,硬件不接受。
二十一、第四步:prepare
设置好参数之后:
snd_pcm_prepare(pcm);可以简单理解为:
让 PCM 设备进入准备播放的状态。
到这里:
PCM 设备 ↓ 打开了 ↓ 参数设置好了 ↓ 准备好了下一步就可以开始送数据。
二十二、第五步:snd_pcm_writei()
真正播放 PCM 数据时,最核心的函数之一就是:
snd_pcm_writei()例如:
snd_pcm_writei( pcm, buffer, frames );它可以简单理解成:
把 PCM 数据写进 ALSA。
例如:
PCM 数据 ↓ snd_pcm_writei() ↓ ALSA PCM Buffer ↓ DMA ↓ 硬件所以如果你以后在源码里看到:
snd_pcm_writei()脑子里可以直接翻译成:
“往播放设备送 PCM 数据。”
二十三、为什么这里不是 write()?
你可能会问:
Linux 不是有
write()吗?为什么 ALSA 要搞一个snd_pcm_writei()?
因为音频不是简单地:
写几个字节PCM 有自己的概念:
Sample Frame Period Buffer而 ALSA PCM API 需要处理:
采样率
声道
Frame
Buffer
Period
硬件状态
所以它提供了专门的 PCM API。
这也是为什么后面学习 ALSA Kernel 时,我们会看到大量 PCM 相关的数据结构。
二十四、Frame 和snd_pcm_writei()的关系
这里再把前面的 Frame 拉回来。
假设:
16 bit 2 Channels那么一个 Frame:
Left = 16 bit Right = 16 bit也就是:
4 Byte如果:
snd_pcm_writei(pcm, buffer, 1024);这里的:
1024通常表示:
写入 1024 个 Frame。
而不是:
写入 1024 Byte。
这一点非常重要。
所以 ALSA PCM 编程时,一定要区分:
Byte Sample Frame否则很容易把 Buffer 大小算错。
二十五、ALSA 播放流程总结
现在把程序重新整理一下:
snd_pcm_open() ↓ 打开 PCM ↓ snd_pcm_hw_params() ↓ 设置采样率、格式、声道等 ↓ snd_pcm_prepare() ↓ 准备播放 ↓ snd_pcm_writei() ↓ 不断写入 PCM 数据 ↓ 硬件播放 ↓ snd_pcm_close() ↓ 关闭 PCM这就是一个非常典型的 ALSA PCM Playback 流程。
二十六、那 aplay 和我们写的程序有什么关系?
现在再回头看:
aplay test.wav是不是突然感觉没那么神秘了?
aplay本质上也是一个 ALSA 用户空间程序。
它里面也需要完成类似的工作:
打开 PCM ↓ 获取/设置参数 ↓ 准备 PCM ↓ 读取音频数据 ↓ 写入 PCM ↓ 处理播放状态 ↓ 关闭 PCM所以:
aplay可以看成:
一个已经帮你把 ALSA PCM 播放流程封装好的工具。
我们自己写程序,就是把这个过程拆出来看。
二十七、为什么aplay能播放 WAV,却不能直接播放 MP3?
这个问题也很有意思。
因为:
WAV通常可以直接包含 PCM 数据。
而:
MP3是压缩编码格式。
所以:
MP3 ↓ MP3 Decoder ↓ PCM ↓ ALSA ↓ Speaker而 WAV 如果本身就是 PCM:
WAV ↓ PCM ↓ ALSA ↓ Speaker这就是为什么:
ALSA 更关心的是 PCM,而不是 MP3、AAC 这些上层音频编码格式。
这点对于理解 Android Audio 非常重要。
Android 上层可能处理:
MP3 AAC FLAC Opus但最终到了底层播放设备,通常还是要变成 PCM 数据。
二十八、这一篇最重要的几张图
如果你只想记重点,记下面这几张。
1. ALSA 播放
Application ↓ ALSA Library ↓ ALSA PCM ↓ Kernel ↓ Hardware ↓ Speaker2. Card 和 Device
Card 0 ├── Device 0 ├── Device 1 └── Device 23.hw:0,0
hw:0,0 │ │ │ └── Device 0 └──── Card 04. PCM API
snd_pcm_open() ↓ snd_pcm_hw_params() ↓ snd_pcm_prepare() ↓ snd_pcm_writei() ↓ snd_pcm_close()把这几张图理解了,这篇文章的目标基本就完成了。
二十九、遇到“没有声音”,先别急着改代码
音频开发有一个非常经典的场景:
程序运行正常 日志也没有报错 但是: 没声音。这时候很多人第一反应:
“是不是 Audio HAL 有问题?”
然后一头扎进几万行代码。
其实没必要。
可以先从最底层开始测试。
例如:
aplay -l确认播放设备存在。
然后:
aplay -D hw:0,0 test.wav如果不行,再尝试:
aplay -D plughw:0,0 test.wav然后查看:
amixer检查相关 Mixer 控件。
还可以查看内核日志:
dmesg | grep -i audio或者:
dmesg | grep -i snd当然,实际平台的调试方法会更加复杂。
但是一个基本原则值得记住:
音频问题最好按照调用链一层一层排查,而不是上来就猜。
三十、下一篇开始进入 ALSA Kernel
到这里,我们已经站在用户空间看完了 ALSA。
现在可以继续往下钻。
下一篇开始进入真正的 Linux 内核:
Application ↓ ALSA Library ↓ ────────────── Kernel ────────────── ↓ ALSA Core ↓ PCM Core ↓ Sound Driver ↓ Hardware我们会重点回答几个问题:
ALSA Kernel 到底是什么? snd_card 是干什么的? snd_pcm 又是什么? snd_pcm_ops 里面为什么有: .open() .hw_params() .prepare() .trigger() .pointer() aplay 执行以后, 这些函数到底是怎么被调用起来的?再往下一步,就会正式进入 ASoC:
ALSA ↓ ASoC ├── CPU DAI ├── Codec DAI ├── Machine Driver └── DAPM到那时候,我们就开始真正接触 Linux 音频驱动的核心内容了。
先把aplay -l、aplay -L、hw:0,0、plughw:0,0这几个东西搞明白。后面看 ASoC,会轻松很多。