音频引擎、实时混音与声音虚拟化
基于静态源码,未测量实际设备延迟或听感。入口:Summary。
1. 将游戏语义与采样时钟分开#
Base 的 audioengine 管理 sound、category、controller、metadata、环境和动态混音语义;audiohardware 管理 driver、PCM source、decoder、voice、submix、command buffer 和流式播放。Framework fwaudio 将实体及世界数据连接进音频;Suite 还包括音乐、语音及视频配套功能。
这层分离的意义在于:游戏帧可能波动,输出设备却持续按采样时钟消耗数据;音频需要自己的截止时间和背压机制。
2. 独立线程不等于不需要同步#
audEngine::ThreadEntryProc 初始化命令缓冲,在 update lock 保护下调用 AudioFrame;pause 分支休眠。AudioFrame 支持 triggered updates,先等更新 semaphore;然后等待 mixer 对线程命令缓冲的处理。捕获/逐帧渲染模式还会触发 mixer 并等待 mix buffers。
TriggerUpdate 和 WaitForAudioFrame 使用不同 semaphore。调用者不能把发出更新信号等同于音频帧已经完成,也不能把 AudioEngine 一帧完成等同于扬声器已经播放完毕。设备、混音和缓冲排队还可能带来额外延迟。
实现以 mixer frame count 推导时间,编译断言采样率为 48000、mix buffer 为 128 或 256 samples;相应采样块时长约 2.67 或 5.33 ms。这个数值只是块长,不是端到端响应延迟。代码使用整数比例累计时间,以减轻长期浮点累计误差。
分析上,锁持有时间、命令消费速度和解码供给共同决定音频线程是否及时推进。对普通帧任务适用的“晚一点补完”策略,在音频输出欠载时会直接成为可闻问题。
3. 命令缓冲是线程契约,也是容量边界#
audCommandPacketHeader 将 system、command、object、packet size 压入 32-bit 位域;object 为 11 bits、packet size 为 10 bits。WriteAlignedPacket 支持 16-byte 对齐,文档提醒连续写入对齐命令以减少填充浪费。缓冲提供 ready 标记、时间戳、提交时间、高水位和处理完成 semaphore。
这使控制线程不必逐个直接调用设备对象,但并不自动保证任何生产者都可并发写:锁行为还受 AUD_CMD_BUFFER_LOCKING 宏控制。位域容量也意味着命令编码是协议,扩容对象或传递大参数要同步调整编码与消费者,不能只扩大容器。
需要区分三种时间:游戏发出事件、命令提交/消费、采样实际输出。只有保留这些时间点,才能判断延迟来自主线程、音频逻辑、解码还是设备队列。
4. 声音虚拟化:逻辑声音多于可播放声音#
BuildPrioritisedVoiceList 从 sound pool buckets 收集声音,保存 VirtualisationScore、CostEstimate 和物理状态,并同时组织独立声音及 voice groups。这里分数越小优先级越高。
AllocatePhysicalVoices 同时受预算、可用物理 voice 数和本帧物理 voice 配额约束。它比较组与独立声音的优先级;虚拟声音开始实际播放前检查成本和可用槽位;组还需满足相应的整体成本/数量要求。物理播放中的声音也扣成本,不能简单把 budget 理解为“本帧新建数量”。
这是按重要性和估算成本筛选工作集,既避免无限解码,也让组合声音可按组竞争资源。代价是:估算成本未必与真实平台耗时一致,分组策略可能牺牲部分低成本独立声音,优先级切换也需要其他声音状态逻辑维护连贯性。本文没有用虚拟化函数独立证明所有格式都能无缝恢复播放相位。
mixer.h 中的 PCM source 容量不能直接当作全游戏同时可闻 voice 上限;逻辑声音、PCM source、物理播放和混音节点是不同对象集合。
5. 流式音频与世界流式共享设备,截止时间不同#
audStreamPlayer 接受外部 PCM ringbuffer,内部记录 starved 状态,并提供低延迟 StopPlayback 命令参数;停止接口的用途包括让生产者尽快复用缓冲。音频数据需持续供应,停止请求到安全复用的具体边界必须按该接口协议执行。
大世界高速移动时,纹理、碰撞和音频都可能竞争 IO/解压/内存带宽。但音频的时间需求更接近持续流:单个块延误即可欠载;地图纹理有时还能通过 LOD 暂时降级。因此新项目应分别记录音频最低缓冲水位和场景资源队列时间,不能用总磁盘吞吐证明两者都健康。
6. 世界声学数据是另一份空间表示#
Framework agPolyMesh 使用量化顶点、polygon、region、region connection 和四个方向的 stitching 数据,保存 cell size/height 与包围盒。它为区域之间的声学关系提供专门表示,并不等同于视觉 portal、物理 BVH 或导航数据。
仅凭这个结构不能宣称引擎执行实时几何声线追踪,也不能证明所有遮挡都由这一张 mesh 处理。可确认的设计启发是:声学查询可使用远比显示网格紧凑的连通图;跨流式块连接必须随着邻区装载/卸载保持有效。
音乐、语音聊天和环境混音拥有各自内容与平台适配,本报告对 audmusic/avchat 为职责级覆盖,未逐个验证编码器和音乐转场规则。
7. 新项目建议与验证#
| 设计要点 | 关注的限制 | 建议指标/实验(未执行) |
|---|---|---|
| 采样时钟与游戏时钟分离 | 多层缓冲扩大响应延迟 | 事件到提交、提交到混音、设备输出分别追踪 |
| 虚拟 voice 与成本预算 | 估算误差、组饥饿、切换抖动 | 逻辑/物理数、start/stop 次数、真实解码时间 |
| 有界命令缓冲 | 高水位、失败写入、锁争用 | 大量同时改变参数及暂停/恢复 |
| 流式 ringbuffer | IO 竞争和生产者延迟 | 高速穿越时最低水位、starved 次数 |
| 独立声学空间表示 | 分块 stitching 生命周期 | 邻区卸载、瞬移、室内外转换 |
8. 源码证据#
| 文件 | 入口 |
|---|---|
| engine.h、engine.cpp | cpp 485–567 线程和时钟;672–705 更新与初始化 |
| virtualisation.h、virtualisation.cpp | 44 起排序输入;417 起预算分配 |
| cmdbuffer.h | 31–38 packet;49–113 写入与同步 |
| streamplayer.h | 外部环形缓冲、停止与欠载状态 |
| audmesh.h | 30–78 区域、连接、量化顶点与 stitching |
| audioentity.h | 世界实体到音频系统的接口 |