1. 使用ALSA库进行PCM录音

任务目标

编写一个C程序,通过ALSA库从默认音频设备采集PCM数据,并封装为WAV文件保存到磁盘。程序需支持指定录音时长,并在结束后正确写入WAV文件头。

环境依赖

  • Linux系统(推荐Ubuntu/Debian)
  • ALSA开发库:sudo apt-get install libasound2-dev
  • 编译工具:gcc

核心实现

程序使用ALSA的snd_pcm_*系列API完成设备打开、参数配置、数据读取,最后将PCM裸数据加上44字节WAV头写入文件。关键点:采样格式为S16_LE(16位小端),采样率44100Hz,单声道,录音时长由命令行参数指定。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <alsa/asoundlib.h>

#define SAMPLE_RATE 44100
#define CHANNELS 1
#define BITS_PER_SAMPLE 16

typedef struct {
    char chunkID[4];     // "RIFF"
    uint32_t chunkSize;
    char format[4];      // "WAVE"
    char subchunk1ID[4]; // "fmt "
    uint32_t subchunk1Size;
    uint16_t audioFormat;
    uint16_t numChannels;
    uint32_t sampleRate;
    uint32_t byteRate;
    uint16_t blockAlign;
    uint16_t bitsPerSample;
    char subchunk2ID[4]; // "data"
    uint32_t subchunk2Size;
} WAVHeader;

void write_wav_header(FILE *fp, uint32_t data_size) {
    WAVHeader hdr;
    memcpy(hdr.chunkID, "RIFF", 4);
    hdr.chunkSize = 36 + data_size;
    memcpy(hdr.format, "WAVE", 4);
    memcpy(hdr.subchunk1ID, "fmt ", 4);
    hdr.subchunk1Size = 16;
    hdr.audioFormat = 1;  // PCM
    hdr.numChannels = CHANNELS;
    hdr.sampleRate = SAMPLE_RATE;
    hdr.byteRate = SAMPLE_RATE * CHANNELS * BITS_PER_SAMPLE / 8;
    hdr.blockAlign = CHANNELS * BITS_PER_SAMPLE / 8;
    hdr.bitsPerSample = BITS_PER_SAMPLE;
    memcpy(hdr.subchunk2ID, "data", 4);
    hdr.subchunk2Size = data_size;

    fwrite(&hdr, sizeof(WAVHeader), 1, fp);
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <output.wav> <seconds>\n", argv[0]);
        return 1;
    }

    const char *filename = argv[1];
    int seconds = atoi(argv[2]);
    if (seconds <= 0) {
        fprintf(stderr, "Seconds must be positive\n");
        return 1;
    }

    snd_pcm_t *handle;
    snd_pcm_hw_params_t *params;
    int rc;

    // 打开默认设备
    rc = snd_pcm_open(&handle, "default", SND_PCM_STREAM_CAPTURE, 0);
    if (rc < 0) {
        fprintf(stderr, "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, CHANNELS);
    snd_pcm_hw_params_set_rate_near(handle, params, &(unsigned int){SAMPLE_RATE}, 0);
    snd_pcm_hw_params_set_period_size_near(handle, params, &(snd_pcm_uframes_t){1024}, 0);

    rc = snd_pcm_hw_params(handle, params);
    if (rc < 0) {
        fprintf(stderr, "Unable to set hw params: %s\n", snd_strerror(rc));
        snd_pcm_close(handle);
        return 1;
    }

    snd_pcm_uframes_t period_size;
    snd_pcm_hw_params_get_period_size(params, &period_size, 0);
    int frame_size = CHANNELS * BITS_PER_SAMPLE / 8;
    int period_bytes = period_size * frame_size;

    FILE *fp = fopen(filename, "wb");
    if (!fp) {
        perror("fopen");
        snd_pcm_close(handle);
        return 1;
    }

    // 先写占位头,录音结束后回填
    write_wav_header(fp, 0);

    char *buffer = malloc(period_bytes);
    if (!buffer) {
        perror("malloc");
        fclose(fp);
        snd_pcm_close(handle);
        return 1;
    }

    int total_frames = SAMPLE_RATE * seconds;
    int frames_recorded = 0;
    uint32_t data_bytes = 0;

    while (frames_recorded < total_frames) {
        int frames_to_read = period_size;
        if (total_frames - frames_recorded < frames_to_read)
            frames_to_read = total_frames - frames_recorded;

        rc = snd_pcm_readi(handle, buffer, frames_to_read);
        if (rc == -EPIPE) {
            snd_pcm_prepare(handle);
            continue;
        } else if (rc < 0) {
            fprintf(stderr, "Read error: %s\n", snd_strerror(rc));
            break;
        }

        fwrite(buffer, frame_size, rc, fp);
        frames_recorded += rc;
        data_bytes += rc * frame_size;
    }

    // 回填正确的数据大小
    fseek(fp, 0, SEEK_SET);
    write_wav_header(fp, data_bytes);

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

    printf("Recording complete: %d frames, %u bytes data\n", frames_recorded, data_bytes);
    return 0;
}

编译与运行

gcc -o alsa_record alsa_record.c -lasound
./alsa_record test.wav 5   # 录制5秒音频

验证方法

  1. 文件头检查:执行 file test.wav,应输出包含 WAVE audio 的描述。
  2. 时长验证:使用 aplay test.wav 播放,应听到约5秒的录音内容(环境需有音频输出设备)。
  3. 数据完整性:用 ls -l test.wav 查看文件大小,应为 44100×2×5 + 44 = 441044 字节(若采样率被驱动调整则按实际值计算)。

常见问题

  • 设备忙:若返回 Device or resource busy,检查是否有其他程序占用声卡,或改用 plughw:0,0 作为设备名。
  • 录音无声:确认系统输入设备(如麦克风)未静音,可用 alsamixer 检查捕获通道。
  • 采样率不匹配:部分声卡不支持44100Hz,程序会自动选择最接近的采样率,可通过 snd_pcm_hw_params_get_rate 获取实际值并调整WAV头。