基于 ALSA 实现录音保存为 WAV 文件

一、为什么不能直接把录音数据保存成 WAV?

在 ALSA 采集过程中,通过 snd_pcm_readi() 读到的数据,本质上是 裸 PCM 数据

也就是说,采集到的内容只是:

text 复制代码
[采样点][采样点][采样点]...

这些数据本身并不包含:

  • 采样率
  • 位深
  • 声道数
  • 编码格式
  • 数据长度

因此,如果直接把 ALSA 采集到的数据保存到文件中,这个文件通常只是一个 .pcm 裸数据文件,而不是播放器可直接识别的 .wav 文件。

WAV 文件之所以能被播放器识别,是因为它在 PCM 数据前面增加了一段文件头信息,用于描述音频参数。

所以,基于 ALSA 实现录音保存为 WAV,本质上就是两件事:

  1. 使用 ALSA 采集 PCM 数据
  2. 在文件前面补写一个合法的 WAV 头

二、整体实现思路

整个录音保存为 WAV 的流程可以概括为:

text 复制代码
打开 ALSA 采集设备
        ↓
配置采样参数
        ↓
创建 WAV 文件并预留头部
        ↓
循环读取 PCM 数据
        ↓
将采集数据写入文件
        ↓
录音结束后回填 WAV 头
        ↓
关闭文件和 ALSA 设备

注意这里有一个关键点:

WAV 头里的数据长度字段,在录音开始前通常是未知的。

因为只有录音结束之后,才能知道一共采集了多少字节的 PCM 数据。

所以最常见的做法是:

  • 先写一个"占位头"
  • 录音结束后再回到文件开头,把正确的 WAV 头补进去

三、WAV 文件头结构回顾

标准 PCM-WAV 文件通常由三部分组成:

复制代码
RIFF Header + fmt Chunk + data Chunk + PCM Data

常见头部长度为 44 字节

下面给出一个典型的 WAV 头结构体:

c 复制代码
#pragma pack(1)
typedef struct WAVHeader {
    char riff_id[4];           // "RIFF"
    unsigned int riff_size;    // 文件总长度 - 8
    char wave_id[4];           // "WAVE"

    char fmt_id[4];            // "fmt "
    unsigned int fmt_size;     // 16
    unsigned short audio_format;   // 1: PCM
    unsigned short num_channels;   // 声道数
    unsigned int sample_rate;      // 采样率
    unsigned int byte_rate;        // 每秒字节数
    unsigned short block_align;    // 每帧字节数
    unsigned short bits_per_sample;// 位深

    char data_id[4];           // "data"
    unsigned int data_size;    // PCM 数据长度
} WAVHeader;

字段之间的关系如下:

复制代码
byte_rate   = sample_rate × channels × bits_per_sample / 8
block_align = channels × bits_per_sample / 8
riff_size   = 36 + data_size

四、ALSA 录音流程分析

在 Linux 用户态中,基于 ALSA 采集音频的一般步骤如下:

  1. 打开 PCM 采集设备
  2. 设置硬件参数
  3. 分配采集缓冲区
  4. 循环调用 snd_pcm_readi() 读取数据
  5. 将数据写入文件
  6. 收尾关闭

我们下面直接结合保存 WAV 文件来实现。


五、实现步骤详解


5.1 定义 WAV 头结构与头部写入函数

首先定义 WAV 头结构,并实现一个用于写入头部的函数。

c 复制代码
#include <stdio.h>
#include <string.h>

#pragma pack(1)
typedef struct WAVHeader {
    char riff_id[4];
    unsigned int riff_size;
    char wave_id[4];

    char fmt_id[4];
    unsigned int fmt_size;
    unsigned short audio_format;
    unsigned short num_channels;
    unsigned int sample_rate;
    unsigned int byte_rate;
    unsigned short block_align;
    unsigned short bits_per_sample;

    char data_id[4];
    unsigned int data_size;
} WAVHeader;

void write_wav_header(FILE *fp,
                      int channels,
                      int sample_rate,
                      int bits_per_sample,
                      unsigned int data_size)
{
    WAVHeader header;

    memcpy(header.riff_id, "RIFF", 4);
    header.riff_size = 36 + data_size;
    memcpy(header.wave_id, "WAVE", 4);

    memcpy(header.fmt_id, "fmt ", 4);
    header.fmt_size = 16;
    header.audio_format = 1; // PCM
    header.num_channels = channels;
    header.sample_rate = sample_rate;
    header.bits_per_sample = bits_per_sample;
    header.byte_rate = sample_rate * channels * bits_per_sample / 8;
    header.block_align = channels * bits_per_sample / 8;

    memcpy(header.data_id, "data", 4);
    header.data_size = data_size;

    fseek(fp, 0, SEEK_SET);
    fwrite(&header, sizeof(WAVHeader), 1, fp);
}

