WAV是Windows平台下常用的音频文件格式,本质上是在PCM原始音频数据前添加一段符合RIFF规范的头部信息,让播放器能够识别音频的采样率、位深、声道数等关键参数。要实现C++将PCM数据保存为WAV文件,核心是正确构造RIFF文件头,再将PCM数据追加到文件头部之后。

WAV文件结构解析
WAV文件整体遵循RIFF(Resource Interchange File Format)规范,主要分为三个部分:
- RIFF块:标识文件类型,包含文件总大小信息
- fmt子块:存储音频格式相关的参数,比如声道数、采样率、位深等
- data子块:存储实际的PCM音频数据
RIFF文件头字段说明
RIFF文件头的各个字段有固定的字节长度和含义,具体对应关系如下:
| 字段名 | 字节数 | 含义 | 取值说明 |
|---|---|---|---|
| ChunkID | 4 | 块标识 | 固定为"RIFF" |
| ChunkSize | 4 | 文件总大小减8 | 后续所有数据的总字节数 |
| Format | 4 | 文件格式 | 固定为"WAVE" |
| Subchunk1ID | 4 | fmt块标识 | 固定为"fmt " |
| Subchunk1Size | 4 | fmt块大小 | 音频格式为PCM时固定为16 |
| AudioFormat | 2 | 音频格式 | PCM格式固定为1 |
| NumChannels | 2 | 声道数 | 1为单声道,2为双声道 |
| SampleRate | 4 | 采样率 | 常见值如44100、48000等 |
| ByteRate | 4 | 每秒字节数 | 采样率*声道数*位深/8 |
| BlockAlign | 2 | 每样本块对齐大小 | 声道数*位深/8 |
| BitsPerSample | 2 | 位深 | 常见值如16、24等 |
| Subchunk2ID | 4 | data块标识 | 固定为"data" |
| Subchunk2Size | 4 | PCM数据大小 | PCM数据的总字节数 |
C++实现PCM转WAV的完整代码
下面给出完整的C++实现代码,包含RIFF文件头结构体定义、文件头构造、PCM数据写入等逻辑:
#include <iostream>
#include <fstream>
#include <cstring>
// RIFF文件头结构体,使用1字节对齐避免编译器自动填充字节
#pragma pack(push, 1)
struct WavHeader {
// RIFF块
char chunkID[4]; // "RIFF"
uint32_t chunkSize; // 文件总大小 - 8
char format[4]; // "WAVE"
// fmt子块
char subchunk1ID[4]; // "fmt "
uint32_t subchunk1Size;// 16(PCM格式固定值)
uint16_t audioFormat; // 1(PCM格式)
uint16_t numChannels; // 声道数
uint32_t sampleRate; // 采样率
uint32_t byteRate; // 每秒字节数
uint16_t blockAlign; // 每样本块对齐大小
uint16_t bitsPerSample;// 位深
// data子块
char subchunk2ID[4]; // "data"
uint32_t subchunk2Size;// PCM数据总字节数
};
#pragma pack(pop)
/**
* 将PCM数据保存为WAV文件
* @param pcmData PCM数据指针
* @param pcmSize PCM数据总字节数
* @param sampleRate 采样率
* @param channels 声道数
* @param bitsPerSample 位深
* @param outputPath 输出WAV文件路径
* @return 保存成功返回true,否则返回false
*/
bool savePcmToWav(const char* pcmData, uint32_t pcmSize,
uint32_t sampleRate, uint16_t channels,
uint16_t bitsPerSample, const char* outputPath) {
// 构造WAV文件头
WavHeader header;
// 设置RIFF块信息
memcpy(header.chunkID, "RIFF", 4);
header.chunkSize = 36 + pcmSize; // 36是fmt和data块头部固定大小之和
memcpy(header.format, "WAVE", 4);
// 设置fmt子块信息
memcpy(header.subchunk1ID, "fmt ", 4);
header.subchunk1Size = 16;
header.audioFormat = 1;
header.numChannels = channels;
header.sampleRate = sampleRate;
header.byteRate = sampleRate * channels * bitsPerSample / 8;
header.blockAlign = channels * bitsPerSample / 8;
header.bitsPerSample = bitsPerSample;
// 设置data子块信息
memcpy(header.subchunk2ID, "data", 4);
header.subchunk2Size = pcmSize;
// 打开输出文件
std::ofstream outFile(outputPath, std::ios::binary);
if (!outFile.is_open()) {
std::cerr << "无法打开输出文件: " << outputPath << std::endl;
return false;
}
// 写入文件头
outFile.write(reinterpret_cast<const char*>(&header), sizeof(WavHeader));
// 写入PCM数据
outFile.write(pcmData, pcmSize);
outFile.close();
return true;
}
int main() {
// 构造一段示例PCM数据,模拟1秒的16位单声道44100采样率音频
uint32_t sampleRate = 44100;
uint16_t channels = 1;
uint16_t bitsPerSample = 16;
uint32_t pcmSize = sampleRate * channels * bitsPerSample / 8; // 1秒数据大小
char* pcmData = new char[pcmSize];
// 填充示例数据,这里用正弦波模拟音频数据
for (uint32_t i = 0; i < pcmSize / 2; ++i) {
// 生成440Hz的正弦波
double value = 32767 * sin(2 * 3.1415926 * 440 * i / (double)sampleRate);
int16_t sample = static_cast<int16_t>(value);
memcpy(pcmData + i * 2, &sample, 2);
}
// 保存为WAV文件
bool result = savePcmToWav(pcmData, pcmSize, sampleRate, channels, bitsPerSample, "output.wav");
if (result) {
std::cout << "WAV文件保存成功" << std::endl;
} else {
std::cout << "WAV文件保存失败" << std::endl;
}
delete[] pcmData;
return 0;
}
注意事项
- 结构体需要使用
#pragma pack(push, 1)设置1字节对齐,避免编译器自动添加填充字节导致文件头格式错误 - 所有多字节字段按照小端序存储,Windows平台下C++的默认字节序就是小端序,不需要额外转换,如果是跨平台开发需要注意字节序问题
- ChunkSize字段的值是整个文件大小减去8,因为ChunkID和ChunkSize本身占用8字节,不包含在这部分大小的计算中
- 如果PCM数据是从其他渠道获取的,需要确保PCM数据的格式参数和构造文件头时传入的参数一致,否则生成的WAV文件播放会出现杂音或者速度异常