嵌入式 Linux 下 ALSA 音频采集与 PCM 播放流程详解

这篇文章延续前文对 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(&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);

    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(&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, 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 头,拿到:

  • SampleRate
  • NumChannels
  • BitsPerSample

再把 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 数据,稳定地送回应用层。

相关推荐
Magic--2 小时前
Linux静态库与共享库(动态库)详解
linux·运维·服务器
桌面运维家2 小时前
TCP拥塞控制:丢包诊断与Linux网络性能优化
linux·网络·tcp/ip
残雪飞扬2 小时前
Ubuntu上安装 WinBoat(让linux上运行windows软件)
linux·windows·ubuntu
m0_683124792 小时前
无U盘装Ubuntu
linux·运维·ubuntu
默|笙2 小时前
【Linux】进程信号(2)_信号捕捉_中断
linux·运维·服务器
图灵机z2 小时前
【操作系统】四、进程管理
linux·服务器·网络·windows·macos·centos·risc-v
haaaaaaarry3 小时前
【操作系统】第三章 内存管理(一)
linux·考研·操作系统
牛奶咖啡133 小时前
DevOps自动化运维实践_基于Cobbler搭建UEFI网络引导的自动安装平台
linux·运维·自动化·uefi·pxe·uefi网络引导自动安装平台·tftp dhcp 环境搭建
Alphapeople3 小时前
安装华为CANN模型导出工具
linux·运维·服务器