ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ALSA 实战入门:从 aplay、arecord 到 PCM 播放(二)

ALSA 实战入门:从 aplay、arecord 到 PCM 播放(二) 目录一、先准备一台 Linux 机器二、先看看系统到底有没有声卡三、什么是 Card四、什么是 Device五、hw:0,0 到底是什么意思六、为什么叫 hw七、plughw:0,0 是什么八、aplay -l 和 aplay -L 到底有什么区别九、举个例子就明白了十、default 又是什么十一、最简单的播放方式十二、指定具体声卡播放十三、为什么调试音频时经常使用 aplay十四、arecord 是干什么的十五、播放和录音其实就是两个方向PlaybackCapture十六、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 和 Device3. hw:0,04. 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这两个东西。三、什么是 Cardcard可以简单理解为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这下是不是简单多了六、为什么叫 hwhw可以理解成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:CARDPCH,DEV0 plughw:CARDPCH,DEV0 ...为什么-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 理解成两个方向。PlaybackApplication ↓ ALSA ↓ PCM Buffer ↓ Hardware ↓ SpeakerCaptureMicrophone ↓ 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(params); 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 需要处理采样率声道FrameBufferPeriod硬件状态所以它提供了专门的 PCM API。这也是为什么后面学习 ALSA Kernel 时我们会看到大量 PCM 相关的数据结构。二十四、Frame 和snd_pcm_writei()的关系这里再把前面的 Frame 拉回来。假设16 bit 2 Channels那么一个 FrameLeft 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 如果本身就是 PCMWAV ↓ 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 和 DeviceCard 0 ├── Device 0 ├── Device 1 └── Device 23.hw:0,0hw:0,0 │ │ │ └── Device 0 └──── Card 04. PCM APIsnd_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 执行以后 这些函数到底是怎么被调用起来的再往下一步就会正式进入 ASoCALSA ↓ ASoC ├── CPU DAI ├── Codec DAI ├── Machine Driver └── DAPM到那时候我们就开始真正接触 Linux 音频驱动的核心内容了。先把aplay -l、aplay -L、hw:0,0、plughw:0,0这几个东西搞明白。后面看 ASoC会轻松很多。
返回列表