5.2 打开 ALSA 采集设备

录音使用的是:

c 复制代码
SND_PCM_STREAM_CAPTURE

典型打开方式如下:

c 复制代码
snd_pcm_t *pcm_handle = NULL;
int rc = snd_pcm_open(&pcm_handle, "hw:0,0", SND_PCM_STREAM_CAPTURE, 0);
if (rc < 0) {
    printf("unable to open pcm device: %s\n", snd_strerror(rc));
    return -1;
}

这里的 "hw:0,0" 表示:

  • 第 0 块声卡
  • 第 0 个 PCM 设备

如果希望系统自动进行部分格式转换,也可以用:

text 复制代码
plughw:0,0

5.3 配置采集参数

在录音场景中,最常见的参数组合之一是:

  • 单声道
  • 16bit
  • 16000Hz

例如用于语音识别前端、对讲、语音控制等。

对应 ALSA 配置如下:

c 复制代码
snd_pcm_hw_params_t *hw_params;
snd_pcm_hw_params_alloca(&hw_params);

snd_pcm_hw_params_any(pcm_handle, hw_params);
snd_pcm_hw_params_set_access(pcm_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
snd_pcm_hw_params_set_format(pcm_handle, hw_params, SND_PCM_FORMAT_S16_LE);
snd_pcm_hw_params_set_channels(pcm_handle, hw_params, 1);

unsigned int sample_rate = 16000;
snd_pcm_hw_params_set_rate_near(pcm_handle, hw_params, &sample_rate, 0);

snd_pcm_uframes_t frames = 1024;
snd_pcm_hw_params_set_period_size_near(pcm_handle, hw_params, &frames, 0);

rc = snd_pcm_hw_params(pcm_handle, hw_params);
if (rc < 0) {
    printf("unable to set hw parameters: %s\n", snd_strerror(rc));
    snd_pcm_close(pcm_handle);
    return -1;
}

参数含义如下:

  • SND_PCM_ACCESS_RW_INTERLEAVED:交错访问方式
  • SND_PCM_FORMAT_S16_LE:16 位有符号小端
  • 1:单声道
  • 16000:16k 采样率
  • 1024:每次读取 1024 帧

5.4 创建 WAV 文件并预留头部

由于 WAV 头要等录音结束后才能确定 data_size,所以一开始通常先写入一个空头占位。

c 复制代码
FILE *fp = fopen("record.wav", "wb");
if (!fp) {
    perror("fopen");
    snd_pcm_close(pcm_handle);
    return -1;
}

/* 先预留 44 字节头部 */
WAVHeader empty_header;
memset(&empty_header, 0, sizeof(WAVHeader));
fwrite(&empty_header, sizeof(WAVHeader), 1, fp);

这样后续采集到的数据会直接写到头部之后。


5.5 循环采集 PCM 数据并写入文件

采集核心调用是:snd_pcm_readi()

它读取的单位是 frame,不是字节。

如果当前参数是:

  • 单声道
  • 16bit

那么:

复制代码
frame_bytes = 1 × 16 / 8 = 2

如果是双声道 16bit,则:

复制代码
frame_bytes = 2 × 16 / 8 = 4

录音循环如下:

c 复制代码
int channels = 1;
int bits_per_sample = 16;
int frame_bytes = channels * bits_per_sample / 8;
int buffer_size = frames * frame_bytes;

char *buffer = (char *)malloc(buffer_size);
if (!buffer) {
    perror("malloc");
    fclose(fp);
    snd_pcm_close(pcm_handle);
    return -1;
}

unsigned int total_data_size = 0;
int seconds = 5; // 录音 5 秒
int loop_count = sample_rate * seconds / frames;

while (loop_count-- > 0) {
    rc = snd_pcm_readi(pcm_handle, buffer, frames);
    if (rc == -EPIPE) {
        /* overrun */
        snd_pcm_prepare(pcm_handle);
        continue;
    } else if (rc < 0) {
        printf("read error: %s\n", snd_strerror(rc));
        break;
    } else if (rc != (int)frames) {
        printf("short read, rc = %d\n", rc);
    }

    int bytes = rc * frame_bytes;
    fwrite(buffer, 1, bytes, fp);
    total_data_size += bytes;
}

这里有几个关键点:

1)为什么要统计 total_data_size

