Godot 的音频架构有个反直觉的选择:混音不用系统 API,全部软件实现。macOS 上 CoreAudio、Windows 上 WASAPI,Godot 只向它们要一块固定大小的 PCM 缓冲,剩下的加法全部自己算。这样做换来了跨平台一致的听感和效果器链的可控性,代价是要自己管好一个实时音频线程。这篇讲 servers/audio_server.cpp(4.x 约两千行)里的混音主流程。
分层:驱动、服务器、播放源
从下往上看是三层。AudioDriver 是平台层(drivers/coreaudio/、drivers/wasapi/ 等),负责打开设备、按设备要求的 buffer 大小拉取混音结果。它对上层只暴露一个关键回调:audio_server_mix_callback——设备要数据了,就来敲 AudioServer 的门。
AudioServer 是中枢单例。它手里有三样东西:bus 布局(一棵以 Master 为根的树,存在 project settings 里)、每个 bus 的效果器数组、以及播放源注册表——所有正在发声的 AudioStreamPlayer 实例都在这里登记。
AudioStreamPlayer/AudioStreamPlayer2D/3D 是发声体。它们每帧(音频帧,不是渲染帧)产生一块采样缓冲,交给服务器累加进目标 bus。
一次 mix step 发生了什么
设备线程每次要数据,触发一次 _mix_step,核心就两件事:先让所有播放源产数据,再把 bus 树从叶到根逐级归并。
产数据阶段,AudioServer 遍历激活的播放源列表,调每个源的 mix 回调(内部走 AudioStreamPlayback——八-2 会展开这个)。每个源产出的是一个 AudioFrame 数组,AudioFrame 就是左右声道的 float 对:
// core/math/audio_frame.h
struct AudioFrame {
float l = 0.0f;
float r = 0.0f;
// 重载了 + += * 等运算符,混音就是两个帧逐分量相加
};
播放源不直接写总线。它把 buffer 交给 AudioServer::mix_bus,后者用带增益的累加写入目标 bus 的 channel buffer:$\text{bus}[i] \mathrel{+}= \text{source}[i] \times \text{volume}$。float 累加是软件混音的自然选择——只要声源不超过几十个,float 的动态范围足以让溢出不可闻;真到了几百路同时发声的场景,归一化策略(自动压总线增益)才需要考虑。
归并阶段,bus 树按后序处理:子 bus 的混音结果先加进父 bus(同样乘上子 bus 的 fader 增益),叶 bus 上的效果器在自己的缓冲上就地处理完再上交。Master 是根,最后把它的左右声道交错写成设备要求的 int16/float32 输出。整条链的延迟预算就是设备 buffer 长度除以采样率:默认 512 帧 @ 48kHz ≈ 10.7ms,这是八-6 讨论输出延迟的起点。
效果器实例化:process 接口与就地修改
每个 bus 上的效果器(AudioEffect 资源)在初始化时创建对应的 AudioEffectInstance(比如 AudioEffectReverbInstance)。实例持有运行态——混响的延迟线缓冲、压缩器的包络状态——这些都是有记忆的,所以一个效果器实例同一时刻只能挂在一条链上;两个 bus 想用同一个 Reverb 资源,会得到两个独立实例,调参数两边同步但记忆不共享。混音时每个实例被调 process(const AudioFrame *p_src_frames, AudioFrame *p_dst_frames, int p_frame_count),允许就地(src 和 dst 同一块缓冲)处理。
音频线程与主线程的边界
这是软件混音最容易出事故的地方。AudioServer 的 mix 跑在驱动线程上,而游戏逻辑(play()、改音量、挂效果器)跑在主线程,两边共享播放源列表和 bus 缓冲。Godot 的解法是 AudioServer::lock()/unlock() 一把大锁:主线程凡是动音频结构的调用(play()、set_bus_volume_db、增删效果器)都先拿锁,改完释放;mix step 全程持锁。锁的粒度很粗,但持锁时间也短——mix 一次就是遍历加加法,几十微秒量级。
实务含义:不要在游戏逻辑里每帧做音频操作。如果你在 _process 里逐帧 set_volume_db 做淡入淡出,等于每帧和音频线程抢一次锁,人多声源多时就是卡顿源。正确姿势是一次性把目标值设好,让效果器或 tween 在音频线程内部完成平滑过渡。八-10 会专门测这个锁的竞争成本。
为什么值得自己混
最后回答开头的问题:系统混音器(CoreAudio/WASAPI 的 session 混音)只能做整体音量,做不了每路音频的实时效果链。Godot 的 bus 效果器——侧链压缩、频谱分析、录制(AudioEffectRecord)——全都建立在"采样数据先过我手"这个前提上。软件混音是用 CPU 换控制力,对游戏这种几十路音频的场景,这笔交易稳赚。
本文基于 Godot 4.2–4.4,源码 servers/audio_server.cpp、core/math/audio_frame.h、servers/audio/ 效果器实现;默认 mix buffer 长度见 audio/driver/mix_rate 相关设置。