news 2026/4/11 22:02:04

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从0构建WAV文件:读懂计算机文件的本质

从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 / 8
BlockAlign(代码中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文件:

/* by yours.tools - online tools website : yours.tools/zh/htpasswd.html */ #include <bits/stdc++.h> 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<NumSamples;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),底层逻辑都是如此:读取特定规则的二进制 -> 在内存中加工处理 -> 按规则写回二进制。当你不再把文件看作“黑盒”,你便拥有了重塑数字世界的能力。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/11 23:32:21

从夯到拉,锐评13个JavaWeb框架

先叠个甲 以下评价基于技术特性、生态成熟度、市场采用情况等客观维度&#xff0c;不代表对任何框架的贬低。 技术选型应根据具体场景&#xff0c;没有银弹。 不同项目有不同需求&#xff0c;合适的才是最好的。 评价维度说明 性能表现&#xff1a;吞吐量、响应时间、资源占…

作者头像 李华
网站建设 2026/3/31 9:38:26

国外的文献资料在哪里查?实用查询途径与方法指南

刚开始做科研的时候&#xff0c;我一直以为&#xff1a; 文献检索就是在知网、Google Scholar 里反复换关键词。 直到后来才意识到&#xff0c;真正消耗精力的不是“搜不到”&#xff0c;而是—— 你根本不知道最近这个领域发生了什么。 生成式 AI 出现之后&#xff0c;学术检…

作者头像 李华
网站建设 2026/3/26 20:16:58

互联网大厂Java求职面试实战:Spring Boot、微服务与AI技术全方位解析

互联网大厂Java求职面试实战&#xff1a;Spring Boot、微服务与AI技术全方位解析 场景背景 在一家大型互联网公司&#xff0c;严肃的面试官对求职者谢飞机进行了Java开发岗位面试。谢飞机是个幽默的程序员&#xff0c;面对简单问题能够顺利回答&#xff0c;复杂问题则回答略显…

作者头像 李华
网站建设 2026/4/11 15:35:38

Java版LeetCode热题100之下一个排列:深入解析与实战应用

Java版LeetCode热题100之下一个排列&#xff1a;深入解析与实战应用 本文目标&#xff1a;全面、系统地讲解 LeetCode 第31题「下一个排列」&#xff08;Next Permutation&#xff09;&#xff0c;从题目理解、算法推导、代码实现到面试技巧和实际应用场景&#xff0c;帮助你真…

作者头像 李华
网站建设 2026/3/30 15:41:33

飞凌嵌入式ElfBoard-系统信息与资源之休眠

在编写应用程序时多多少少会遇到需要程序等待一会&#xff0c;再执行下面的程序&#xff0c;这时候就需要延时函数来实现这个功能&#xff0c;本节就来介绍延时函数的使用。 一、sleep 用于使程序暂停执行一段时间&#xff0c;通常是以秒为单位。 1.头文件 #include <un…

作者头像 李华
网站建设 2026/4/11 18:17:00

基于YOLOv8的校园安全隐患识别系统研究与实现(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_文章底部可以扫码

基于YOLOv8的校园安全隐患识别系统研究与实现 本文提出并实现了一种基于YOLOv8目标检测算法的校园安全隐患识别系统&#xff0c;可对校园场景中6类典型安全隐患(人员摔倒、交通事故、物品破坏、斗殴行为、火灾、盗窃行为)进行自动识别与实时预警 研究过程中&#xff0c;首先构建…

作者头像 李华