Vue + Iframe 实战:打造企业级流程配置中心

从0构建WAV文件:读懂计算机文件的本质

虽然接触计算机有一段时间了,但是我的视野一直局限于一个较小的范围之内,往往只能看到于算法竞赛相关的内容,计算机各种文件在我看来十分复杂,认为构建他们并能达到目的是一件困难的事情,然而近期我观看了油管上Magicalbat大神的视频,发现其实它们的本质都惊人地简单:所有计算机文件,都是按特定规则组织的二进制数据,是人为规定好格式再由计算机解析,对于我们来说,只要根据规定格式进行编辑,就能够成功构建。

今天,我们就从最朴素的方式入手,通过手动构建一个WAV音频文件,拆解WAV格式的底层逻辑,同时理解一个核心认知:只要掌握了文件的格式规范,任何类型的文件都能像搭积木一样,一行行代码"拼"出来。

先认识WAV:WAV文件的格式

WAV是微软开发的无损音频格式,相比于压缩后的MP3,它的结构更直白,没有复杂的编码压缩,因此我们能够通过C++文件写入的方式直接完成wav文件的构建,wav文件的核心由三个关键的"数据块(Chunk)"组成:

RIFF块:文件的"身份卡",告诉计算机"我是一个WAV文件";

fmt块:音频的"参数说明",记录采样率、声道数、位深等核心参数;

data块:真正的音频数据,存储着声音的数字信号。

而每个块的内容又如下图所示:

RIFF:

字段名 字节数 数据类型 固定值/计算规则

ChunkID 4 ASCII字符 固定为"RIFF"(无终止符,严格4字节)

ChunkSize 4 32位无符号整数 取值 = 整个WAV文件大小 - 8字节(减去ChunkID和ChunkSize自身的8字节)

Format 4 ASCII字符 固定为"WAVE"(无终止符,严格4字节)

fmt:

字段名 字节数 数据类型 固定值/计算规则

ChunkID 4 ASCII字符 固定为"fmt "(末尾空格,无终止符)

ChunkSize 4 32位无符号整数 PCM编码(最常用)下固定为16(代表后续字段的总字节数,不含ChunkID和ChunkSize)

AudioFormat(代码中Tag) 2 16位无符号整数 编码格式:1=PCM(无压缩,通用);3=IEEE浮点;6=μ律;7=A律等

NumChannels(代码中Chnnels,拼写笔误) 2 16位无符号整数 声道数:1=单声道;2=立体声;>2=多声道

SampleRate 4 32位无符号整数 采样率(每秒采样次数):常见44100Hz(CD音质)、48000Hz、22050Hz等

ByteRate 4 32位无符号整数 每秒音频数据字节数 = SampleRate × NumChannels × BitsPerSample / 8

BlockAlign(代码中BloclAlign,拼写笔误) 2 16位无符号整数 每个"采样帧"的字节数 = NumChannels × BitsPerSample / 8(播放器一次读取的最小单位)

BitsPerSample(代码中BitsperSample) 2 16位无符号整数 采样位深(每个采样点的比特数):8/16/24/32,16位最常用

data:

字段名 字节数 数据类型 固定值/计算规则

ChunkID(代码中DataId) 4 ASCII字符 固定为"data"(无终止符,严格4字节)

DataSize 4 32位无符号整数 音频数据总字节数 = 采样总数 × BlockAlign;采样总数 = SampleRate × 音频时长

音频数据区 可变 二进制流 PCM编码下为线性整数/浮点数:16位位深对应int16_t,8位对应uint8_t,32位浮点对应float

我们接下来的代码,就是严格按照这个模板,把每个部分的二进制数据"写"进文件里。

从零构建WAV:一行代码拆解核心逻辑

下面是完整的C++代码(新手也能看懂),我们逐段拆解,看如何从0生成一个能播放的440Hz正弦波WAV文件:

#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

f32 t = (f32)i/HZ; // 计算当前时间点(秒)

// 生成440Hz正弦波的数值(声音的本质是振动,正弦波模拟声波)

f32 y =sinf(t*440.0f*2.0f*3.1415926f);

// 转换为16位整数(适配16位位深的音频)

i16 sample = (i16)(y*INT16_MAX);

// 写入单个音频样本(2字节)

fwrite(&sample,sizeof(i16),1,fp);

}

fclose(fp); // 关闭文件

return 0;

}

所有文件,都是"按规则写二进制"的产物

写完这段代码,你可能会发现:生成WAV文件的过程,就是"按格式规范往文件里写二进制数据"的过程。而这个逻辑,适用于所有计算机文件:

TXT文档:本质是字符的ASCII/UTF-8编码(比如字符'A'对应二进制01000001),我们按顺序写入这些编码,就成了TXT文件;

BMP图片:由文件头(记录宽、高、位深)+ 像素数据(每个像素的RGB值)组成,按BMP格式写这些数据,就能生成图片;

MP4视频:哪怕是压缩过的视频,也是按MP4的格式规范,把编码后的视频帧、音频帧组织成二进制数据;

EXE可执行文件:遵循PE格式,把指令、数据、资源按规则写入,操作系统就能识别并运行。

计算机之所以能"看懂"不同的文件,不是因为文件有"魔法",而是因为程序员提前约定了"格式规范"------就像我们约定"RIFF"开头的是WAV文件,播放器读到这个标识,就按WAV的规则解析后续数据。

计算机的本质是"朴素的规则"

对刚接触计算机的人来说,各种文件、软件、系统看似复杂,但拆解到最底层,都是"数据+规则"的组合,

只要我们对着格式手册,即便使用最朴素的方式,也能够成功构建出可以使用的音频文件。计算机的世界没有想象中那般复杂,计算机只在乎那最终排好队的 0 和 1。

进一步思考:从文件到软件

了解了各类文件本质,我们自然能理解计算机中各个编辑软件的原理是什么了,就比如今天举的wav的例子,如果我们将示例程序改进一下,加入输入,那么这是否就成了一个简单的音频编辑软件了呢,所有的复杂软件(如 Photoshop、Premiere),底层逻辑都是如此:读取特定规则的二进制 -> 在内存中加工处理 -> 按规则写回二进制。当你不再把文件看作"黑盒",你便拥有了重塑数字世界的能力。秩俣裳退

相关推荐
EOB2OL2ep1 小时前
记录复现多模态大模型论文OPERA的一周工作()
分享
ejTAU1G8E2 小时前
接口测试——pytest框架续集
分享
kAZ4VvwC53 小时前
再次革新 .NET 的构建和发布方式(三)
分享
hO1u6096X3 小时前
Serilog 日志库简单实践(五)数据库 Sinks(.net)
分享
Xe621l7ha6 小时前
揭秘MySL索引分类
分享
HvO9a3WnL6 小时前
对接OpenClaw的常见问题和解决方案
分享
Upg8152276 小时前
单调队列优化多重背包 学习笔记 & 详解
分享
QFzarHV897 小时前
你的SSH密钥可能已经过期了
分享
Go08aMvmB7 小时前
dplyr和tidyr用法
分享