4. PCM 数据流播放实战:使用 aplay 播放 WAV 文件

理论讲完了,咱们直接上手。这一章我带你用 aplay 实际播放一个 WAV 文件,把 PCM 数据流从用户态到内核态再到声卡硬件的完整路径走一遍。同时我会把常见的坑和排查方法一并交代清楚。

4.1 aplay 是什么?

aplay 是 ALSA 用户空间工具集里最常用的命令行播放器。它不依赖任何桌面环境,纯命令行操作,非常适合调试和验证音频通路。

我个人习惯在调试新板子时,第一件事就是跑 aplay -l 看看声卡设备有没有被正确识别。如果这一步都过不了,后面全是白搭。

核心定位:aplay 是 ALSA 框架的“Hello World”工具。它直接调用 libasound 库,绕过 PulseAudio 等上层音频服务,能最真实地反映底层 PCM 设备状态。

4.2 基本用法与参数解析

先看最常用的命令形式:

aplay -D hw:0,0 test.wav

这里 -D 指定 PCM 设备名称。hw:0,0 表示第一张声卡(card 0)的第一个设备(device 0)。如果你有 USB 声卡,可能是 hw:1,0。

其他常用参数:

参数 含义 典型值
-l 列出所有声卡和数字设备 —
-L 列出所有 PCM 设备名称(含插件) —
-D 指定 PCM 设备 hw:0,0, plughw:0,0, default
-f 指定采样格式和位宽 S16_LE, S24_3LE, FLOAT_LE
-r 指定采样率(Hz) 44100, 48000, 96000
-c 指定声道数 1, 2, 6
-t 指定文件类型 wav, raw, voc
-v 显示 PCM 参数详情 —

小技巧:加上 -v 参数,aplay 会在播放结束后打印出实际的 PCM 参数,包括 period size、buffer size、采样率等。这是快速验证硬件配置是否生效的好办法。

4.3 播放流程深度拆解

当你敲下 aplay test.wav 回车后,背后发生了什么?我把它拆成 6 个关键步骤:

  1. 解析 WAV 头部:aplay 读取 WAV 文件的 RIFF 头,提取采样率、位宽、声道数、数据长度等信息。
  2. 打开 PCM 设备:调用 snd_pcm_open(),默认使用 default 设备(通常指向 PulseAudio 或 ALSA 的 dmix 插件)。
  3. 设置硬件参数:调用 snd_pcm_hw_params_set_*() 系列函数,配置采样率、格式、声道数、period size、buffer size。
  4. 设置软件参数:调用 snd_pcm_sw_params_set_*(),配置 start threshold、stop threshold、avail min 等。
  5. 启动 PCM 流:调用 snd_pcm_prepare() 和 snd_pcm_start(),或者通过 snd_pcm_writei() 写入足够数据后自动启动。
  6. 循环写入数据:以 period 为单位,反复调用 snd_pcm_writei() 将 PCM 数据送入内核缓冲区,直到文件结束。

嗯,这里要注意:aplay 默认使用 plughw 插件路径,它会自动处理采样率转换和格式转换。如果你用 hw 设备,格式必须完全匹配,否则会报错。

4.4 播放流程可视化

下面这张 SVG 图展示了 PCM 数据从 WAV 文件到声卡硬件的完整流动路径:

PCM 数据流播放流程(aplay) WAV 文件 解析 RIFF 头部 snd_pcm_open() 设置 hw/sw 参数 snd_pcm_writei() 循环 声卡 DAC / 播放 关键点 • period 决定中断频率 • buffer 决定延迟上限 • writei 返回实际写入帧数 常见错误点 • 设备忙(Device or resource busy) • 参数不匹配(Invalid argument) • XRUN(overrun / underrun)

4.5 常见错误与排查方法

我在项目中遇到过不少 aplay 翻车的情况。下面列出最常见的 5 类错误,以及对应的排查思路。

4.5.1 设备不存在或不可用

aplay: main:828: audio open error: No such file or directory

先确认声卡驱动是否加载:

cat /proc/asound/cards
ls /dev/snd/

如果 /dev/snd/ 下没有 pcmC0D0p 之类的设备节点,说明驱动没加载或设备树配置有问题。

4.5.2 设备忙

aplay: main:828: audio open error: Device or resource busy

这通常意味着声卡被其他进程占用了。用 fuser 或 lsof 查一下:

fuser /dev/snd/pcmC0D0p
lsof | grep snd

