C++实现PCM数据封装为WAV文件的完整指南
声音设备采集的原始数据通常为PCM格式,直接保存无法被常规播放器识别。本文将详细讲解如何使用C++构造WAV头部结构(Header),通过将头部信息后置写入的方式,将连续的PCM音频流正确封装为标准的WAV文件。
一、实现原理
WAV文件由“头部”和“数据块”组成,头部记录了声道数、采样率、位深等关键参数,而数据块仅包含原始的PCM样本。由于在录制过程中数据的总长度是未知的,因此不能直接在文件开头写入头部。标准的做法如下:
- 预留头部空间:以读写模式打开新文件,文件指针跳过头部大小的位置,先写入PCM数据。
- 记录数据长度:在写入每一个数据块时,累加统计总的数据字节数。
- 回填头部信息:所有数据写入完毕后,关闭文件前,将文件指针移回文件起始处,根据已记录的参数和长度重新计算并写入完整的WAV Header。
二、核心数据结构与操作
要实现这一功能,首先需要定义符合WAV规范的Header结构体,并确保内存对齐。以下是关键步骤的代码解析:
第1步:定义WAV头部结构
C++标准库中的 struct 默认可能存在内存对齐差异,导致生成的文件大小或格式不规范。我们需要手动指定各字段的大小和内容。
//WAV头部结构-PCM格式
struct WavPCMFileHeader
{
struct RIFF {
const char riff[4] = { 'R','I', 'F', 'F' };
uint32_t fileLength; // 剩余文件长度 = 36 + dataSize
const char wave[4] = { 'W','A', 'V', 'E' };
}riff;
struct Format
{
const char fmt[4] = { 'f','m', 't', ' ' };
uint32_t blockSize = 16;
uint16_t formatTag = 1; // PCM类型
uint16_t channels;
uint32_t samplesPerSec;
uint32_t avgBytesPerSec;
uint16_t blockAlign;
uint16_t bitsPerSample;
}format;
struct Data
{
const char data[4] = { 'd','a', 't', 'a' };
uint32_t dataLength;
}data;
};
第2步:创建文件并预留空间
调用 fopen 打开文件后,使用 fseek 跳过 sizeof(WavPCMFileHeader) 字节,确保后续写入的PCM数据紧接着空出的头部区域之后。
// wb+ 表示二进制写入模式,且支持读写以便稍后回填头部
FILE* f = fopen(fileName.c_str(), "wb+");
fseek(f, sizeof(WavPCMFileHeader), SEEK_SET);
第3步:循环写入PCM数据
直接使用 fwrite 将PCM缓冲区的二进制数据写入文件,并在内存中维护一个计数器来累加数据长度。
// data为PCM数据指针, dataLength为本次写入的字节数
fwrite(data, 1, dataLength, f);
_totalDataLength += dataLength;
第4步:回填头部并关闭文件
写入完成后,将文件指针重置到 0 位置,实例化Header结构体填入具体参数(采样率、声道、位深、数据总长),最后 fclose。
// 回到文件头
fseek(f, 0, SEEK_SET);
WavPCMFileHeader h(_channels, _sampleRate, _bitsPerSample, _totalDataLength);
fwrite(&h, 1, sizeof(h), f);
fclose(f);
三、完整工程代码
为了方便复用,我们将上述逻辑封装为一个 C++ 类 WavWapper。
1. 头文件 (WavWapper.h)
#pragma once
#include<string>
namespace AC {
class WavWapper {
public:
WavWapper();
~WavWapper();
/// <summary>
/// 创建wav文件
/// </summary>
/// <param name="fileName">文件名</param>
/// <param name="channels">声道数</param>
/// <param name="sampleRate">采样率,单位hz</param>
/// <param name="bitsPerSample">位深</param>
void CreateWavFile(const std::string &fileName, int channels, int sampleRate, int bitsPerSample);
/// <summary>
/// 写入PCM数据
/// </summary>
void WriteToFile(unsigned char* data, int dataLength);
/// <summary>
/// 关闭文件
/// </summary>
void CloseFile();
private:
void* _file=nullptr;
uint32_t _totalDataLength=0;
int _channels;
int _sampleRate;
int _bitsPerSample;
};
}
2. 实现文件 (WavWapper.cpp)
#include"WavWapper.h"
#include<stdio.h>
namespace AC {
// WAV头部结构-PCM格式定义(同上文)...
struct WavPCMFileHeader {
struct RIFF {
const char riff[4] = { 'R','I', 'F', 'F' };
uint32_t fileLength;
const char wave[4] = { 'W','A', 'V', 'E' };
}riff;
struct Format
{
const char fmt[4] = { 'f','m', 't', ' ' };
uint32_t blockSize = 16;
uint16_t formatTag = 1;
uint16_t channels;
uint32_t samplesPerSec;
uint32_t avgBytesPerSec;
uint16_t blockAlign;
uint16_t bitsPerSample;
}format;
struct Data
{
const char data[4] = { 'd','a', 't', 'a' };
uint32_t dataLength;
}data;
// 构造函数用于初始化Header内容
WavPCMFileHeader(int nCh, int nSampleRate, int bitsPerSample, int dataSize) {
riff.fileLength = 36 + dataSize;
format.formatTag = 1;
format.channels = nCh;
format.samplesPerSec = nSampleRate;
format.avgBytesPerSec = nSampleRate * nCh * bitsPerSample / 8;
format.blockAlign = nCh * bitsPerSample / 8;
format.bitsPerSample = bitsPerSample;
data.dataLength = dataSize;
}
};
WavWapper::WavWapper()
{
}
WavWapper::~WavWapper()
{
CloseFile();
}
void WavWapper::CreateWavFile(const std::string& fileName, int channels, int sampleRate, int bitsPerSample)
{
if (!_file)
{
_channels = channels;
_sampleRate = sampleRate;
_bitsPerSample = bitsPerSample;
_totalDataLength = 0;
_file = fopen(fileName.c_str(), "wb+");
// 预留头部位置
fseek(static_cast<FILE*>(_file), sizeof(WavPCMFileHeader), SEEK_SET);
}
}
void WavWapper::WriteToFile(unsigned char* data, int dataLength)
{
fwrite(data, 1, dataLength, static_cast<FILE*>(_file));
_totalDataLength += dataLength;
}
void WavWapper::CloseFile()
{
if (_file)
{
if (_totalDataLength > 0)
{
// 写入头部信息
fseek(static_cast<FILE*>(_file), 0, SEEK_SET);
WavPCMFileHeader h(_channels, _sampleRate, _bitsPerSample, _totalDataLength);
fwrite(&h, 1, sizeof(h), static_cast<FILE*>(_file));
}
fclose(static_cast<FILE*>(_file));
_file = nullptr;
}
}
}
四、使用示例
以下示例展示了如何创建一个双声道、44.1kHz采样率、16位深度的WAV文件,并模拟写入PCM数据的过程。
#include "WavWapper.h"
#include<Windows.h>
int main()
{
AC::WavWapper ww;
// 创建wav文件:文件名,声道,采样率,位深
ww.CreateWavFile("sound.wav", 2, 44100, 16);
bool flag = true; // 模拟录音标志
while (flag)
{
// 1. 获取PCM数据(此处略过实际采集逻辑)
unsigned char* data = ...
int dataLength = ...;
// 2. 写入PCM数据
ww.WriteToFile(data, dataLength);
// 模拟结束条件
if (...){ flag = false; }
}
// 3. 关闭文件,触发头部写入
ww.CloseFile();
return 0;
}
五、常见问题排查
- 播放器提示格式错误或无法识别:通常是因为 Header 结构体的大小不对。请检查编译器是否插入了额外的对齐字节,建议在结构体前添加
#pragma pack(push, 1)强制单字节对齐,或在 Linux/GCC 环境下确认__attribute__((packed))的使用。 - 文件时长或音量异常:检查传入的
avgBytesPerSec(每秒字节数)和blockAlign(块对齐)计算是否正确。公式为:平均速率 = 采样率 × 声道数 × 位深 ÷ 8。 - 生成的文件尾部有多余乱码:
CloseFile方法必须调用,否则 Header 未回填,部分严格遵循格式的播放器可能拒绝读取整个文件。
以上就是C++将PCM数据封装成WAV文件的详细内容,更多关于C++音频处理技巧的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













