从0构建WAV文件:读懂计算机文件的本质
虽然接触计算机有一段时间了,但是我的视野一直局限于一个较小的范围之内,往往只能看到于算法竞赛相关的内容,计算机各种文件在我看来十分复杂,认为构建他们并能达到目的是一件困难的事情,然而近期我观看了油管上Magicalbat大神的视频,发现其实它们的本质都惊人地简单:所有计算机文件,都是按特定规则组织的二进制数据,是人为规定好格式再由计算机解析,对于我们来说,只要根据规定格式进行编辑,就能够成功构建。
今天,我们就从最朴素的方式入手,通过手动构建一个WAV音频文件,拆解WAV格式的底层逻辑,同时理解一个核心认知:只要掌握了文件的格式规范,任何类型的文件都能像搭积木一样,一行行代码“拼”出来。
先认识WAV:WAV文件的格式
WAV是微软开发的无损音频格式,相比于压缩后的MP3,它的结构更直白,没有复杂的编码压缩,因此我们能够通过C++文件写入的方式直接完成wav文件的构建,wav文件的核心由三个关键的“数据块(Chunk)”组成:
- RIFF块:文件的“身份卡”,告诉计算机“我是一个WAV文件”;
- fmt块:音频的“参数说明”,记录采样率、声道数、位深等核心参数;
- data块:真正的音频数据,存储着声音的数字信号。
而每个块的内容又如下图所示:
RIFF:
字段名字节数数据类型固定值/计算规则ChunkID4ASCII字符固定为"RIFF"(无终止符,严格4字节)ChunkSize432位无符号整数取值 = 整个WAV文件大小 - 8字节(减去ChunkID和ChunkSize自身的8字节)Format4ASCII字符固定为"WAVE"(无终止符,严格4字节)fmt:
字段名字节数数据类型固定值/计算规则ChunkID4ASCII字符固定为"fmt "(末尾空格,无终止符)ChunkSize432位无符号整数PCM编码(最常用)下固定为16(代表后续字段的总字节数,不含ChunkID和ChunkSize)AudioFormat(代码中Tag)216位无符号整数编码格式:1=PCM(无压缩,通用);3=IEEE浮点;6=μ律;7=A律等NumChannels(代码中Chnnels,拼写笔误)216位无符号整数声道数:1=单声道;2=立体声;>2=多声道SampleRate432位无符号整数采样率(每秒采样次数):常见44100Hz(CD音质)、48000Hz、22050Hz等ByteRate432位无符号整数每秒音频数据字节数 = SampleRate × NumChannels × BitsPerSample / 8BlockAlign(代码中BloclAlign,拼写笔误)216位无符号整数每个“采样帧”的字节数 = NumChannels × BitsPerSample / 8(播放器一次读取的最小单位)BitsPerSample(代码中BitsperSample)216位无符号整数采样位深(每个采样点的比特数):8/16/24/32,16位最常用data:
字段名字节数数据类型固定值/计算规则ChunkID(代码中DataId)4ASCII字符固定为"data"(无终止符,严格4字节)DataSize432位无符号整数音频数据总字节数 = 采样总数 × BlockAlign;采样总数 = SampleRate × 音频时长音频数据区可变二进制流PCM编码下为线性整数/浮点数:16位位深对应int16_t,8位对应uint8_t,32位浮点对应float我们接下来的代码,就是严格按照这个模板,把每个部分的二进制数据“写”进文件里。
从零构建WAV:一行代码拆解核心逻辑
下面是完整的C++代码(新手也能看懂),我们逐段拆解,看如何从0生成一个能播放的440Hz正弦波WAV文件:
[code]#include using namespace std;// 类型别名:让代码更易读,明确数据的字节长度#define u32 uint32_t // 32位无符号整数(4字节)#define u16 uint16_t // 16位无符号整数(2字节)#define f32 float // 32位浮点数(4字节)#define i16 int16_t // 16位有符号整数(2字节)#define HZ 44100 // 采样率:每秒采集44100个声音样本(标准音频采样率)#define DURATION 5 // 音频时长:5秒// 1. 定义WAV的三个核心数据块结构(对应格式规范)// RIFF块:文件整体标识struct chunk1{ char ChunkID[4]; // 块标识,固定为"RIFF" u32 ChunkSize; // 从该字段到文件末尾的字节数(总字节数-8) char Format[4]; // 格式类型,固定为"WAVE"}RIFF;// fmt块:音频参数配置struct chunk2{ char ChunkID[4]; // 块标识,固定为"fmt "(注意末尾有空格) u16 Tag; // 编码格式,1代表PCM(无压缩) u32 ChunkSize; // fmt块的大小,PCM格式固定为16 u16 Chnnels; // 声道数:1=单声道,2=立体声 u32 SampleRate; // 采样率 u32 ByteRate; // 每秒数据量 = 采样率×声道数×位深/8 u16 BloclAlign; // 每个采样的总字节数 = 声道数×位深/8 u16 BitsperSample; // 每个采样的位深:16位(常见)}Fmt;// data块:音频数据存储区struct chunk3{ char DataId[4]; // 块标识,固定为"data" u32 DataSize; // 音频数据的总字节数}Data;signed main(int argc,char* argv[]){ // 打开文件:"wb"表示以二进制模式写入(关键!文件本质是二进制) FILE *fp = fopen("test.wav","wb"); // 计算总采样数:采样率×时长(5秒×44100=220500个样本) u32 NumSamples = HZ * DURATION; // 2. 填充RIFF块并写入文件 memcpy(RIFF.ChunkID,"RIFF",4); // 写入块标识 RIFF.ChunkSize = NumSamples*sizeof(u16)+36; // 计算块大小 memcpy(RIFF.Format,"WAVE",4); // 声明为WAVE格式 fwrite(RIFF.ChunkID,sizeof(char),4,fp); // 写入4个字符的ChunkID fwrite(&RIFF.ChunkSize,sizeof(u32),1,fp); // 写入4字节的ChunkSize fwrite(RIFF.Format,sizeof(char),4,fp); // 写入4个字符的Format // 3. 填充fmt块并写入文件 memcpy(Fmt.ChunkID,"fmt ",4); Fmt.ChunkSize = 16; // PCM格式下fmt块固定16字节 Fmt.Tag = 1; // PCM无压缩编码 Fmt.Chnnels = 1; // 单声道 Fmt.SampleRate = HZ; // 44100Hz采样率 Fmt.ByteRate = HZ*sizeof(u16); // 每秒字节数:44100×2=88200 Fmt.BloclAlign = Fmt.Chnnels * sizeof(u16); // 每个采样2字节 Fmt.BitsperSample = 16; // 16位位深 // 按顺序写入fmt块的所有参数(严格遵循格式规范) fwrite(&Fmt.ChunkID,sizeof(char),4,fp); fwrite(&Fmt.ChunkSize,sizeof(u32),1,fp); fwrite(&Fmt.Tag,sizeof(u16),1,fp); fwrite(&Fmt.Chnnels,sizeof(u16),1,fp); fwrite(&Fmt.SampleRate,sizeof(u32),1,fp); fwrite(&Fmt.ByteRate,sizeof(u32),1,fp); fwrite(&Fmt.BloclAlign,sizeof(u16),1,fp); fwrite(&Fmt.BitsperSample,sizeof(u16),1,fp); // 4. 填充data块并写入文件 memcpy(Data.DataId,"data",4); Data.DataSize = NumSamples * sizeof(u16); // 音频数据总字节数 fwrite(&Data.DataId,sizeof(char),4,fp); fwrite(&Data.DataSize,sizeof(u32),1,fp); // 5. 生成音频数据并写入(440Hz正弦波,标准A调) for(int i=0;i 在内存中加工处理 -> 按规则写回二进制。当你不再把文件看作“黑盒”,你便拥有了重塑数字世界的能力。</p>
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |