开发日志

ALSA 音频子系统:从内核驱动到 PCM 编程

· luo980 · linux · 约 4 分钟阅读

#linux#内核#alsa#音频

Linux 音频这块经常被”PulseAudio 还是 PipeWire”的讨论盖过,但两者其实都是建立在 ALSA 之上的——搞清楚 ALSA 这一层,上面的分歧才看得明白。

两层架构

ALSA(Advanced Linux Sound Architecture)分两层:

  1. 内核驱动层:具体硬件的驱动,比如 snd-hda-intel(大部分主板集成声卡)、 snd-usb-audio(USB 声卡/耳机)。这层负责跟硬件寄存器打交道。
  2. 用户态库 libasound:应用程序实际链接的库,提供 snd_pcm_* 这套 API, 把 ioctl 细节封装掉。

/proc/asound/cards 能看到当前系统识别到的声卡,aplay -l 能看到可用的播放 设备——这些命令行工具本身就是 libasound 的薄封装。

PCM 设备命名:hw / plughw / default 的区别

初学者最容易踩坑的地方:

Terminal window
aplay -L
hw:CARD=PCH,DEV=0 # 直接访问硬件,格式必须跟硬件完全匹配,不做任何转换
plughw:CARD=PCH,DEV=0 # hw 的封装,会自动做采样率/格式转换
default # ALSA 配置文件(.asoundrc / asound.conf)里定义的默认设备,
# 通常经过 dmix 插件支持多进程混音

hw: 设备是独占的、不做任何软件转换——如果你的程序申请 48kHz 但硬件配置成了 44.1kHz,hw: 直接返回错误;plughw: 会在中间插一层重采样帮你转换。这也是为什么 两个程序同时用 hw:0,0 会有一个失败:真硬件设备不支持多路复用,default/dmix 才是允许多进程共享的那一层。

hw_params:协商真正生效的参数

打开一个 PCM 设备之后,实际参数需要协商,不是你指定什么就一定拿到什么:

pcm_playback.c
snd_pcm_t *handle;
snd_pcm_open(&handle, "default", SND_PCM_STREAM_PLAYBACK, 0);
snd_pcm_hw_params_t *params;
snd_pcm_hw_params_alloca(&params);
snd_pcm_hw_params_any(handle, params);
snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED);
snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE);
snd_pcm_hw_params_set_channels(handle, params, 2);
unsigned int rate = 44100;
snd_pcm_hw_params_set_rate_near(handle, params, &rate, 0); // 注意是 "near"
snd_pcm_hw_params(handle, params); // 真正提交协商结果
snd_pcm_writei(handle, audio_buffer, frame_count); // 写入交织的 PCM 数据
snd_pcm_drain(handle); // 等缓冲区播放完再关闭,避免截断尾音

set_rate_near 这个函数名是个提示:ALSA 允许硬件返回”最接近”的采样率而不是精确 匹配,协商完之后一定要读回 rate 变量确认实际值,不能假设申请的就是拿到的——这个 模式跟 V4L2 的 S_FMT 几乎一模一样,都是”申请 → 驱动可能打折 → 读回实际值”。

跟 PulseAudio / PipeWire 的关系

一个常见误解是”PipeWire 取代了 ALSA”——不对,PipeWire(现在大多数发行版的默认 音频服务器)和更早的 PulseAudio 一样,底层仍然通过 ALSA 跟硬件通信,它们解决 的是 ALSA 本身不管的问题:

  • 多个应用同时访问同一个硬件设备(软件混音)
  • 音频路由(把某个应用的输出转到蓝牙耳机)
  • 网络透明(跨机器播放)
  • 对 Vulkan/Video 场景很重要的低延迟/图数据流处理(这是 PipeWire 相对 PulseAudio 的主要设计目标之一,最初就是奔着统一音频 + 视频流处理去的)

应用程序通常不直接调用 snd_pcm_*,而是链接 PulseAudio/PipeWire 提供的客户端库 (或者走 ALSA 的 pulse/pipewire plugin,让老程序”以为”自己还在直接跟 ALSA 对话)。理解这层关系之后,排查”为什么这个程序听不到声音”的问题会顺畅很多—— 先确认 ALSA 层硬件本身工作正常(speaker-test -D hw:0,0),再往上排查路由层。