游戏音频系统
全库目录221
本文目录13
游戏音频管线把压缩资源解码为采样数据,按发声实例(Voice)计算增益和空间参数,再经过总线、效果器和最终混音器输出。音频线程有严格的实时性要求。一次锁等待、磁盘卡顿或临时分配都可能直接变成爆音。
样本交付与实时预算
回调的实时预算
音频回调的时间预算由缓冲区决定:48 kHz 采样率、512 帧缓冲意味着约 10.7 ms 内必须交付出 512 个采样帧(每帧含各输出声道的样本),超时就出现可闻的爆音或断裂。这个预算与帧率无关,帧率降到 20 FPS 时音频线程仍要按同样的节奏交付。
因此音频回调内不允许出现不可控耗时:分配内存、读写文件、等待锁、调用未知复杂度的回调都在禁止之列。能预先算的在主线程算好,通过无锁队列或双缓冲交给音频线程;音频线程执行有明确耗时上界的混合与滤波;压缩解码若无法满足上界,就由后台预解码。
缓冲越大预算越宽,代价是延迟:按键音效 100 ms 后才出声是可感知的。较大的流送缓冲适合音乐与对白,较小的设备回调缓冲有助于降低操作反馈延迟。它们是不同层次:一个最终设备通常按共同回调节奏混音,各 Voice 可以有不同的预解码和流送缓存。
流送欠载
预解码环形缓冲把后台解码与设备回调分开。生产者写样本后发布写游标,消费者只读取已经发布的区间,再发布读游标。帧数必须按“每帧含所有声道”计算,立体声512帧就是1024个标量样本。
例如容量为1024帧,单生产者与单消费者分别维护单调递增的写游标W与读游标R,且保持0<=W-R<=1024。W=1500、R=1200时,有效区间为逻辑帧1200到1499,共300帧;实际数组位置为logicalFrame % 1024。生产者只能写入1024-(W-R)=724个空闲帧。立体声每个数组帧还包含左右两个float,两个声道一起写好以后才发布新的W,避免消费者读到半帧。消费者读取发布的W,消费以后再发布R;两个方向都需要相应的内存同步。此处明确限定单生产者、单消费者,多写者不能直接共享这套游标规则。
如果本次需要512帧,缓冲只有300帧,回调仍必须填满输出。一种欠载策略是消费这300帧,随后用各声道的最后一个有效样本乘以递减包络,短暂过渡到零,其余位置填零;缺失区间没有可继续读取的源样本。与此同时增加欠载计数。等待磁盘补齐会把缺口扩大成整个回调超时,恢复时再渐入可以减轻第二次点击。回调日志写入预分配计数或追踪环,格式化和磁盘输出交给其他线程。
数字音频基础
PCM 用固定采样率记录离散振幅。采样率必须覆盖目标信号带宽,低于 Nyquist 条件会产生混叠。位深决定可表示的动态范围和量化噪声,不等于播放音量。
分贝是对数比值。振幅比常写为:
两个声音的线性振幅不能直接按 dB 数值相加。Mixer 通常在线性域累加,再用 Meter 显示 dBFS。总线增益过高会削波,浮点混音虽能暂时容纳超出范围的值,最终输出前仍需控制峰值。
WAV 常作为 PCM 容器,FLAC 是无损压缩,MP3、Vorbis、Opus 等常用于有损编码;Ogg 是可承载不同编码的容器。短而频繁的音效可预解码,长音乐和对白更适合流式解码。选择取决于内存、I/O、CPU 和随机跳转需求。
线性增益与连续混合
音量参数以dB表示时,样本乘法使用gain = 10^(db/20)。0、-6、-12 dB约对应1、0.501、0.251的振幅比,-80 dB是否静音由产品阈值决定。声源和总线增益可以在线性域相乘,多个声音则在线性域相加。
直接把增益从0切到1,会在波形上制造不连续台阶,其宽频能量表现为点击声。下面的C++机制片段把单声道预解码浮点样本混入已有浮点总线,输入输出均为count个样本,数值无单位。缓冲由调用者预先分配;声道映射、采样率转换、解码和最终限幅在相邻阶段处理。
void MixMono(float* bus, const float* pcm, unsigned count,
float previousGain, float targetGain)
{
for (unsigned i = 0; i < count; ++i) {
float t = count > 1 ? float(i) / float(count - 1) : 1.0f;
float gain = previousGain + (targetGain - previousGain) * t;
bus[i] += pcm[i] * gain;
}
}每个Voice从上一块末尾增益开始,在本块内逼近目标,块结束后保存targetGain。实际平滑时长若需固定为毫秒,应按采样率确定斜坡长度并跨块保留剩余样本数,避免设备缓冲变化导致音量响应改变。
总线累加阶段保留超过1的浮点值。若每混入一个Voice就截断,结果会依赖Voice遍历顺序;最终输出前统一控制峰值,才便于定义削波或限制器行为。测试两路同相满幅正弦时,求和峰值为2;两路反相则相消。这能同时检查符号、增益和通道映射。
空间化与混音
Voice 与空间化
每个发声实例包含资源、播放位置、音高、增益、循环状态和路由。Listener 提供听者位置与朝向。一种可计算的立体声模型把声源在听者右轴上的归一化投影映射为pan,限制在[-1,1],再令theta=(pan+1)*pi/4、左右增益分别为cos(theta)与sin(theta)。居中时各约0.707,平方和为1;最左侧为(1,0),最右侧为(0,1)。它保持该模型的功率权重,却不模拟真实双耳的时差与频率变化。耳机的HRTF滤波以方向对应的脉冲响应卷积样本,解释目标不同,不能只调左右音量替代。
距离衰减曲线是设计参数,不必机械照搬物理平方反比。Min/Max Distance、Roll-off 和 Priority 一起决定哪些声音值得保留。超过实体Voice预算时,系统可按感知响度、距离和玩法重要性进行虚拟化:保留逻辑播放位置,暂不产生采样;重新变得重要时按该位置恢复。恢复需要处理流送定位和增益渐变,否则会听到重播或突跳。
遮蔽通常用少量 Raycast 或场景声学数据估计,再驱动音量和低通滤波。每帧为所有声源做精确射线检测成本过高,需要分帧、缓存并限制重要声源。混响描述空间反射形成的尾音,常按 Reverb Zone、卷积响应或参数化模型实现。
多普勒效应由声源和听者的相对径向速度产生。速度不连续会让 Pitch 突变,因此需要稳定速度估计和合理上限。
Mixer 与资源系统
Voice先路由到音效、音乐、对白等总线,再经过压缩、EQ、Reverb Send 和 Master。Ducking 可以在对白出现时压低音乐,但攻击和释放时间过短会抽动。
音频 Bank 负责把事件、样本和元数据作为一组资源发布。异步加载要区分“事件已请求”“Bank 已加载”“样本已解码”和“Voice 已开始”。卸载时需要引用计数或所有权约定,避免正在播放的 Voice 失去底层数据。
Wwise、FMOD 一类中间件提供事件、混音、Profiler 和平台编码工具。接入重点在于统一资源 ID、语言包、版本、流送策略和构建产物,包一层播放函数只是起点。
调试
音频 Profiler 要同时观察活跃 Voice、虚拟 Voice、解码耗时、Stream Buffer、回调峰值和总线电平。偶发爆音应记录 Audio Callback 的最长耗时,而不是只看平均值。暂停、切后台、切换输出设备和快速场景切换都是必要回归场景。
验证方法
- 记录音频回调的最长耗时,与缓冲时间预算对比,而不是只看平均值。
- 压力测试:同时播放上百个 Voice,观察虚拟化策略与爆音出现的先后顺序。
- 暂停、切后台、切换输出设备后恢复,确认 Voice 状态与流送缓冲正确续播。
- 用正弦波测试增益链路:0、-6、-12 dB 的线性振幅应分别约为 1、0.5、0.25。
- 频繁启停流式音乐,检查解码线程与缓冲的边界状态。
参考实验与证据
CPU参考模型的mix_mono验证浮点叠加、块内线性斜坡、单样本和空缓冲。2026-10-22执行python 知识库/examples/revision_22_26/test_reference.py,21项通过,其中3项为混音检查。Python模型是离线数值实验,不能证明回调实时性;C++片段编译、设备回放、流送欠载和听觉结果尚未验证。
相关主题
参考资料
- PortAudio 2.0/v19 Writing a Callback Function,实时回调禁止无界耗时调用与完整输出缓冲的示例;2026-10-22核读。
- 分贝与线性增益、采样预算和斜坡公式由本页定义及CPU数值实验支持。Wwise、FMOD和引擎音频接口需另按项目版本接入,本页不提供已运行的中间件工程。