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(¶ms);
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"通常已包含)。