这篇文章延续前文对 PCM/WAV 的分析,进一步落到嵌入式 Linux 实战:在 Linux 平台上,音频数据是如何通过 ALSA 完成采集与播放的?
理解 ALSA 的 PCM 数据流、设备节点、缓冲区、周期、中断唤醒机制,对于做录音、播音、回声消除、音视频同步、声卡驱动适配都非常重要。
一、为什么要理解 ALSA?
在嵌入式 Linux 开发中,只会解析 WAV 文件还远远不够。
因为实际项目里,音频数据往往不是从文件直接来,也不是最终直接保存成文件,而是要经过:
- 麦克风采集
- 编码前预处理
- 网络传输
- 解码后播放
- 声卡驱动输出
在这个过程中,Linux 音频子系统最核心的一层就是 ALSA。
ALSA 全称为:Advanced Linux Sound Architecture
它是 Linux 下标准的音频架构,主要负责:
- 音频设备管理
- 音频采集与播放
- Mixer 控制
- PCM 数据流传输
- 驱动层和用户层之间的接口抽象
对于应用开发者来说,最常接触的是 ALSA-lib 提供的用户态 API ;
对于驱动开发者来说,更关注的是 ASoC / ALSA Driver Framework。
本文重点讨论的是:
用户态程序如何基于 ALSA 完成 PCM 音频采集与播放。
二、ALSA 音频处理在系统中的位置
在嵌入式 Linux 中,音频链路可以简化理解为:
text
应用程序
↓
ALSA-lib
↓
ALSA Kernel Driver
↓
I2S / DMA / Codec / 声卡硬件
↓
麦克风 / 喇叭
如果是播放流程:
text
PCM数据 → ALSA → 声卡驱动 → Codec → 喇叭
如果是采集流程:
text
麦克风 → Codec → 声卡驱动 → ALSA → PCM数据
这里的关键点在于:
- 播放(Playback):应用把 PCM 数据送给 ALSA
- 采集(Capture):应用从 ALSA 读取 PCM 数据
三、ALSA 中的核心概念
3.1 PCM 是什么?
在 ALSA 里,PCM 不是文件格式,而是:
音频采样数据流接口
也就是说,ALSA 的 PCM 接口负责处理的,是最原始的音频采样流。
它通常分两类:
SND_PCM_STREAM_PLAYBACK:播放SND_PCM_STREAM_CAPTURE:采集
3.2 声卡(card)、设备(device)、子设备(subdevice)
ALSA 使用类似下面的方式标识音频硬件:
text
hw:0,0
含义一般是:
0:第 0 块声卡(card0)0:第 0 个 PCM 设备(device0)
有时还能细分到子设备,但应用层最常用的是:
text
default
hw:0,0
plughw:0,0
区别大致如下:
hw:0,0
直接访问硬件设备,不做格式转换。
要求应用设置的采样率、位宽、声道数必须是硬件支持的。
plughw:0,0
在 hw 基础上增加插件层,可以自动做一部分格式转换,比如采样率转换、声道转换。
default
系统默认音频设备,可能经过 dmix、dsnoop 或其他 ALSA 配置重定向。
在嵌入式系统中,为了行为可控,通常更常直接用:
text
hw:0,0
或:
text
plughw:0,0
3.3 帧(frame)与采样点(sample)
这是很多初学者最容易混淆的地方。
sample
一个声道在某一时刻的一个采样值。
frame
某一时刻所有声道的采样值集合。
例如:
- 16bit
- 双声道
那么:
- 左声道一个 sample = 2 字节
- 右声道一个 sample = 2 字节
- 一个 frame = 4 字节
所以:
text
frame_size = channels × bits_per_sample / 8
这和前面 WAV 里说的 BlockAlign 本质一致。
3.4 缓冲区(buffer)与周期(period)
ALSA 传输 PCM 数据时,不是一个采样点一个采样点地处理,而是使用缓冲机制。
buffer
整个环形缓冲区
period
缓冲区中的一个周期块
可以简单理解成:
text
buffer = 多个 period 组成
例如:
- buffer_size = 4096 frames
- period_size = 1024 frames
那么整个缓冲区就可以分成 4 个 period。
为什么需要 period?
因为音频设备通常不会等"所有数据准备完"才工作,而是按周期不断中断/唤醒处理:
- 播放时:设备每消耗完一个 period,就通知应用继续送数据
- 采集时:设备每采满一个 period,就通知应用读取数据
所以音频实际上是:
基于环形缓冲区 + 周期中断机制流动的。
四、ALSA PCM 播放流程
先看播放的总体过程。
4.1 播放流程框图
text
读取WAV/PCM文件
↓
解析音频参数
↓
打开ALSA播放设备
↓
设置硬件参数
↓
设置软件参数(可选)
↓
循环写入PCM数据
↓
播放结束,关闭设备
4.2 播放的主要步骤
第一步:打开 PCM 播放设备
使用 snd_pcm_open() 打开设备:
c
snd_pcm_t *handle = NULL;
snd_pcm_open(&handle, "hw:0,0", SND_PCM_STREAM_PLAYBACK, 0);
这里:
handle:PCM 句柄"hw:0,0":设备名SND_PCM_STREAM_PLAYBACK:表示播放流0:阻塞模式
第二步:分配并初始化硬件参数对象
c
snd_pcm_hw_params_t *hw_params;
snd_pcm_hw_params_alloca(&hw_params);
snd_pcm_hw_params_any(handle, hw_params);
这一步的目的是拿到一份当前设备支持的硬件参数集合,然后逐项设置。
第三步:设置硬件参数
通常至少要设置以下内容:
- 访问方式
- 采样格式
- 声道数
- 采样率
- 周期大小
- 缓冲区大小
例如:
c
/* 设置访问方式
* 访问方式 SND_PCM_ACCESS_RW_INTERLEAVED。这是最常见方式,表示交错存储。
* 比如双声道数据排列为: L R L R L R ...
*/
snd_pcm_hw_params_set_access(handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
/* 设置采样格式
* 采样格式 SND_PCM_FORMAT_S16_LE
* 表示:
* Signed
* 16bit
* Little Endian
*/
snd_pcm_hw_params_set_format(handle, hw_params, SND_PCM_FORMAT_S16_LE);
/* 设置声道数
* 比如:
* 1:单声道
* 2:双声道
*/
snd_pcm_hw_params_set_channels(handle, hw_params, 2);
unsigned int rate = 44100; // 采样率
// 设置采样率
snd_pcm_hw_params_set_rate_near(handle, hw_params, &rate, 0);
snd_pcm_uframes_t period_size = 1024; // 周期大小
// 设置周期大小
snd_pcm_hw_params_set_period_size_near(handle, hw_params, &period_size, 0);
第四步:提交硬件参数
c
snd_pcm_hw_params(handle, hw_params);
这一步之后,参数才真正生效。
第五步:循环写入 PCM 数据
最核心的调用是:
c
snd_pcm_writei(handle, buffer, frames);
它的含义是:
向 PCM 播放设备写入若干帧音频数据
注意这里单位是 frame,不是字节。
例如:
c
while (1) {
int ret = fread(buffer, 1, buf_size, fp);
if (ret <= 0) {
break;
}
snd_pcm_writei(handle, buffer, frames_per_period);
}
第六步:排空并关闭
c
snd_pcm_drain(handle);
snd_pcm_close(handle);
snd_pcm_drain() 表示等待缓冲区中的数据全部播放完成。
五、ALSA PCM 采集流程
采集和播放的思路基本对称,只是方向相反。
5.1 采集流程框图
text
打开ALSA采集设备
↓
设置硬件参数
↓
启动采集
↓
循环读取PCM数据
↓
保存文件/编码/网络发送
↓
停止采集并关闭设备
5.2 采集的主要步骤
第一步:打开采集设备
c
snd_pcm_t *handle = NULL;
snd_pcm_open(&handle, "hw:0,0", SND_PCM_STREAM_CAPTURE, 0);
与播放不同的是流方向:
c
SND_PCM_STREAM_CAPTURE
第二步:配置采集参数
和播放类似,设置:
- 访问方式
- 采样格式
- 声道数
- 采样率
- period_size
- buffer_size
例如:
c
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_channels(handle, hw_params, 1);
unsigned int rate = 16000;
snd_pcm_hw_params_set_rate_near(handle, hw_params, &rate, 0);
语音采集场景里常见参数是:
text
单声道 + 16bit + 16kHz
第三步:循环读取 PCM 数据
采集最核心的调用是:
c
snd_pcm_readi(handle, buffer, frames);
表示从采集设备读取若干帧 PCM 数据。
例如:
c
while (1) {
int ret = snd_pcm_readi(handle, buffer, frames_per_period);
if (ret > 0) {
fwrite(buffer, frame_bytes, ret, fp);
}
}
第四步:保存为 PCM 或 WAV
采集到的数据本质上是裸 PCM。
如果直接保存,就是 .pcm 文件。
如果希望播放器可直接识别,就要在前面补一个 WAV 头。
所以采集应用常见有两种输出:
- 保存为裸 PCM
- 保存为 WAV 文件
六、播放与采集的完整示意
6.1 播放链路
text
WAV文件
↓(解析头)
PCM裸数据
↓
snd_pcm_writei()
↓
ALSA缓冲区
↓
DMA/I2S/Codec
↓
喇叭发声
6.2 采集链路
text
麦克风输入
↓
Codec/ADC
↓
DMA/I2S
↓
ALSA缓冲区
↓
snd_pcm_readi()
↓
PCM裸数据
↓
保存文件/编码发送
七、最小播放示例
下面给一个简化版的播放示例,方便你后续扩展成工程代码。
c
#include <stdio.h>
#include <alsa/asoundlib.h>
int main() {
snd_pcm_t *handle;
snd_pcm_hw_params_t *params;
FILE *fp = fopen("test.pcm", "rb");
if (!fp) {
perror("fopen");
return -1;
}
int rc = snd_pcm_open(&handle, "hw:0,0", SND_PCM_STREAM_PLAYBACK, 0);
if (rc < 0) {
printf("unable to open pcm device: %s\n", snd_strerror(rc));
return -1;
}
snd_pcm_hw_params_alloca(¶ms);
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);
snd_pcm_hw_params(handle, params);
int channels = 2;
int bits = 16;
int frames = 1024;
int frame_bytes = channels * bits / 8;
int buf_size = frames * frame_bytes;
char *buffer = malloc(buf_size);
if (!buffer) {
perror("malloc");
snd_pcm_close(handle);
fclose(fp);
return -1;
}
while (1) {
int n = fread(buffer, 1, buf_size, fp);
if (n <= 0) {
break;
}
int write_frames = n / frame_bytes;
rc = snd_pcm_writei(handle, buffer, write_frames);
if (rc == -EPIPE) {
snd_pcm_prepare(handle);
} else if (rc < 0) {
printf("write error: %s\n", snd_strerror(rc));
}
}
snd_pcm_drain(handle);
snd_pcm_close(handle);
fclose(fp);
free(buffer);
return 0;
}
八、最小采集示例
c
#include <stdio.h>
#include <alsa/asoundlib.h>
int main() {
snd_pcm_t *handle;
snd_pcm_hw_params_t *params;
FILE *fp = fopen("record.pcm", "wb");
if (!fp) {
perror("fopen");
return -1;
}
int rc = snd_pcm_open(&handle, "hw:0,0", SND_PCM_STREAM_CAPTURE, 0);
if (rc < 0) {
printf("unable to open pcm capture device: %s\n", snd_strerror(rc));
return -1;
}
snd_pcm_hw_params_alloca(¶ms);
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, 1);
unsigned int rate = 16000;
snd_pcm_hw_params_set_rate_near(handle, params, &rate, 0);
snd_pcm_hw_params(handle, params);
int channels = 1;
int bits = 16;
int frames = 1024;
int frame_bytes = channels * bits / 8;
int buf_size = frames * frame_bytes;
char *buffer = malloc(buf_size);
if (!buffer) {
perror("malloc");
snd_pcm_close(handle);
fclose(fp);
return -1;
}
while (1) {
rc = snd_pcm_readi(handle, buffer, frames);
if (rc == -EPIPE) {
snd_pcm_prepare(handle);
continue;
} else if (rc < 0) {
printf("read error: %s\n", snd_strerror(rc));
break;
}
fwrite(buffer, frame_bytes, rc, fp);
}
snd_pcm_close(handle);
fclose(fp);
free(buffer);
return 0;
}
九、为什么会出现 underrun 和 overrun?
这是 ALSA 实战中最常见的问题。
9.1 underrun(播放欠载)
播放时,声卡消耗数据的速度比应用写入速度快。
结果就是:
缓冲区空了,但设备还在等数据
这就叫 underrun,通常对应错误:
text
-EPIPE
表现可能是:
- 播放卡顿
- 爆音
- 中断后停止播放
解决方法一般包括:
- 增大 buffer_size
- 增大 period_size
- 提高写入线程优先级
- 减少系统阻塞
- 出错后调用
snd_pcm_prepare()
9.2 overrun(采集溢出)
采集时,设备写入数据的速度比应用读取速度快。
结果就是:
缓冲区满了,但应用还没来得及读走
这就叫 overrun。
表现可能是:
- 录音丢帧
- 声音断裂
snd_pcm_readi()返回错误
解决方法类似:
- 提高读取频率
- 增大缓冲区
- 减少文件 IO 阻塞
- 使用单独线程处理采集数据
十、WAV 播放和 PCM 播放有什么区别?
很多人写 ALSA 播放程序时会混淆这两个概念。
PCM 播放
直接把裸 PCM 数据送入 snd_pcm_writei()
要求你已经知道:
- 采样率
- 声道数
- 位深
WAV 播放
先解析 WAV 头,拿到:
SampleRateNumChannelsBitsPerSample
再把 data 块里的 PCM 数据送给 ALSA。
所以本质上:
ALSA 播放的是 PCM,WAV 只是带头信息的 PCM 文件。
十一、嵌入式项目中的典型应用场景
ALSA PCM 采集与播放在嵌入式项目里非常常见,例如:
- 智能音箱本地录音与播报
- 行车记录仪语音采集
- IPC 摄像机双向对讲
- 工控终端告警播音
- 语音识别前端采集
- VoIP/对讲机音频链路
- 本地音频回放测试工具
很多更复杂的模块,本质上也都是以 ALSA 为底层入口,例如:
- GStreamer 音频源
- FFmpeg 设备采集
- Qt 多媒体底层音频输出
- WebRTC 音频采集适配
十二、调试 ALSA 常用命令
在嵌入式 Linux 上调试音频时,常用下面这些命令。
查看声卡信息
bash
cat /proc/asound/cards
查看 PCM 设备
bash
cat /proc/asound/pcm
查看录音设备
bash
arecord -l
查看播放设备
bash
aplay -l
录音测试
bash
arecord -D hw:0,0 -f S16_LE -r 16000 -c 1 test.wav
播放测试
bash
aplay -D hw:0,0 test.wav
这些命令在排查"设备节点是否正常""参数是否支持""驱动是否工作"时非常有用。
十三、总结
在嵌入式 Linux 中,ALSA 是连接应用与音频硬件的核心桥梁。
无论是录音还是放音,本质都围绕 PCM 数据流展开。
你只要真正理解下面这些点,ALSA 就不再神秘:
- PCM 是 ALSA 传输的核心数据形态
- 播放用
snd_pcm_writei(),采集用snd_pcm_readi() - 参数配置的关键是 采样率、位深、声道数
- ALSA 通过
buffer/period机制完成稳定的数据流传输 underrun/overrun本质都是" 生产消费速度不匹配 "
一句话概括:
ALSA 负责把应用层的 PCM 数据,稳定地送到音频硬件;或者把音频硬件采集到的 PCM 数据,稳定地送回应用层。