因为 WAV 头中的 data_size 需要在最后回填。

2)为什么会出现 -EPIPE

在采集场景里,这通常表示 overrun(溢出)

说明应用读数据不够及时,ALSA 缓冲区已经被硬件写满。

处理方式通常是:

c 复制代码
snd_pcm_prepare(pcm_handle);

然后继续采集。

3)为什么可能出现 rc != frames

说明本次实际读到的帧数比期望值少,这种情况也需要按实际返回值计算写入字节数。


5.6 录音结束后回填 WAV 头

录音结束后,文件中的 PCM 数据长度已经知道了,这时就可以写入正确的 WAV 头。

c 复制代码
write_wav_header(fp, channels, sample_rate, bits_per_sample, total_data_size);

最后关闭资源:

c 复制代码
free(buffer);
fclose(fp);
snd_pcm_close(pcm_handle);

六、完整示例代码

下面给出一个可直接用于学习和移植的完整示例。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <alsa/asoundlib.h>

#pragma pack(1)
typedef struct WAVHeader {
    char riff_id[4];
    unsigned int riff_size;
    char wave_id[4];

    char fmt_id[4];
    unsigned int fmt_size;
    unsigned short audio_format;
    unsigned short num_channels;
    unsigned int sample_rate;
    unsigned int byte_rate;
    unsigned short block_align;
    unsigned short bits_per_sample;

    char data_id[4];
    unsigned int data_size;
} WAVHeader;

void write_wav_header(FILE *fp,
                      int channels,
                      int sample_rate,
                      int bits_per_sample,
                      unsigned int data_size)
{
    WAVHeader header;

    memcpy(header.riff_id, "RIFF", 4);
    header.riff_size = 36 + data_size;
    memcpy(header.wave_id, "WAVE", 4);

    memcpy(header.fmt_id, "fmt ", 4);
    header.fmt_size = 16;
    header.audio_format = 1;
    header.num_channels = channels;
    header.sample_rate = sample_rate;
    header.bits_per_sample = bits_per_sample;
    header.byte_rate = sample_rate * channels * bits_per_sample / 8;
    header.block_align = channels * bits_per_sample / 8;

    memcpy(header.data_id, "data", 4);
    header.data_size = data_size;

    fseek(fp, 0, SEEK_SET);
    fwrite(&header, sizeof(WAVHeader), 1, fp);
}