我曾经在调试一块 I2S 编解码器时,发现 PulseAudio 偷偷占用了设备,导致 aplay 一直报 busy。停掉 PulseAudio 就好了。

4.5.3 参数不匹配

aplay: set_params:1403: Unable to install hw params:
ACCESS:  RW_INTERLEAVED
FORMAT:  S16_LE
...
Broken configuration for this PCM: no configurations available

这说明你请求的参数组合硬件不支持。比如声卡只支持 48kHz,你却传了 44100Hz 的 WAV 文件。解决办法:

  • 使用 plughw 设备,让 ALSA 自动做采样率转换
  • 或者用 -r 参数强制指定硬件支持的采样率

4.5.4 XRUN(欠载/过载)

aplay: pcm_write:2051: write error: Input/output error
underrun!!!

XRUN 是音频调试中最常见的性能问题。说白了就是数据没跟上硬件的节奏。原因可能是:

  • period size 太小,中断太频繁,CPU 来不及响应
  • buffer size 太小,没有足够的缓冲余量
  • 系统负载过高,调度延迟太大

排查方法:用 aplay -v 查看实际使用的 period 和 buffer 参数,然后逐步增大 period size 或 buffer size。

注意:增大 buffer size 会降低 XRUN 概率,但会增加延迟。实时音频应用(如语音通话)需要权衡。非实时播放(如音乐播放)可以适当放宽。

4.5.5 静音或音量异常

有时候 aplay 不报错,但就是没声音。这时候检查三件事:

  1. ALSA 混音器:用 alsamixer 或 amixer 查看 Master、PCM、Headphone 等通道是否静音或音量太低。
  2. 硬件连接:喇叭/耳机是否插对接口?功放是否使能?
  3. DMA 通路:检查 /proc/asound/card0/pcm0p/sub0/status,确认 DMA 确实在搬运数据。

4.6 实战:手动模拟 aplay 的播放流程

为了加深理解,我建议你试试用 C 语言手动调用 ALSA API 播放一个 WAV 文件。核心代码骨架如下:

#include <alsa/asoundlib.h>

snd_pcm_t *handle;
snd_pcm_hw_params_t *hw_params;
snd_pcm_sw_params_t *sw_params;

// 1. 打开设备
snd_pcm_open(&handle, "hw:0,0", SND_PCM_STREAM_PLAYBACK, 0);

// 2. 设置硬件参数
snd_pcm_hw_params_alloca(&hw_params);
snd_pcm_hw_params_any(handle, hw_params);
snd_pcm_hw_params_set_access(handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
snd_pcm_hw_params_set_format(handle, hw_params, SND_PCM_FORMAT_S16_LE);
snd_pcm_hw_params_set_rate_near(handle, hw_params, &rate, 0);
snd_pcm_hw_params_set_channels(handle, hw_params, 2);
snd_pcm_hw_params_set_period_size_near(handle, hw_params, &period_size, 0);
snd_pcm_hw_params_set_buffer_size_near(handle, hw_params, &buffer_size);
snd_pcm_hw_params(handle, hw_params);

// 3. 设置软件参数
snd_pcm_sw_params_alloca(&sw_params);
snd_pcm_sw_params_current(handle, sw_params);
snd_pcm_sw_params_set_start_threshold(handle, sw_params, period_size);
snd_pcm_sw_params(handle, sw_params);

// 4. 循环写入数据
while (frames_to_write > 0) {
    ret = snd_pcm_writei(handle, buffer, frames);
    if (ret == -EPIPE) {
        snd_pcm_prepare(handle);  // 处理 XRUN
    }
    frames_to_write -= ret;
}

// 5. 关闭设备
snd_pcm_drain(handle);
snd_pcm_close(handle);

这段代码虽然简单,但涵盖了 PCM 播放的核心流程。你可以在自己的开发板上编译运行,配合 strace 观察系统调用,效果更直观。

4.7 调试技巧汇总

  • strace:strace -e trace=ioctl aplay test.wav 可以查看 ALSA 的 ioctl 调用序列。
  • ltrace:ltrace -e '*' aplay test.wav 追踪 libasound 库函数调用。
  • /proc/asound:查看 card0/pcm0p/sub0/status 和 hw_params 文件,获取实时状态。
  • dmesg:驱动层的错误信息通常会打印到内核日志。

我的习惯:调试新板子时,我会先跑 speaker-test -t sine -f 1000 生成一个 1kHz 正弦波,确认基本通路没问题,再用 aplay 播放 WAV 文件。这样能快速区分是文件解析问题还是硬件通路问题。