同一个"播放背景音乐"的需求,选 WAV、OGG 还是 MP3,在 Godot 里的差别远不止是文件大小。三种格式背后是三条独立的解码后端、三种内存占用曲线、以及一个只有 MP3 会踩的循环缝隙坑。这篇把这笔账算清楚。
三条解码路径
引擎源码 modules/ 下的目录结构就是答案:modules/stb_vorbis(OGG)、modules/minimp3(MP3)、modules/dr_wav(WAV,4.x 起的默认 WAV 解码器,替换掉了旧的 wave 模块)。加上 modules/vorbis 提供音频导入时的编码侧支持。它们都是单头文件库(stb 系列),选它们的原因和引擎选 stb 的原因一样:可移植、无外部依赖、许可宽松,代价是解码性能不是最快——对音频这种"48kHz 下每 10.7ms 解 512 帧"的负载,单线程完全够。
行为差异来自数据模型。WAV 不解码:PCM 数据原样加载(除非导入时选了压缩模式),AudioStreamWAV 直接持有采样数组,播放就是内存拷贝,CPU 开销三者最低。OGG/MP3 是有损压缩,解码不能随机访问——Vorbis 和 MP3 都是依赖前后帧的变换编码,seek 必须从关键帧(或文件头)一路解过去。所以流式播放的 AudioStreamOggVorbis/AudioStreamMP3 内部是"边下边解":一个解码线程(或按需同步解码)维持着环形缓冲,混音器消费,八-2 讲过的那套。
内存账:别只盯文件大小
文件大小差距巨大,但运行时内存的差距取决于加载方式。WAV 全量加载的内存是
$$M = f_s \times \text{ch} \times 2 \text{ B} \times T$$
3 分钟立体声 48kHz 音乐 = $48000 \times 2 \times 2 \times 180 \approx 33$ MB——文件本身如果是 16-bit PCM 也正好这么大。OGG 压到 128kbps 只有 2.8 MB 文件,但流式解码的常驻内存只是缓冲(默认几千帧,百 KB 级),代价是持续占一个解码线程和 seek 延迟。
什么时候 WAV 反而是对的?短音效。0.5 秒的枪声,PCM 只要 96 KB,解码开销为零,CPU 缓存友好,触发延迟最低。行业标准做法(Godot 官方文档也是这么建议的):短音效 WAV、长音频 OGG。MP3 在 Godot 里的位置很尴尬——vorbis 在同等码率下音质更好且完全没有下面要讲的循环问题,除非你的素材只有 MP3 一种来源,否则没有理由主动选它。
循环缝隙:MP3 的先天缺陷
这是三种格式最实际的行为差异。无缝循环要求:文件结尾的采样和开头采样在解码后首尾相接,连续无跳变。逐个看:
- WAV:PCM 数组直接环绕读,$i \bmod N$ 索引回绕,采样级精确,天然无缝;
- OGG:Vorbis 码流支持 sample-accurate 的循环(导入时设 loop begin/end 帧),解码器处理 granule 对齐,实际无缝;
- MP3:做不到。MP3 编码器会在首尾各加一段编码器延迟(encoder delay)和解码器填充(padding),这些帧不属于原始音频但会被解码出来。结果就是你设了循环点,接缝处要么多一段静默/杂音,要么开头几毫秒被吃掉。这是格式缺陷不是引擎 bug——所有用通用 MP3 解码器的引擎都一样。做循环 BGM 直接排除 MP3。
流式播放的两个隐藏成本
OGG 流式不是免费的,两个成本要知道。一是 seek 延迟:把播放位置跳到 1:30,解码器要从头解到那里(或从最近的关键帧),操作耗时和位置成正比,做进度条拖动的 UI 要给反馈。二是解码线程的调度抖动:解码线程偶尔被 OS 调度慢了,环形缓冲耗尽,缓冲管理器(八-2)会顶到 buffer_size 下限处——下限设太小(导入默认偏小)会在繁忙机器上出现周期性咔哒声。实际项目里 BGM 的 buffer_size 建议调到 1 秒上下,用一点点内存换抗抖动。
另外 4.x 起 AudioStreamOggVorbis.load_from_buffer() 支持运行时从 PackedByteArray 构造流——配合 FileAccess 或下载的 buffer,可以做到不进导入管线、不落地文件的动态音频。这给热更音乐(六-9 的 PCK 替换之外的另一条路)留了个口子。
本文基于 Godot 4.2–4.4;解码后端库(stb_vorbis / minimp3 / dr_wav)版本随引擎 minor 版本更新,行为语义稳定。