int main()
{
    snd_pcm_t *pcm_handle = NULL;
    snd_pcm_hw_params_t *hw_params;
    FILE *fp = NULL;
    int rc;

    int channels = 1;
    int bits_per_sample = 16;
    unsigned int sample_rate = 16000;
    snd_pcm_uframes_t frames = 1024;
    int seconds = 5;

    rc = snd_pcm_open(&pcm_handle, "hw:0,0", SND_PCM_STREAM_CAPTURE, 0);
    if (rc < 0) {
        printf("unable to open pcm device: %s\n", snd_strerror(rc));
        return -1;
    }

    snd_pcm_hw_params_alloca(&hw_params);
    snd_pcm_hw_params_any(pcm_handle, hw_params);
    snd_pcm_hw_params_set_access(pcm_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
    snd_pcm_hw_params_set_format(pcm_handle, hw_params, SND_PCM_FORMAT_S16_LE);
    snd_pcm_hw_params_set_channels(pcm_handle, hw_params, channels);
    snd_pcm_hw_params_set_rate_near(pcm_handle, hw_params, &sample_rate, 0);
    snd_pcm_hw_params_set_period_size_near(pcm_handle, hw_params, &frames, 0);

    rc = snd_pcm_hw_params(pcm_handle, hw_params);
    if (rc < 0) {
        printf("unable to set hw parameters: %s\n", snd_strerror(rc));
        snd_pcm_close(pcm_handle);
        return -1;
    }

    fp = fopen("record.wav", "wb");
    if (!fp) {
        perror("fopen");
        snd_pcm_close(pcm_handle);
        return -1;
    }

    WAVHeader empty_header;
    memset(&empty_header, 0, sizeof(WAVHeader));
    fwrite(&empty_header, sizeof(WAVHeader), 1, fp);

    int frame_bytes = channels * bits_per_sample / 8;
    int buffer_size = frames * frame_bytes;

    char *buffer = (char *)malloc(buffer_size);
    if (!buffer) {
        perror("malloc");
        fclose(fp);
        snd_pcm_close(pcm_handle);
        return -1;
    }

    unsigned int total_data_size = 0;
    int loop_count = sample_rate * seconds / frames;

    while (loop_count-- > 0) {
        rc = snd_pcm_readi(pcm_handle, buffer, frames);
        if (rc == -EPIPE) {
            snd_pcm_prepare(pcm_handle);
            continue;
        } else if (rc < 0) {
            printf("read error: %s\n", snd_strerror(rc));
            break;
        } else if (rc != (int)frames) {
            printf("short read, rc = %d\n", rc);
        }

        int bytes = rc * frame_bytes;
        fwrite(buffer, 1, bytes, fp);
        total_data_size += bytes;
    }

    write_wav_header(fp, channels, sample_rate, bits_per_sample, total_data_size);

    free(buffer);
    fclose(fp);
    snd_pcm_close(pcm_handle);

    printf("record finished, data size = %u bytes\n", total_data_size);
    return 0;
}

七、编译方法

如果系统已经安装 ALSA 开发库,可以使用:

bash 复制代码
gcc record_wav.c -o record_wav -lasound

如果是交叉编译环境,需要确认:

  • 头文件 alsa/asoundlib.h
  • 库文件 libasound.so

已经正确放入工具链和目标系统中。


八、运行效果说明

执行程序后,会进行固定时长录音,例如 5 秒,最终生成:record.wav

这个文件可以直接通过以下命令验证播放:

bash 复制代码
aplay record.wav

如果能正常播放,说明:

  • ALSA 采集成功
  • WAV 头写入正确
  • 录音数据保存无误

九、常见问题分析


9.1 录出来的 WAV 文件无法播放

最常见原因有:

  • WAV 头字段填写错误
  • data_size 不正确
  • riff_size 不正确
  • 采样参数和真实采集参数不匹配
  • 没有在录音结束后回填头部

建议重点检查:

复制代码
riff_size = 36 + data_size
byte_rate = sample_rate × channels × bits_per_sample / 8
block_align = channels × bits_per_sample / 8

9.2 录音过程中频繁出现 overrun

说明应用读取速度慢于设备采集速度。

常见解决方法:

  • 增大 period_size
  • 增大 buffer_size
  • 提高线程优先级
  • 减少磁盘写入阻塞
  • 采集线程和文件写线程分离

如果是实时性要求较高的项目,通常会采用:

  • 一个线程专门从 ALSA 取数据
  • 一个线程负责编码/存盘/发送

9.3 生成的声音速度不对

这通常是因为 WAV 头中的采样率与实际采样率不一致

例如:

  • 实际按 16000Hz 录音
  • 头部却写成 8000Hz

那么播放时声音就会变慢、变调。


9.4 为什么建议先保存 PCM,再考虑 WAV?

在调试阶段,很多工程会先验证:

  1. ALSA 是否成功采到 PCM
  2. 数据长度是否正确
  3. 声音是否正常

确认 PCM 没问题后,再封装成 WAV。

因为这样可以把"采集问题"和"封装问题"拆开排查。


十、总结

基于 ALSA 实现录音保存为 WAV,本质上就是:

用 ALSA 采集 PCM,再为这些 PCM 数据补上标准 WAV 文件头。

它涉及两个核心知识点:

  1. ALSA 采集流程
  2. WAV 文件结构

只要把这两部分打通,录音保存功能就不复杂了。

可以用一句话总结整个流程:
ALSA 负责采集原始 PCM 数据,WAV 负责为 PCM 数据提供标准文件封装。

相关推荐
轻口味18 小时前
HarmonyOS 6 NDK开发系列1:音视频播放能力介绍
华为·音视频·harmonyos
大模型实验室Lab4AI20 小时前
Demystifing Video Reasoning 论文简析
音视频
天上路人21 小时前
A-59F 多功能语音处理模组在本地会议系统扩音啸叫处理中的技术应用与性能分析
人工智能·神经网络·算法·硬件架构·音视频·语音识别·实时音视频
kingcjh971 天前
一、大模型视频生成实战:Wan2.1 本地部署全记录
深度学习·生成对抗网络·ai作画·音视频
枳实-叶1 天前
50 道嵌入式音视频面试题
面试·职场和发展·音视频
淑子啦1 天前
React录制视频和人脸识别
javascript·react.js·音视频
电商API&Tina1 天前
比价 / 选品专用:京东 + 淘宝 核心接口实战(可直接复制运行)
大数据·数据库·人工智能·python·json·音视频
不才小强1 天前
ScreenRecorder 源码分析
音视频
枳实-叶1 天前
音频基础知识
音视频
不才小强1 天前
Windows Graphics Capture (WGC) 屏幕捕获简介
windows·音视频