2. 使用ALSA库进行PCM播放

任务目标

编写一个C程序,读取标准WAV文件(PCM格式),通过ALSA库的snd_pcm_writei接口将音频数据写入声卡设备完成播放。程序需自行解析WAV头,支持常见的16bit/双声道/44100Hz配置,并在播放结束后正确释放资源。

环境依赖

  • Linux系统,内核已启用ALSA驱动
  • 安装ALSA开发库:sudo apt-get install libasound2-dev(Debian/Ubuntu)
  • 编译链接参数:-lasound

核心实现

程序分为三个部分:WAV文件头解析、PCM参数配置与数据写入、资源清理。WAV头解析仅读取必要的44字节标准头(假定为线性PCM,无扩展块),并校验RIFF/WAVE/fmt 标记。ALSA参数通过snd_pcm_hw_params_*系列函数设置,采用阻塞写入模式,按块循环写入直到文件结束。

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

typedef struct {
    char     riff[4];
    uint32_t file_size;
    char     wave[4];
    char     fmt_marker[4];
    uint32_t fmt_chunk_size;
    uint16_t audio_format;
    uint16_t num_channels;
    uint32_t sample_rate;
    uint32_t byte_rate;
    uint16_t block_align;
    uint16_t bits_per_sample;
    char     data_marker[4];
    uint32_t data_size;
} wav_header_t;

static int parse_wav_header(FILE *fp, wav_header_t *hdr) {
    if (fread(hdr, 1, sizeof(wav_header_t), fp) != sizeof(wav_header_t))
        return -1;
    if (memcmp(hdr->riff, "RIFF", 4) || memcmp(hdr->wave, "WAVE", 4) ||
        memcmp(hdr->fmt_marker, "fmt ", 4) || memcmp(hdr->data_marker, "data", 4))
        return -1;
    if (hdr->audio_format != 1) {  /* 仅支持线性PCM */
        fprintf(stderr, "Unsupported audio format: %u\n", hdr->audio_format);
        return -1;
    }
    return 0;
}

static int set_hw_params(snd_pcm_t *handle, const wav_header_t *hdr) {
    snd_pcm_hw_params_t *params;
    snd_pcm_hw_params_alloca(&params);
    if (snd_pcm_hw_params_any(handle, params) < 0) return -1;
    if (snd_pcm_hw_params_set_access(handle, params,
            SND_PCM_ACCESS_RW_INTERLEAVED) < 0) return -1;
    if (snd_pcm_hw_params_set_format(handle, params,
            SND_PCM_FORMAT_S16_LE) < 0) return -1;
    if (snd_pcm_hw_params_set_channels(handle, params,
            hdr->num_channels) < 0) return -1;
    unsigned int rate = hdr->sample_rate;
    if (snd_pcm_hw_params_set_rate_near(handle, params,
            &rate, 0) < 0) return -1;
    if (snd_pcm_hw_params(handle, params) < 0) return -1;
    return 0;
}

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

    FILE *fp = fopen(argv[1], "rb");
    if (!fp) { perror("fopen"); return 1; }

    wav_header_t hdr;
    if (parse_wav_header(fp, &hdr) < 0) {
        fprintf(stderr, "Invalid or unsupported WAV file\n");
        fclose(fp);
        return 1;
    }

    snd_pcm_t *handle;
    if (snd_pcm_open(&handle, "default", SND_PCM_STREAM_PLAYBACK, 0) < 0) {
        fprintf(stderr, "Cannot open PCM device\n");
        fclose(fp);
        return 1;
    }
    if (set_hw_params(handle, &hdr) < 0) {
        fprintf(stderr, "Failed to set hw params\n");
        snd_pcm_close(handle);
        fclose(fp);
        return 1;
    }

    /* 按帧大小分块读取并写入声卡 */
    size_t frame_size = hdr.num_channels * (hdr.bits_per_sample / 8);
    size_t frames_per_buf = 4096;
    size_t buf_size = frames_per_buf * frame_size;
    char *buf = malloc(buf_size);
    if (!buf) {
        snd_pcm_close(handle);
        fclose(fp);
        return 1;
    }

    size_t bytes_read;
    while ((bytes_read = fread(buf, 1, buf_size, fp)) > 0) {
        size_t frames = bytes_read / frame_size;
        snd_pcm_sframes_t ret = snd_pcm_writei(handle, buf, frames);
        if (ret < 0) {
            ret = snd_pcm_recover(handle, ret, 1);
            if (ret < 0) {
                fprintf(stderr, "Write error: %s\n", snd_strerror(ret));
                break;
            }
        }
    }

    snd_pcm_drain(handle);  /* 等待播放完成 */
    free(buf);
    snd_pcm_close(handle);
    fclose(fp);
    printf("Playback finished.\n");
    return 0;
}

编译与运行

gcc -o wav_play wav_play.c -lasound
./wav_play test.wav

验证方法

  • 准备测试文件:ffmpeg -f lavfi -i sine=frequency=440:duration=2 -ar 44100 -ac 2 test.wav(需安装ffmpeg)
  • 运行程序后应听到2秒440Hz正弦音,终端输出Playback finished.
  • 若声卡不支持44100Hz或双声道,程序会因snd_pcm_hw_params失败退出并打印错误信息——此时可改用plughw设备(将"default"替换为"plughw:0,0")以启用ALSA自动转换。
  • 检查退出码:echo $? 返回0表示成功。

常见问题

  • 播放速度异常:确认WAV采样率与sample_rate字段一致,程序已按文件头设置硬件参数。
  • 只有噪声:检查WAV是否为16bit线性PCM(audio_format=1),压缩格式(如ADPCM)不被支持。
  • 设备忙:关闭其他占用声卡的进程,或改用dmix混音设备("default"通常已包含)。