【语音学习记录】WAV 文件读写操作(C++)
WAV 是常见的声音文件格式之一,WAV 是最常见的声音文件格式之一,是微软公司专门为 Windows 开发的一种标准数字音频文件,该文件能记录各种单声道或立体声的声音信息,并能保证声音不失真。但 WAV 文件有一个致命的缺点,就是它所占用的磁盘空间太大(每分钟的音乐大约需要 12 兆磁盘空间)。它符合资源互换文件格式(RIFF)规范,用于保存 Windows 平台的音频信息资源,被 Windows 平台及其应用程序所广泛支持。Wave 格式支持 MSADPCM、CCITT A A A律、CCITT μ \mu μ律和其他压缩算法,支持多种音频位数、采样频率和声道,是 PC 机上最为流行的声音文件格式;但其文件尺寸较大,多用于存储简短的声音片段。
见百度百科
1、WAV 文件结构
WAV 是符合 RIFF 标准的多媒体文件,其文件结构可以如下:
如上图,WAV 文件结构包含共 5 块部分。首先是一个 RIFF 块,指明该文件是符合 RIFF 标准的文件;接着是一个 FourCC,WAVE,该文件为 WAV 文件;fmt 块包含了音频的一些属性:采样率、码率、声道等;fact 块是一个可选块,不是 PCM 数据格式的需要该块;最后 data 块,则包含了音频的 PCM 数据。
实际上,可以将一个 WAV 文件看着由两部分组成:文件头和 PCM 数据,其中 WAV 文件头各字段如下:
偏移地址 | 字节数 | 数据类型 | 字段名称 | 字段说明 |
---|---|---|---|---|
00 H | 4 | 字符 | 文档标识 | 大写字符串 “RIFF”,标明该文件为有效的 RIFF 格式文档 |
04 H | 4 | 长整型 | 文件数据长度 | 从下一个字段首地址开始到文件末尾的总字节数。 |
08 H | 4 | 字符 | 文件格式类型 | 所有 WAV 格式的文件此处为字符串 WAVE,标明该文件是 WAV 格式文件。 |
0C H | 4 | 字符 | 格式块标识 | 小写字符串,“fmt ”。 |
10 H | 4 | 长整型 | 格式块长度 | 其数值不确定,取决于编码格式。可以是 16、18、20、40 等。 |
14 H | 2 | 整型 | 编码格式代码 | 常见的 WAV 文件使用 PCM 脉冲编码调制格式,该数值通常为 1 。 |
16 H | 2 | 整型 | 声道个数 | 单声道为 1 ,立体声或双声道为 2 。 |
18 H | 4 | 长整型 | 采样频率 | 每个声道单位时间采样次数。(常见的有 11025,22050 和 44100 kHZ ) |
1C H | 4 | 长整型 | 数据传输速率 | 该数值为:声道数 * 采样频率 * 每样本的数据位数 / 8 。(播放软件利用此值可以估计缓冲区的大小) |
20 H | 2 | 整型 | 数据块对齐单位 | 采样帧大小。该数值为:声道数 * 位数 / 8 。(播放软件需要一次处理多个该值大小的字节数据) |
22 H | 2 | 整型 | 采样位数 | 存储每个采样值所用的二进制位数。(常见的位数有 4、8、12、16、24、32 ) |
24 H | - | - | - | 该部分是对基本格式快的扩充部分 |
2、下面以 C++ 为例进行文件写入的准备
-
首先,定义文件头结构体:
struct WaveHeader { /* 文件头部分 */ char MainChunk[4]; /* 文档标识 (默认为 “RIFF”)*/ uint32_t LenFile; /* 文件数据长度 (默认为 0)*/ char ChunkType[4]; /* 文件格式类型 (默认为 “WAVE”)*/ char SubChunk[4]; /* 格式块标识 (默认为 “fmt ”)*/ uint32_t LenSubChunk; /* 格式块长度 (默认为 16)*/ uint16_t FormatTag; /* 编码格式代码 (默认为 1)*/ uint16_t Channels; /* 声道个数 (可选值为 1,2)*/ uint32_t SamplesRate; /* 采样频率 */ uint32_t AvgBytesPerSec; /* 数据传输速率 */ uint16_t BlockAlign; /* 数据块对齐单位 */ uint16_t BitsPerSample; /* 采样位数 */ /* PCM 数据部分 */ char DataChunk[4]; /* "data" */ uint32_t DataLen; /* Length of data */ };
-
定义读取数据函数:
/* 定义读取数据函数 */ int read_wav(FILE *input, int *buffer, int readNum) { int readLen = fread(buffer, 2 * sizeof(short), readNum, input); // 结束文件读取,关闭打开的音频文件 cout << "A total of " << readLen << " items of data were obtained by reading the file." << endl; return readLen; }
3、完成上述准备工作后,开始实现 WAV 文件的复写
-
首先,本次测试所使用的 WAV 文件的基本数据如下(使用 Adobe audition 打开获取的信息):
持续时间(s) 采样率(Hz) 声道 位深度 样点数 10.8 44100 单声道 16 476280
-
根据原始文件数据定义复写文件的头文件数据:
uint32_t sr = 44100; uint32_t bits = 16; uint16_t channels = 1; uint32_t samples = 476280 * bits / 8; const WaveHeader WaveHdr = { {'R', 'I', 'F', 'F'}, uint32_t(samples + sizeof(WaveHeader)), {'W', 'A', 'V', 'E'}, {'f', 'm', 't', ' '}, bits, 1, channels, sr, sr * channels * bits / 8, uint16_t(channels * bits / 8), uint16_t(bits), {'d', 'a', 't', 'a'}, samples };
tips:这里有尝试过将结构体 WaveHeader 中的 LenFile 和 DataLen 修改为较大的值,对复写结果并没有影响。
-
打开读入数据文件以及写入数据文件:
string input_path = "数据数据文件路径"; string output_dir = "输出数据文件路径"; // 打开文件用于数据读取 FILE *iFIle = fopen(input_path.c_str(), "rb"); fseek(iFIle, sizeof(WaveHeader), SEEK_SET); // 打开文件用于数据写入 FILE *oFile = fopen(output_dir.c_str(), "wb"); fwrite(&WaveHdr, sizeof(WaveHeader), 1, oFile);
tips:这里需要注意当前所进行的复写过程,仅仅复写文件的 PCM 数据部分,因此输入数据部分需要跳过头文件部分,及代码中使用的
fseek
函数 -
定义一些基本变量,包含
readSize:每次读取的数据数量
、readData:读取到的数据数量
、buffer:用于存放读取到的数据内容
:int readSize = 4096; int readData = readSize; int *buffer = new int[readSize];
-
使用循环,读取文件数据并写入至输出文件中:
for (int i = 0; readData == readSize; ++i) { readData = read_wav(iFIle, buffer, readSize); fwrite(buffer, 2 * sizeof(short), readData, oFile); }
-
源码如下:
#include <iostream> struct WaveHeader { /* 文件头部分 */ char MainChunk[4]; /* 文档标识 (默认为 “RIFF”)*/ uint32_t LenFile; /* 文件数据长度 (默认为 0)*/ char ChunkType[4]; /* 文件格式类型 (默认为 “WAVE”)*/ char SubChunk[4]; /* 格式块标识 (默认为 “fmt ”)*/ uint32_t LenSubChunk; /* 格式块长度 (默认为 16)*/ uint16_t FormatTag; /* 编码格式代码 (默认为 1)*/ uint16_t Channels; /* 声道个数 (可选值为 1,2)*/ uint32_t SamplesRate; /* 采样频率 */ uint32_t AvgBytesPerSec; /* 数据传输速率 */ uint16_t BlockAlign; /* 数据块对齐单位 */ uint16_t BitsPerSample; /* 采样位数 */ /* PCM 数据部分 */ char DataChunk[4]; /* "data" */ uint32_t DataLen; /* Length of data */ }; using namespace std; int read_wav(FILE *iFileReceiver, int *buffer, int readNum) { int readLen = fread(buffer, 2 * sizeof(short), readNum, iFileReceiver); // 结束文件读取,关闭打开的音频文件 cout << "A total of " << readLen << " items of data were obtained by reading the file." << endl; return readLen; } int main() { uint32_t sr = 44100; uint32_t bits = 16; uint16_t channels = 1; uint32_t samples = 476280 * bits / 8; const WaveHeader WaveHdr = { {'R', 'I', 'F', 'F'}, uint32_t(samples + sizeof(WaveHeader)), {'W', 'A', 'V', 'E'}, {'f', 'm', 't', ' '}, bits, 1, channels, sr, sr * channels * bits / 8, uint16_t(channels * bits / 8), uint16_t(bits), {'d', 'a', 't', 'a'}, samples }; string input_path = "数据数据文件路径"; string output_dir = "输出数据文件路径"; // 打开文件用于数据读取 FILE *iFIle = fopen(input_path.c_str(), "rb"); fseek(iFIle, sizeof(WaveHeader), SEEK_SET); // 打开文件用于数据写入 FILE *oFile = fopen(output_dir.c_str(), "wb"); fwrite(&WaveHdr, sizeof(WaveHeader), 1, oFile); int readSize = 4096; int readData = readSize; int *buffer = new int[readSize]; // 读取文件数据并写入至输出文件 for (int i = 0; readData == readSize; ++i) { readData = read_wav(iFIle, buffer, readSize); fwrite(buffer, 2 * bits / 8, readData, oFile); } fclose(oFile); oFile = NULL; return 0; }
4、对于未知数据长度的 WAV 文件写入
对于 WAV 的音频写入数据,很多音频数据是以数据流的形式不断向文件中写入的,这是头文件的文件长度和数据长度就变为了动态的值,在写入这类数据时,可以采用fseek
对头文件的内容进行修改,如下(将复写部分的第五步修改如下即可):
int *buffer = new int[readSize];
// 读取文件数据并写入至输出文件
for (int i = 0; readData == readSize; ++i) {
readData = read_wav(iFIle, buffer, readSize);
fwrite(buffer, 2 * bits / 8, readData, oFile);
}
// 音频文件长度
const uint32_t iFileLength = iBytesWritten - 8;
fseek(fOutput, 4 /* offset */, SEEK_SET /* origin */);
(void)fwrite((const void*)&iFileLength, size_t(4), size_t(1), fOutput);
// 音频数据长度
const uint32_t iDataLength = iBytesWritten - sizeof(CWaveHdr);
fseek(fOutput, 40 /* offset */, SEEK_SET /* origin */);
(void)fwrite((const void*)&iDataLength, size_t(4), size_t(1), fOutput);
fclose(oFile);
oFile = NULL;