第 10 章 音频怎么”存”进计算机:格式与编码
上一章你看到,一段声音被采样量化后,落进计算机就是一串数字(PCM)。那问题紧接着来了:
这一串数字,直接塞进文件就完事了吗?为什么市面上又有 WAV、MP3、OGG 这么多格式?它们到底差在哪?
这一章解决的就是”同样的数字,怎么装进不同的盒子、占不同的地方”。
10.1 不压缩:WAV / PCM(原始但占地方)
最直白的存法:把上一章那串 PCM 数字,原封不动写进文件,再在文件头写上采样率、位深、声道数这些”说明书”。
这就是 WAV(以及它内部装的 PCM 数据)。它就像把一堆零件连包装箱都不拆直接码进仓库——毫发无损,但占地极大。
回想第 9 章算过:立体声 44.1kHz / 16bit,每秒约 176400 字节 ≈ 172 KB。一首 4 分钟的歌 ≈ 41 MB。一张专辑十几首,轻松上 G。
优点:解码零成本(直接读数字播放),音质完美(无损)。 缺点:太占空间、太占内存、加载慢。
那游戏里能不能全用 WAV?想想第 1 章的”餐厅”——内存和带宽都是金贵的。一个游戏几百个音效,全 WAV 会把内存撑爆、加载卡死。所以 WAV 通常只用在”对音质极敏感且较短”的地方(比如专业音效预览),大部分声音得想办法”瘦身”。
10.1.1 WAV 文件头逐字段拆解:PCM 数组是怎么被包进文件的
我在 9.9 节说过 WAV 头”是一组固定顺序的字段”,现在把它拆给你看。你记住第 9 章那个 samples 数组——它就是 data 块里那一长串原始字节。WAV 文件整体是一个 RIFF 容器,结构像俄罗斯套娃,一层套一层(这种”块”结构叫 chunk):
RIFF 4字节 标记,固定为 "RIFF"
chunkSize 4字节 整个文件剩余字节数(= 文件总大小 - 8)
WAVE 4字节 标记,说明这是 WAV 类型
fmt 4字节 标记,表示"格式说明块"开始
subchunk1Size 4字节 fmt 块的大小(PCM 通常是 16)
audioFormat 2字节 编码格式:1 = PCM(未压缩);其他值=压缩
numChannels 2字节 声道数(1=单声道,2=立体声,6=5.1...)
sampleRate 4字节 采样率(如 44100)
byteRate 4字节 每秒字节数 = sampleRate × numChannels × bitDepth/8
blockAlign 2字节 每帧字节数 = numChannels × bitDepth/8
bitsPerSample 2字节 位深(如 16)
data 4字节 标记,表示"采样数据块"开始
subchunk2Size 4字节 PCM 数据总字节数
[采样数据...] 这里就是 9.6.1 那个 int16 数组,原样躺在这儿
你看,字段byteRate、blockAlign 都是可以从 sampleRate / numChannels / bitsPerSample 推出来的”冗余信息”——它们存在的意义是让老式播放器不用自己算,直接读。而 data 块里那串字节,和第 9 章 Python 造出的 samples 列表是同一份数据的两种视图:在 Python 里它是 int16 列表,在文件里它是按小端字节序排好的原始字节。
这正是整章的落点:PCM 数组本身从 9 章到 10 章一字未改,变的只是”外面那层盒子”。WAV 是”连盒子都不拆”的盒子;MP3 是”把数组压缩后再装盒”的盒子。你读懂了 WAV 头,就读懂了所有音频容器——它们无非是”先写说明书、再写数据”的变体。
10.2 有损压缩:MP3 / OGG(扔掉你听不见的)
瘦身的核心思路,叫感知压缩(Perceptual Coding)。它建立在一条心理学事实上:
人耳不是万能录音机。有些声音,你”物理上存在”却”听不见”——比如被更响的声音盖住的、频率极端的、变化太快的。
于是压缩算法干一件事:分析信号,把那些”你听不见或不太听得出来”的部分,大刀阔斧地扔掉,只保留你耳朵真正在乎的。
- MP3:最早的明星,靠”心理声学模型”砍掉不敏感频段。128kbps 的 MP3 体积只有 WAV 的约 1/10,普通人听着”差不多”。
- OGG / Vorbis:开源、效率常比同码率 MP3 略好,游戏圈偏爱(免版权费)。Unity 里音效常用
.ogg或.wav,背景音乐常压成 OGG。
关键认知:MP3/OGG 是有损(Lossy) 的——扔掉的信息回不来,再解压也恢复不到原始 PCM。所以叫”损”。但你耳朵基本原谅了它,换来 10 倍体积缩小。
对照:照片的 JPEG 也是同一思路(扔掉眼睛不敏感的高频细节)。你后面会发现,“利用人类感知的弱点来压缩”是多媒体领域最通用的省钱大招。
10.2.1 心理声学原理:为什么”有损”你却听不出来(掩蔽效应)
“扔掉听不见的”听起来玄,其实有一条很具体的原理叫听觉掩蔽(Auditory Masking)。我用一个生活画面讲清它:
想象你在安静的图书馆里,旁边有人用很小的声音念叨,你能听清。可如果同一时刻,舞台上一支摇滚乐队在轰鸣,那个”小声念叨”就彻底被淹没了——你不是听不到它,而是它被更响的声音盖住了,大脑自动忽略。这就是同时掩蔽(频域掩蔽):一个强音会让它附近频率的弱音”听不见”。
还有时域掩蔽:一声巨响过后的一小段时间里(几十毫秒),你听不清紧接着出现的更弱的声音——强音”余震”盖住了弱音。
MP3/OGG 的编码器内部就跑这样一个”心理声学模型”:它把声音切成一小段一小段,算出”当前这一刻,哪些频率、哪些时刻人耳会被掩蔽而听不见”,然后只保留不被掩蔽的部分,被掩蔽的部分直接量化成 0(相当于删除)。这比”无脑降精度”聪明得多——它删的是”你注定听不到”的信息,所以解压回来你毫无察觉。
这直接解释了 10.2 那句”为什么多数人感觉差不多”:丢的不是随机信息,而是被精确预测为’你听不见’的信息。对比一下:如果不用掩蔽、只是粗暴地把位深从 16bit 砍到 4bit,你立刻听到刺耳底噪——那叫”失真”,不是”有损压缩”。两者的差别,就在于有没有用到”人耳的弱点”这道金矿。
10.3 容器 vs 编码:文件和”里面的东西”是两回事
这里有个容易混的点,必须掰清:
- 编码(Codec, 编码解码器):指的是”怎么把 PCM 数字压缩 / 还原”的算法。MP3、OGG、AAC 是编码。
- 容器(Container):指的是”把编码后的数据、元数据、可能还有多路音轨,打包成一个文件的格式”。比如
.mp3文件既是容器也是编码标识;而.ogg是个容器,里面通常装 Vorbis 或 Opus 编码。
类比:编码是”怎么把衣服叠小”(真空压缩袋法),容器是”外面的旅行箱”(登机箱 / 行李箱)。同一个压缩袋法,可以装进不同箱子;同一个箱子,也能装不同叠法。
游戏引擎读音频时,干的是:拆箱(解析容器)→ 解压(按编码还原成 PCM)→ 送声卡播放。你作为开发者,主要关心”文件后缀(容器)“和”引擎支不支持这个编码”。
10.4 游戏里的现实账本:流式加载 vs 一次性载入
声音进了游戏,还有一道工程抉择,直接对应”内存账本”:
- 一次性载入(Decompress on Load):把整个音频解压成 PCM 塞进内存,播放时直接读。优点:播放零延迟、可任意拖拽进度。缺点:占内存。适合短音效(开枪、脚步),几十毫秒就播完,常驻内存无妨。
- 流式加载(Streaming):文件留在硬盘,播放时一边读硬盘一边解压一边播,像看在线视频。优点:几乎不占内存。缺点:有轻微延迟、不能随便往后拖。适合长背景音乐(BGM),一首几分钟,全载入内存太亏。
Unity 里 AudioClip 的 Load Type 就是这三个选项:Decompress On Load / Compressed In Memory / Streaming。你选哪个,本质是用”内存”换”加载快/可拖动”还是反过来——又是第 1 章那句”把贵的提前做掉、运行时拼装”的变体:短音效值得提前解压换实时性,长音乐值得流式换内存。
10.4.1 流式 vs 整段加载:连回你懂的内存与 IO 账本
我把这笔账翻成你写后端时的语言,你会立刻通透。一个 4 分钟 44.1kHz/16bit 立体声 BGM,原始 PCM 约 41 MB(见 10.1)。如果选 Decompress On Load,这 41 MB 的 PCM 数组要常驻内存——相当于你服务里缓存了一个 41 MB 的大对象,且游戏运行时它不能被释放(因为随时可能循环播放)。一百个这样的 BGM?内存直接爆。
而 Streaming 的做法,你在工程上等价于是”边读文件边消费”的流式处理(streaming):声卡像一个消费者,按播放进度从磁盘文件里”逐块”读取、解码、播放,内存里只保留一个小小的环形缓冲区(ring buffer,几 KB 到几百 KB)。这正是你后端处理大文件下载时”不要一次性 readAll 进内存,而要边读边写”的同一思路——用磁盘 IO 的”慢但无限”,去换内存的”快但有限”。
代价是什么?开篇那一瞬间要去磁盘”够”数据,所以会有几毫秒到几十毫秒的启动延迟;而且你没法像拖进度条那样瞬间跳到 3 分 20 秒——因为那一段还没读进来。所以短音效(几十毫秒播完、要随机触发、要零延迟)走 Decompress On Load,长 BGM 走 Streaming,本质是按”单次时长 × 触发频率”来选 IO 策略,和后端按”对象大小 × 访问频率”决定缓存还是直读,是同一个脑子。
10.4.2 不同平台的编解码坑:iOS / Android 对压缩格式的支持差异
你以为”选了 OGG 就万事大吉”?到了真机上有坑。音频编解码分两类:硬件解码和软件解码。
- iOS:系统对 AAC 有专门的硬件解码单元,播放 AAC 几乎不耗 CPU;但对 OGG/Vorbis 没有硬件支持,只能靠软件解码,会多吃 CPU、增加耗电和发热。所以 iOS 上背景音乐常倾向 AAC(或让 Unity 在导入时转成平台友好格式)。
- Android:阵营复杂,硬件编解码能力随芯片(高通/联发科/三星)差异很大;OGG/Vorbis 在 Android 上软件解码支持普遍,但某些低端机解压高压缩音频仍会卡顿。
Unity 的”平台覆盖(Platform Override)“机制就是为这准备的:同一个 AudioClip,你可以在 iOS 覆盖成 AAC、在 Android 覆盖成 Vorbis,引擎打包时各给各的格式。这背后的思想你一定熟:同一份逻辑,不同运行环境的”最优实现”不同,要做平台适配。和你在后端针对不同数据库写不同 SQL 方言,是同一种工程判断。
10.5 一张表收尾
| 格式 | 压不压 | 体积 | 音质 | 游戏里常见用途 |
|---|---|---|---|---|
| WAV (PCM) | 不压 | 巨大 | 无损 | 极短、极敏感音效;中间素材 |
| MP3 | 有损 | 小 | 好(多数人听不出) | 过场语音、BGM(移动端常见) |
| OGG/Vorbis | 有损 | 小 | 同码率常略优于 MP3 | Unity 音效、BGM(免版权) |
| AAC | 有损 | 小 | 优 | 视频伴音、iOS 生态 |
记住这条主线:连续声波 → 采样量化成 PCM(无损、占地方)→ 按感知压缩成 MP3/OGG(省地方、有损)→ 打包进容器 → 游戏里按”短则载入、长则流式”上账本。
每一环,都是为了对付”实时 + 有限资源”这个老问题。
10.6 Unity 里怎么选:一张决策表
把第 9、10 章合起来,落到你每天要做出的选择:
| 音频用途 | 推荐格式 / Load Type | 理由 |
|---|---|---|
| 短音效(枪声/脚步/UI) | WAV 或 OGG + Decompress On Load | 短,常驻内存无妨;要零延迟、可随机触发 |
| 长 BGM | OGG / MP3 + Streaming | 长,全载入内存太亏;流式省内存 |
| 过场语音 | OGG / MP3(压缩) | 体积敏感,轻微延迟可接受 |
| 需精确循环的音效 | WAV(无压缩循环点最干净) | 压缩可能破坏循环接缝 |
记住:选格式 = 在”体积 / 内存 / 延迟 / 音质”四个旋钮里取舍,不是”越高级越好”。
10.7 为什么手机游戏偏爱 OGG 而不是 MP3
不是音质问题,是授权:MP3 的专利池历史上有 licensing 费用,OGG/Vorbis 完全开源免费。移动端量产游戏为避免法律和分成麻烦,默认选 OGG。桌面/主机则 MP3、OGG、AAC 都常见。技术选型背后常有非技术因素——这是你作为工程师会反复遇到的现实。
10.8 无损压缩(FLAC / ALAC)游戏里为什么少见
你可能听过 FLAC”无损还压缩”。它确实比 WAV 小(约一半)且音质零损。但游戏少用,原因直白:
- 解压要算力:实时解 FLAC 比直接读 WAV 多一道 CPU 开销,对大量音效不划算。
- 省下的体积不如有损多:FLAC 约 50%,MP3/OGG 可达 90%。对”听感差不多”的游戏音频,90% 的节省更香。
所以 FLAC 多在音乐制作/归档场景,游戏运行时仍是有损当道。又一次:实时约束压倒”完美”。
10.9 码率(kbps):你手里调音质最直接的那个旋钮
前面反复出现 128kbps、172KB/s 这种数,我单独拎出来说清它是什么。码率(Bitrate)就是”每秒钟这段音频占用多少比特”,单位 kbps(千比特每秒)。它和你在第 9 章算的”每秒原始字节数”是同一类概念的两种形态:
- 原始 PCM 的码率是固定的、由采样率×声道×位深算出(如 44.1kHz/16bit/立体声 = 1411 kbps)。它和”内容”无关——无论你录的是交响乐还是静音,每秒都是这么多比特。
- 压缩后的码率是可调的”目标”。你告诉编码器”我要 128kbps”,它就尽量把每帧压到平均 128kbps 对应的体积。内容越复杂(乐器多、动态大),越接近这个上限;安静段会省一点。
于是码率成了你调音质最直观的手柄,且直接关联文件大小(文件大小 ≈ 码率 × 时长)。常见档位直觉:
- 64–96 kbps:语音清晰,音乐发糊,适合 NPC 对话、无线电音效。
- 128–192 kbps:多数玩家听不出和 CD 的差别,是游戏 BGM 甜点区。
- 256–320 kbps:接近透明,体积也大,只在”音乐类游戏”或对音质洁癖的场景用。
你作为后端一定懂这个 trade-off:码率就是”精度 vs 体积”的旋钮,和图片压缩的 quality 参数、视频的 CRF 是同一个家族。调它前先问自己:这段声音玩家用心听吗?时长多久?要常驻内存还是流式?——答案决定你该拧到哪一档。
10.9.1 CBR、VBR:码率也可以”自适应”
MP3/OGG 还有两种控码率的方式,值得你知道,因为它反映了”均匀 vs 按需”的工程取舍:
- CBR(恒定码率):每一帧都压到相同大小。优点是实现简单、流式播放时缓冲区行为可预测(每秒固定消耗这么多);缺点是”安静段也占满比特、复杂段却不够用”,浪费又不讨好。
- VBR(可变码率):复杂段给高码率、简单段给低码率,整体平均到目标。优点是同等听感下体积更小;缺点是文件总大小不那么精确可控,且极早期某些硬件播放器不支持。
这又回到第 1 章那句”按需分配”的老话:CBR 像”每个请求都给固定配额”,VBR 像”按实际负载弹性伸缩”。你选哪种,取决于你更在乎”可预测”还是”更省”。
10.10 一个音效从导入到播放的完整链路(把全章串起来)
最后我带你把一条真实的游戏音频链路走一遍,把 9、10 两章所有概念落到一个具体的动作上。假设你在 Unity 里拖进一个 gunshot.wav:
- 导入时:Unity 读它的 WAV 头(10.1.1 那堆字段),知道它是 44.1kHz/16bit/立体声。你在 Inspector 里选了 OGG + Decompress On Load。
- 打构建包时:Unity 用 OGG 编码器(10.2 的感知压缩,靠掩蔽效应丢信息)把它压成
.ogg,体积从几十 KB 降到几 KB。 - 游戏启动时:因为选了 Decompress On Load,引擎把
.ogg解压回 PCM 数组(9.6.1 那个int16/float 数组),塞进内存常驻——因为它短,占点内存无所谓(10.4)。 - 玩家开枪时:你的代码
audioSource.Play(),引擎按采样率把那段 PCM 数组喂给声卡,声卡翻译成电压、推动喇叭(9.6 的”啊哈”时刻)。 - 同一时刻若用的是 BGM:它走 Streaming,不解压进内存,而是边从磁盘读
.ogg边解码边播(10.4.1 的流式账本)。
你看,从”一个 wav 文件”到”玩家听到枪声”,中间每一步都对应本章或第 9 章的一个概念。哪一步慢了、大了、糊了,你都能顺着这条链路定位到是该换格式、调压率、还是改 Load Type。理解原理的意义,就是让你在出问题时知道去拧哪个旋钮,而不是瞎试。
10.10.1 回到第 9 章:格式选择本质是”采样率 / 位深 / 压缩”三连
你回头看会发现,所谓”选音频格式”,底层不过是第 9 章三个旋钮的再组合:采样率决定时间精细度、位深决定振幅精度、压缩决定体积。WAV 是”三高全开、零压缩”;MP3/OGG 是”采样率位深不变(解码后仍是原始 PCM 的规格),但用感知压缩把数据量砍掉九成”;而像游戏里 NPC 对话那种场景,甚至会主动把采样率降到 22.05kHz 甚至 16kHz(回到 9.8 算过的:人声能量在 4kHz 以下,奈奎斯特只需 8kHz 采样就够),从源头再省一轮。所以”格式”不是孤立的标签,而是”这三个旋钮 + 一层压缩”的共同结果。你以后再面对任何音频格式,先用这三个旋钮把它拆开,它立刻就透明了。
10.11 MP3 / AAC 的”帧结构”思想:不是一条数据流,是一节节车厢
我最后补一个能让你”看穿”有损格式本质的点:有损音频不是一整条连续的 PCM 流,而是一串独立的”帧(frame)”。
以 MP3 为例,编码器把 PCM 切成一小段一小段(约 1152 个采样点为一帧),每一帧独立压缩成几十到几百字节。文件结构大致是:
[帧头][压缩数据][帧头][压缩数据][帧头][压缩数据]...
每帧的”帧头”里写着”我这一帧多长、什么码率、什么采样率”。这带来两个工程后果,你一听就懂:
- 可随机定位:播放器想跳到 1 分 30 秒,不用从 0 解码,只要找到那个位置的帧头,从那一帧开始解即可——对比之下,没有帧结构的原始 PCM 反而要”按字节偏移精确计算”才能跳。
- 抗损坏局部化:某一帧数据坏了,只影响那一小段(几十毫秒),后面的帧不受影响;而 WAV 若中间字节错位,后面全废。
AAC 是更进一步的”包(packet)“结构,且引入了ADTS / ADIF 这类封装头,思想一样:把连续信号切成可独立处理的小块。这和第 11 章视频的”帧”是同一个哲学——把连续的东西切成小块,每块自带说明,既能压缩又能随机访问。你作为后端,可以类比成”把大消息拆成带序号的数据包(packet),丢了一个重传那一个,而不是整条重来”。
【思考题】
- WAV 为什么”无损但占地方”?它和第 9 章的 PCM 是什么关系?
- MP3 的”有损”损掉的是什么?为什么大多数人感觉”差不多”?
- “编码”和”容器”的区别是什么?用生活比喻说明。
- 一个 4 分钟立体声 44.1kHz/16bit 的歌,WAV 约多大?若压成 128kbps MP3 约多大?体积差多少倍?(粗略算即可)
- 游戏里短音效和长 BGM,加载策略为什么该相反?分别对应哪个 Load Type?
【参考答案】
- WAV 内部装的就是原始 PCM 数字(连包装都不拆),所以是无损(还原 = 原样);正因不压缩,数据量 = 采样率×声道×位深,体积巨大。关系:WAV 是 PCM 数据的”原样容器”。
- MP3 损掉的是人耳听不见或极不敏感的成分(被掩蔽的、极端频率、过快变化),靠心理声学模型识别并丢弃。多数人感觉差不多,是因为丢掉的正好是感知弱点区,大脑补全了。
- 编码=怎么把数据压缩/还原的算法(如 MP3、OGG);容器=把这些数据打包成文件的格式(如 .mp3、.ogg 文件)。比喻:编码是”真空压缩袋的叠衣法”,容器是”外面的旅行箱”。一种叠法可装不同箱,一种箱也能装不同叠法。
- WAV:172KB/s × 240s ≈ 41 MB。128kbps MP3:128kbps = 16KB/s × 240 ≈ 3.84 MB。体积差约 10 倍。这就是为什么要用压缩。
- 短音效(枪声、脚步)常驻内存也无妨,且要求播放零延迟、可随机触发 →
Decompress On Load(提前解压)。长 BGM 若全载入内存太亏 →Streaming(边读边播,省内存)。本质是”用内存换实时性”还是”用硬盘 IO 换内存”的取舍。