WAV文件结构和PCM数据转存WAV文件
Alsa相关命令
arecord 录制
可以使用arecord录制音频输入,保存为wav文件
arecord -f <格式> -r <采样率> -c <声道数> -d <时长> output.wav例如:
arecord -f S16_LE -r 16000 -c 1 -d 5 output.wav
常用参数说明
-f
:指定音频格式(常用S16_LE
表示 16 位 PCM,WAV 常用格式)-r
:采样率(如44100
Hz,CD 音质)-c
:声道数(1
单声道,2
立体声)-d
:录制时长(单位:秒,省略则手动停止)
aplay 播放
再使用aplay来播放
aplay output.wav
实验环境:
下面这个我是直接在Linux虚拟机上做的实验
WAV文件结构
可以参考这一篇:【音视频 | wav】wav音频文件格式详解——包含RIFF规范、完整的各个块解析、PCM转wav代码
我有一个wav文件,除了LIST段的Chuck,我都做了简单的对照
WAV文件头结构体:
除音频数据外的信息都属于WAV文件头的部分
先定义一下相关结构体,用于解析头部数据
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <vector>
#include <algorithm>
#include <stddef.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>#include <alsa/asoundlib.h>
#include <stdint.h>/*
编译命令: gcc wav_tool.c -o AlsaTool -lasound -lstdc++
使用方法:录音: ./AlsaTool播放: ./AlsaTool 文件名.wav
*/// 固定宽度类型定义
typedef uint16_t WORD;
typedef uint32_t DWORD;
typedef int8_t CHAR;// 结构体定义(按字节对齐)
typedef struct
{WORD wFormatTag; // 编码格式(1=PCM)WORD nChannels; // 声道数DWORD nSamplesPerSec; // 采样率DWORD nAvgBytesPerSec; // 平均字节率WORD nBlockAlign; // 块对齐
} __attribute__((packed)) WAVEFORMAT;typedef struct
{WAVEFORMAT wf;WORD wBitsPerSample; // 采样位数
} __attribute__((packed)) PCMWAVEFORMAT;typedef struct
{CHAR SubchunkID[4]; // 块标识("fact")DWORD SubchunkSize; // 块大小DWORD dwSampleLength; // 采样帧数
} __attribute__((packed)) FACT_CHUNK;typedef struct
{CHAR ChunkID[4]; // 标识("RIFF")DWORD ChunkSize; // 总大小(不含自身ID和大小)CHAR Format[4]; // 格式("WAVE")
} __attribute__((packed)) RIFF_CHUNK;typedef struct
{CHAR SubchunkID[4]; // 标识("data")DWORD SubchunkSize; // 数据大小
} __attribute__((packed)) DATA_CHUNK;// 完整WAV头结构体(RIFF + fmt块 + data块)
typedef struct
{RIFF_CHUNK riff; // RIFF块(12字节)char fmt_id[4]; // fmt块ID("fmt ",4字节)DWORD fmt_size; // fmt块大小(4字节)PCMWAVEFORMAT fmt; // fmt块内容(24字节)DATA_CHUNK data; // data块(8字节)
} __attribute__((packed)) WAV_HEADER; // 总大小:12+4+4+24+8=52字节
解析头部信息的函数
根据上面的结构体,一个一个chuck来解析,适应有额外元素和LIST Chuck的情况
通过参数传出解析出来的采样率,通道数,位深
// 解析WAV文件头
int ParseWav(const char* filename, unsigned int* sample_rate, unsigned short* channels, unsigned short* bits_per_sample)
{if (!sample_rate || !channels || !bits_per_sample) {fprintf(stderr, "参数错误:指针不能为空\n");return -1;}FILE* fp = fopen(filename, "rb");if (!fp) {perror("文件打开失败");return -1;}// 解析RIFF块RIFF_CHUNK riff;size_t read_size = fread(&riff, 1, sizeof(RIFF_CHUNK), fp);if (read_size != sizeof(RIFF_CHUNK)) {fprintf(stderr, "RIFF块读取失败(实际%zu字节,预期%zu字节)\n", read_size, sizeof(RIFF_CHUNK));fclose(fp);return -1;}printf("检测到的标识: ChunkID=%.4s, Format=%.4s\n", riff.ChunkID, riff.Format);if (memcmp(riff.ChunkID, "RIFF", 4) != 0 || memcmp(riff.Format, "WAVE", 4) != 0) {fprintf(stderr, "不是有效的WAV文件\n");fclose(fp);return -1;}// 查找并解析fmt块PCMWAVEFORMAT pcmFmt = {0};FACT_CHUNK fact = {0};int foundFmt = 0, foundFact = 0;CHAR subchunkID[4];DWORD subchunkSize;while (1) {if (fread(subchunkID, 1, 4, fp) != 4 || fread(&subchunkSize, sizeof(DWORD), 1, fp) != 1) {perror("子块信息读取失败");fclose(fp);return -1;}if (memcmp(subchunkID, "fmt ", 4) == 0) {if (subchunkSize < sizeof(PCMWAVEFORMAT)) {fprintf(stderr, "fmt块大小异常(%u字节 < %zu字节)\n", subchunkSize, sizeof(PCMWAVEFORMAT));fclose(fp);return -1;}if (fread(&pcmFmt, sizeof(PCMWAVEFORMAT), 1, fp) != 1) {perror("fmt块内容读取失败");fclose(fp);return -1;}if (subchunkSize > sizeof(PCMWAVEFORMAT)) {fseek(fp, subchunkSize - sizeof(PCMWAVEFORMAT), SEEK_CUR);}// 提取参数*sample_rate = pcmFmt.wf.nSamplesPerSec;*channels = pcmFmt.wf.nChannels;*bits_per_sample = pcmFmt.wBitsPerSample;foundFmt = 1;break;}else if (memcmp(subchunkID, "fact", 4) == 0) {foundFact = 1;memcpy(fact.SubchunkID, subchunkID, 4);fact.SubchunkSize = subchunkSize;if (subchunkSize >= sizeof(DWORD)) {fread(&fact.dwSampleLength, sizeof(DWORD), 1, fp);}if (subchunkSize > sizeof(DWORD)) {fseek(fp, subchunkSize - sizeof(DWORD), SEEK_CUR);}}else {fseek(fp, subchunkSize, SEEK_CUR);}if (ftell(fp) > (long)(riff.ChunkSize + 8)) {fprintf(stderr, "未找到fmt块\n");fclose(fp);return -1;}}// 解析data块DATA_CHUNK dataChunk = {0};int foundData = 0;while (1) {if (fread(dataChunk.SubchunkID, 1, 4, fp) != 4 || fread(&dataChunk.SubchunkSize, sizeof(DWORD), 1, fp) != 1) {perror("data块信息读取失败");fclose(fp);return -1;}if (memcmp(dataChunk.SubchunkID, "data", 4) == 0) {foundData = 1;break;} else if (memcmp(dataChunk.SubchunkID, "fact", 4) == 0 && !foundFact) {foundFact = 1;fact.SubchunkSize = dataChunk.SubchunkSize;if (dataChunk.SubchunkSize >= sizeof(DWORD)) {fread(&fact.dwSampleLength, sizeof(DWORD), 1, fp);}if (dataChunk.SubchunkSize > sizeof(DWORD)) {fseek(fp, dataChunk.SubchunkSize - sizeof(DWORD), SEEK_CUR);}}else {fseek(fp, dataChunk.SubchunkSize, SEEK_CUR);}if (ftell(fp) > (long)(riff.ChunkSize + 8)) {fprintf(stderr, "未找到data块\n");fclose(fp);return -1;}}// 打印头信息printf("===== WAV 文件头信息 =====\n");printf("RIFF 块:\n");printf(" 标识: %.4s\n", riff.ChunkID);printf(" 总大小: %u 字节(不含 RIFF 头)\n", riff.ChunkSize);printf(" 格式: %.4s\n", riff.Format);printf("\nWAVEFORMAT:\n");printf(" 编码格式: %u (%s)\n", pcmFmt.wf.wFormatTag,pcmFmt.wf.wFormatTag == 1 ? "WAVE_FORMAT_PCM" : "非 PCM");printf(" 声道数: %u\n", pcmFmt.wf.nChannels);printf(" 采样率: %u Hz\n", pcmFmt.wf.nSamplesPerSec);printf(" 平均字节率: %u 字节/秒\n", pcmFmt.wf.nAvgBytesPerSec);printf(" 块对齐: %u 字节\n", pcmFmt.wf.nBlockAlign);printf("\nPCMWAVEFORMAT:\n");printf(" 采样位数: %u bits\n", pcmFmt.wBitsPerSample);if (foundFact) {printf("\nfact 块:\n");printf(" 标识: %.4s\n", fact.SubchunkID);printf(" 块大小: %u 字节\n", fact.SubchunkSize);printf(" 总采样帧数: %u\n", fact.dwSampleLength);printf(" 音频时长: %.2f 秒(基于采样帧)\n",(float)fact.dwSampleLength / pcmFmt.wf.nSamplesPerSec);}printf("\ndata 块:\n");printf(" 标识: %.4s\n", dataChunk.SubchunkID);printf(" 数据大小: %u 字节\n", dataChunk.SubchunkSize);if (!foundFact) {printf(" 音频时长: %.2f 秒(估算)\n",(float)dataChunk.SubchunkSize / pcmFmt.wf.nAvgBytesPerSec);}fclose(fp);return 0;
}
设置声卡参数
录制和播放都需要设置声卡
// 设置PCM硬件参数
static int set_hw_params(snd_pcm_t *pcmHandler, unsigned int sample_rate, unsigned short channels, unsigned short bits_per_sample)
{snd_pcm_hw_params_t *hwParams;int ret;int dir;snd_pcm_format_t pcm_format;// 映射采样位数到ALSA格式if (bits_per_sample == 8)pcm_format = SND_PCM_FORMAT_U8;else if (bits_per_sample == 16)pcm_format = SND_PCM_FORMAT_S16_LE;else if (bits_per_sample == 24)pcm_format = SND_PCM_FORMAT_S24_LE;else if (bits_per_sample == 32)pcm_format = SND_PCM_FORMAT_S32_LE;else{fprintf(stderr, "不支持的采样位数: %u bits\n", bits_per_sample);return -1;}snd_pcm_hw_params_malloc(&hwParams);do {ret = snd_pcm_hw_params_any(pcmHandler, hwParams);if (ret < 0) {fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));break;}ret = snd_pcm_hw_params_set_access(pcmHandler, hwParams, SND_PCM_ACCESS_RW_INTERLEAVED);if (ret < 0) {fprintf(stderr, "set_access error: %s\n", snd_strerror(ret));break;}ret = snd_pcm_hw_params_set_format(pcmHandler, hwParams, pcm_format);if (ret < 0) {fprintf(stderr, "set_format error: %s\n", snd_strerror(ret));break;}ret = snd_pcm_hw_params_set_channels(pcmHandler, hwParams, channels);if (ret < 0) {fprintf(stderr, "set_channels error: %s\n", snd_strerror(ret));break;}ret = snd_pcm_hw_params_set_rate_near(pcmHandler, hwParams, &sample_rate, &dir);if (ret < 0) {fprintf(stderr, "set_rate error: %s\n", snd_strerror(ret));break;}printf("实际设置的采样率: %u Hz\n", sample_rate);unsigned int period_time = 10000; // 10ms周期ret = snd_pcm_hw_params_set_period_time_near(pcmHandler, hwParams, &period_time, &dir);if (ret < 0) {fprintf(stderr, "set_period_time error: %s\n", snd_strerror(ret));break;}ret = snd_pcm_hw_params(pcmHandler, hwParams);if (ret < 0) {fprintf(stderr, "apply hw_params error: %s\n", snd_strerror(ret));break;}snd_pcm_hw_params_free(hwParams);return 0;} while (0);snd_pcm_hw_params_free(hwParams);return -1;
}
Record 麦克风输入为WAV文件
因为需要保存在标准的WAV文件中,所以需要先写入头部数据。
写头部数据
写入各个字段
// 写入WAV文件头
void write_wav_header(int fd, unsigned int sample_rate, unsigned short channels, unsigned short bits_per_sample, int duration)
{WAV_HEADER header = {0};DWORD byte_rate = sample_rate * channels * (bits_per_sample / 8);WORD block_align = channels * (bits_per_sample / 8);DWORD data_size = duration * byte_rate; // 预估数据大小// 填充RIFF块memcpy(header.riff.ChunkID, "RIFF", 4);header.riff.ChunkSize = data_size + sizeof(WAV_HEADER) - 8; // 总大小计算memcpy(header.riff.Format, "WAVE", 4);// 填充fmt块memcpy(header.fmt_id, "fmt ", 4);header.fmt_size = sizeof(PCMWAVEFORMAT);header.fmt.wf.wFormatTag = 1; // PCM格式header.fmt.wf.nChannels = channels;header.fmt.wf.nSamplesPerSec = sample_rate;header.fmt.wf.nAvgBytesPerSec = byte_rate;header.fmt.wf.nBlockAlign = block_align;header.fmt.wBitsPerSample = bits_per_sample;// 填充data块memcpy(header.data.SubchunkID, "data", 4);header.data.SubchunkSize = data_size;// 写入完整头部write(fd, &header, sizeof(WAV_HEADER));
}
读声卡数据,存为WAV文件
// 录音函数
int RecordAudio(const char *filename, int duration)
{printf("Calling RecordAudio() filename = %s, duration = %d\n", filename, duration);unsigned int sample_rate = 48000;unsigned short channels = 2;unsigned short bits_per_sample = 16;snd_pcm_t *pcm_handle = NULL;snd_pcm_uframes_t period_size;unsigned char *buff = NULL;unsigned int buf_bytes;int ret = -1;int fd = -1;snd_pcm_hw_params_t *temp_hw_params = NULL;do {// 打开PCM捕获设备ret = snd_pcm_open(&pcm_handle, "hw:0,0", SND_PCM_STREAM_CAPTURE, 0);if (ret < 0) {printf("Open PCM device error: %s\n", snd_strerror(ret));break;}// 设置硬件参数if (set_hw_params(pcm_handle, sample_rate, channels, bits_per_sample) < 0) {snd_pcm_close(pcm_handle);break;}// 获取周期大小snd_pcm_hw_params_malloc(&temp_hw_params);ret = snd_pcm_hw_params_current(pcm_handle, temp_hw_params);if (ret < 0) {fprintf(stderr, "get current hw_params error: %s\n", snd_strerror(ret));break;}snd_pcm_hw_params_get_period_size(temp_hw_params, &period_size, NULL);snd_pcm_hw_params_free(temp_hw_params);// 分配缓冲区buf_bytes = period_size * channels * (bits_per_sample / 8);buff = (unsigned char *)malloc(buf_bytes);if (buff == NULL) {printf("Malloc buffer failed\n");break;}// 打开文件fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);if (fd < 0) {perror("Open file failed");break;}// 写入WAV头write_wav_header(fd, sample_rate, channels, bits_per_sample, duration);// 录音循环unsigned int total_periods = (duration * sample_rate) / period_size;for (unsigned int i = 0; i < total_periods; i++) {ret = snd_pcm_readi(pcm_handle, buff, period_size);if (ret < 0) {fprintf(stderr, "snd_pcm_readi error: %s\n", snd_strerror(ret));ret = snd_pcm_recover(pcm_handle, ret, 0);if (ret < 0) {fprintf(stderr, "Recover PCM failed: %s\n", snd_strerror(ret));break;}continue;}int write_len = ret * channels * (bits_per_sample / 8);if (write(fd, buff, write_len) != write_len) {perror("Write file failed");break;}}// 修正文件头if (fd != -1 && ret >= 0) {off_t total_file_size = lseek(fd, 0, SEEK_END);DWORD real_data_size = total_file_size - sizeof(WAV_HEADER);DWORD real_riff_size = real_data_size + sizeof(WAV_HEADER) - 8;lseek(fd, offsetof(WAV_HEADER, data.SubchunkSize), SEEK_SET);write(fd, &real_data_size, sizeof(real_data_size));lseek(fd, offsetof(WAV_HEADER, riff.ChunkSize), SEEK_SET);write(fd, &real_riff_size, sizeof(real_riff_size));printf("录音完成,实际数据大小: %u 字节\n", real_data_size);}if (ret >= 0) printf("Recording completed successfully.\n");ret = 0;} while (0);// 资源清理if (fd != -1) close(fd);if (buff != NULL) free(buff);if (pcm_handle != NULL) snd_pcm_close(pcm_handle);return ret;
}
播放WAV文件
- 先读取WAV文件头部信息,读出采样率,通道数和位深
- 然后根据读出的数据,设置声卡
- 然后读取文件数据,写到声卡中播放
// 播放函数
int PlayWavFile(const char * cstrFileName)
{printf("Start Play Wav File %s\n", cstrFileName);// 解析WAV参数unsigned int sample_rate;unsigned short channels, bits_per_sample;if (ParseWav(cstrFileName, &sample_rate, &channels, &bits_per_sample) != 0){fprintf(stderr, "WAV文件解析失败,无法播放\n");return -1;}// 打开WAV文件FILE *file = fopen(cstrFileName, "rb");if (!file) {perror("fopen");return 1;}// 定位到音频数据(跳过WAV头)fseek(file, sizeof(WAV_HEADER), SEEK_SET);snd_pcm_t *handle;snd_pcm_hw_params_t *params;snd_pcm_uframes_t period_size;int err;// 打开播放设备err = snd_pcm_open(&handle, "default", SND_PCM_STREAM_PLAYBACK, 0);if (err < 0) {fprintf(stderr, "cannot open audio device %s (%s)\n", "default", snd_strerror(err));fclose(file);return 1;}// 设置播放参数snd_pcm_hw_params_alloca(¶ms);err = snd_pcm_hw_params_any(handle, params);if (err < 0){fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(err));snd_pcm_close(handle);fclose(file);return 1;}if (set_hw_params(handle, sample_rate, channels, bits_per_sample) < 0){snd_pcm_close(handle);fclose(file);return 1;}// 获取周期大小snd_pcm_hw_params_get_period_size(params, &period_size, NULL);unsigned int buf_bytes = period_size * channels * (bits_per_sample / 8);unsigned char *buffer = (unsigned char *)malloc(buf_bytes);if (!buffer) {fprintf(stderr, "malloc failed\n");snd_pcm_close(handle);fclose(file);return 1;}// 播放循环while (1) {size_t read_bytes = fread(buffer, 1, buf_bytes, file);if (read_bytes == 0) {if (feof(file)) {printf("播放完成(已到文件末尾)\n");break;} else {fprintf(stderr, "fread failed\n");break;}}snd_pcm_uframes_t play_frames = read_bytes / (channels * (bits_per_sample / 8));err = snd_pcm_writei(handle, buffer, play_frames);if (err < 0) {fprintf(stderr, "write error: %s\n", snd_strerror(err));err = snd_pcm_recover(handle, err, 0);if (err < 0) {fprintf(stderr, "Recover PCM failed: %s\n", snd_strerror(err));break;}continue;}}// 清理资源free(buffer);snd_pcm_drain(handle);snd_pcm_close(handle);fclose(file);return 0;
}
调用函数
// 检查是否为WAV文件
bool IsAudioFile(const std::string &strFileName)
{size_t pos = strFileName.rfind('.');if (pos == std::string::npos) return false;std::string extension = strFileName.substr(pos);std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);return extension == ".wav";
}int main(int argc, char *argv[])
{if (argc == 1) {RecordAudio("Record.wav", 10); // 录制10秒return 0;} else if (argc == 2) {char cstrArg[256] = {0};strncpy(cstrArg, argv[1], sizeof(cstrArg)-1);if (IsAudioFile(cstrArg)) {PlayWavFile(cstrArg);}else{fprintf(stderr, "仅支持WAV格式文件\n");ShowHelpInfo();}} else {ShowHelpInfo();}return 0;
}