从”普通程序员能听懂”到”能改 UE 音频源码”——基于源码逐行考证:空间音频的原理、使用、
玩法结合、优化、封装五条主线全覆盖
- 文档版本:1.0(2026-08-26)
- 源码快照:本地仓库
d:\Project\GameDevelop\UnrealEngine,UE 5.9 移动优化 fork,HEAD6cea9bd20f8f - 写作原则:文中所有”源码考古”块引用的文件路径、行号、函数名均在本仓库中实际验证过;行号只用于定位,请以
路径:函数名为准 - 姊妹文档:本文是《Nanite》《VSM》《UE 网络架构》之后的第四篇——前两篇是 GPU 侧单进程,网络篇是 CPU 侧跨进程,本篇是 CPU 侧单进程 + 每音频帧 10ms 级实时循环。无需先读前三篇,但第 10 章(玩法结合)会引用网络文档的 RPC 章节
- 符号说明:
Attenuation=衰减,Occlusion=遮挡,Reverb=混响,Concurrency=并发,Spatialization=空间化,Panning=声像(扬声器声像),Listener=听者,Ducking=避让;Submix/Bus/HRTF/ITD/ILD/Ambisonics/FOA/Focus不翻译
第 0 章 导读:这份文档怎么读
TL;DR|这份文档把 UE 空间音频拆成五条主线讲透:原理(人耳如何定位声音)、机制
(一个声音从游戏代码到耳朵的完整旅程)、使用(从零配置一个 3D 声源)、玩法结合
(事件驱动/多人同步/环境区域)、优化与封装(并发预算/流送/音频管家模式)。
心智模型:空间音频 = 衰减(距离感)+ 空间化(方向感)+ 遮挡(隔墙感)+ 混响(空间感)
四件套的合奏。三个深度档位:30 秒看懂、半天学会用、1 天读源码。
0.1 目标读者与前置知识
目标读者是普通程序员:会写 C++ 或蓝图、用过 UE 的场景与组件、想给自己的游戏加上”听声辨位”级别的音频体验。每个音频术语第一次出现都配一句直觉解释。
前置知识三个:
- C++/蓝图与 Actor 组件:声音挂在场景里(AudioComponent),由代码触发;
- 向量与距离:衰减/遮挡全是”声源与听者的距离/方向”计算;
- 实时循环:音频线程以 10ms 级周期处理声音——理解”每帧几十个声音 × 每个几毫秒 DSP”的预算思维。
本文的特别之处:前四篇只讲”引擎如何工作”,本篇额外回答”项目里怎么用、怎么管“——第 9~12 章的”动手实验室”块是新增的配方型内容。
0.2 三条阅读路径
| 通道 | 读什么 | 耗时 | 读完能做什么 |
|---|---|---|---|
| 🚀 30 秒通道 | 每章 TL;DR + 第 2 章九类比 + 图 F1 | 10 分钟 | 跟人讲清空间音频四件套与游戏音频预算 |
| 📚 标准通道 | TL;DR + 正文 + 第 9~12 章(四大重点) | 6~8 小时 | 能配衰减/遮挡/混响、能写事件驱动音频、能做并发预算、能搭 AudioManager |
| 🔬 源码考古通道 | 全部 + 源码考古块 + 附录 A 对照 | 1~2 天 | 敢打开 Engine/Private/ 对照着读,能定位任意音频子系统 |
0.3 块标记约定
与前三篇一致,新增一种:
- **
TL;DR**(每章开头):30 秒读完本章,3~5 句、必须含类比名、不含源码路径; - **
类比实验室**(正文小节):用普通程序员已懂的东西讲陌生概念; - **
源码考古**(每章末尾,折叠块):贴真实源码(≤25 行/段),逐行翻译; - **
避坑**(正文穿插):版本差异、已删除类、公开资料不一致; - **
动手实验室**(第 9~12 章新增):配方型内容——资产配置步骤/代码骨架/验收标准,给”想马上用起来”的读者。
0.4 一张图看懂全局
读图方法:一个声音从游戏代码出发,过三道门(距离门控 → 并发检查 → 入队),在音频线程被求值(衰减/遮挡/混响算好”该多响、多闷、多少回声”),在渲染线程被逐源处理(解码 → 滤波 → 空间化 → 混音),最终被听者”接收”。四件套(衰减/空间化/遮挡/混响)在音频线程求值、渲染线程执行——这是全书的骨架。
0.5 章节地图
| 篇 | 章节 | 一句话 |
|---|---|---|
| 开篇 | 第 0~1 章 | 怎么读 + 人耳如何定位声音(四要素) |
| 概念篇 | 第 2 章 | 九大概念速览(每个一个类比) |
| 机制篇 | 第 3~8 章 | 播放链路 → 衰减 → 空间化 → 遮挡 → 混响 → 并发 |
| 使用篇 | 第 9 章 | 从零配置一个 3D 声源(重点) |
| 玩法篇 | 第 10 章 | 事件驱动 / 多人同步 / 环境区域(重点) |
| 优化篇 | 第 11 章 | 并发预算 / 流送 / 虚拟循环 / 移动端(重点) |
| 封装篇 | 第 12 章 | AudioManager 设计模式 / 数据驱动(重点) |
| 调优篇 | 第 13~14 章 | CVar 速查与常见坑 / 局限与展望 |
| 附录 | A~D | 源码地图、术语表、参考资料、自测题 |
第 1 章 空间音频是什么:人耳如何定位声音
TL;DR|空间音频的目标是”让玩家凭耳朵知道声音在哪“。人耳用四要素定位:ITD
(双耳时间差,低频)/ ILD(双耳响度差,高频)/ 耳廓滤波(前后上下)/ 转头动态线索。
游戏把这四要素拆成四件套:衰减管距离感、空间化管方向感、遮挡管隔墙感、
混响管空间感。类比:人耳是立体声雷达,游戏是雷达的模拟器。
1.1 人耳定位四要素
| 线索 | 全称 | 原理 | 频段 | 游戏实现 |
|---|---|---|---|---|
| ITD | Interaural Time Difference | 声音先到近耳(时间差 ~0.6ms 量级) | 低频(波长长,相位差可辨) | 空间化(Panning 近似 + HRTF 插件精确模拟) |
| ILD | Interaural Level Difference | 近耳更响(头部遮挡高频) | 高频(头影效应) | 声像增益差(EqualPower/Linear) |
| 耳廓滤波 | Pinna Filtering | 外耳形状对特定角度滤波 | 全频段 | 仅 HRTF 插件能模拟(本 fork 无内置) |
| 动态线索 | Head Movement | 转头时线索变化 | — | 听者朝向(Listener 旋转) |
为什么”前后与上下最难分”:同一距离球面上的点,ITD/ILD 相同——所以”正前方 vs 正后方”在双耳线索上几乎等价。游戏里常见解决:轻微增益差异 + 混响差异 + 玩家转头。
1.2 两条出路:耳机与扬声器
- 耳机:双声道直达双耳,天然适合 ITD/ILD/HRTF——双耳渲染(Binaural)的舞台;
- 扬声器:左扬声器会响进右耳(串音),需要交叉串音消除(Crosstalk Cancellation)才能模拟双耳效果——游戏里通常退化为声像(Panning):把声音按方位角分配到最近的两个扬声器。
UE 的默认路径:扬声器声像(Panning,引擎内置)为兜底,双耳渲染(HRTF)完全交给插件(第 5 章详述)——这是本 fork 的架构事实。
1.3 游戏音频四问题 ↔ UE 四件套(全书总纲)
玩家的问题 引擎的答案 章节 ┌───────────────────┐ ┌──────────────────────────────┐ │ "它离我多远?" │ → │ Attenuation 衰减(距离→音量曲线) │ Ch4 │ "它在哪个方向?" │ → │ Spatialization 空间化(方位→声道)│ Ch5 │ "中间有堵墙?" │ → │ Occlusion 遮挡(射线+低通+音量) │ Ch6 │ "我在什么房间?" │ → │ Reverb 混响(回声指纹) │ Ch7 └───────────────────┘ └──────────────────────────────┘ 外加两个全局系统: │ "太多声音了怎么办?" │ → │ Concurrency 并发(占线/偷声) │ Ch8 │ "怎么管理全部声音?" │ → │ SoundClass/Mix 分层(调音台推子) │ Ch9
记住这张表——后文所有章节都是它的展开。
1.4 UE 实现路径五站预告
1 | ① 游戏代码(PlaySoundAtLocation) —— 事件出口(Ch3/Ch9) |
1.5 一点历史与本 fork 事实
- UE4 早期:平台专属音频(XAudio2/OpenAL 等 ~95% 平台差异代码);
- AudioMixer(4.20+):统一跨平台渲染器(
Engine/Source/Runtime/AudioMixer/),平台层只剩设备查询/缓冲提交; - 本 fork(5.9):无内置 HRTF/ITD DSP(双耳渲染全靠插件,见
au.EnableBinauralAudioForAllSpatialSounds,AudioDevice.cpp:121);fork 对音频零改动(git 验证)。
避坑:网上很多文章会教你”打开 HRTF”——本 fork 里没有内置 HRTF。
bSpatialize + SPATIALIZATION_HRTF只是”请求走插件路径”,必须启用空间化插件(项目插件里开
Windows Spatial Audio / ResonanceAudio / Oculus 等)才有双耳效果。第 5.4 章详述。
1.6 小结
这一章记住三句话:
- 人耳四要素:ITD / ILD / 耳廓滤波 / 动态线索;
- 游戏四件套:衰减(距离)/ 空间化(方向)/ 遮挡(障碍)/ 混响(空间);
- 耳机走双耳渲染(插件),扬声器走声像(引擎内置)。
下一章,九大概念各配一个类比。
第 2 章 九大核心概念速览(每个概念 = 一个类比)
TL;DR|UE 音频的九个核心概念,用九个你早就懂的东西类比:衰减 = 音量地图、
空间化 = 把声音放进耳朵、遮挡 = 隔墙听声、混响 = 房间的回声指纹、
并发 = 电话占线、SoundClass = 调音台推子、AudioBus = 音频接线板、
虚拟循环 = 远处声音放假、AudioManager = 音频管家。这一章只立类比不给代码。
2.1 衰减(Attenuation)= 声音的”音量地图”
定义:声源与听者距离 → 音量的映射曲线。FSoundAttenuationSettings(SoundAttenuation.h:148)定义距离算法(线性/对数/逆距离等六种)、形状(球/胶囊/盒/锥)与衰减距离。衰减范围同时也是”听不见就跳过”的性能参数(SoundIsAudible 门控)。
类比实验室:音量地图
想象手机地图的”声音图层”:离声源越近音量越大,曲线形状决定了”多快变小声”。
线性 = 匀速衰减(直观);逆距离平方 = 真实物理(声音按距离平方衰减);
对数 = 可听距离长(远处还有声)。地图画多大,声音就能传多远——超出地图边界
的声音引擎根本不创建(省 CPU)。
2.2 空间化(Spatialization)= 把声音”放”进耳朵里
定义:方位角(声源相对听者头部的方向)→ 左右声道增益的转换。默认走声像(Panning):方位角在相邻扬声器间插值(Get3DChannelMap,AudioMixerDevice.cpp:2915);走插件则用 HRTF 模拟完整双耳滤波。
类比实验室:舞台上的音箱
声像是”舞台上的两只音箱”:声音在左音箱响一点、右音箱轻一点,你的大脑就以为
声源在左边。HRTF 是”直接塞进耳朵的耳机”:连”声音来自斜后方”这种细节都能模拟。
简单游戏用音箱(Panning)够了,听声辨位射击游戏才需要耳机(HRTF 插件)。
2.3 遮挡(Occlusion)= 隔墙听声
定义:声源与听者之间有墙时,声音变”闷”(低通滤波)变”轻”(音量衰减)。引擎用物理射线(LineTraceTestByChannel,ActiveSound.cpp:1636)检测遮挡,结果平滑插值避免”咔哒”声。
类比实验室:隔墙听邻居
墙后的电视声:听得到”在响”但听不清”在说啥”(高频被墙吸收 = 低通滤波),
音量也小一截。遮挡就是把”墙”翻译成”低通 + 音量”两个旋钮。
2.4 混响(Reverb)= 房间的”回声指纹”
定义:声音在房间里的多次反射形成的尾音。引擎把混响发送到专用子混音(MasterReverbSubmix,AudioMixerDevice.cpp:2082),用板式混响算法(FPlateReverbFast)模拟。
类比实验室:房间的回声指纹
山洞喊一声有长回声,卫生间拍手有清脆的短回声,客厅几乎没有——每个房间都有
自己的”回声指纹”(衰减时间、密度、高频吸收)。混响参数 = 给声音盖上房间的
指纹章。**玩家闭眼也能听出”我进山洞了”**,这就是玩法价值。
2.5 并发(Concurrency)= 电话占线
定义:同时播放的声音超过预算时的裁决规则。FSoundConcurrencySettings(SoundConcurrency.h:73)的 MaxCount + ResolutionRule(七种规则:阻止新的/停最老的/停最远的/停最低优先级的……)。
类比实验室:电话占线
同一时刻只能有 N 通电话(MaxCount)。第 N+1 通打进来怎么办?七种规则任选:
挂断新来的(PreventNew)、挂断最老的(StopOldest)、挂断最远的(StopFarthest)、
挂断最不重要的(StopLowestPriority)……还能”不挂断只调小”(Ducking 避让——
语音通话时把背景音乐调小,而不是挂断音乐)。
2.6 SoundClass = 调音台推子、SoundMix = 混音快照
定义:USoundClass(SoundClass.h:208)组成层级树(Master → SFX/音乐/语音/UI),每个推子管音量/音高/低通;USoundMix(SoundMix.h:170)是”一组推子的预设快照”(进战斗时压低音乐、Boss 战时再加一层)。
类比实验室:调音台
游戏混音 = 电台调音台:主持人(语音)一档、背景音乐一档、音效一档,各有一个
推子。SoundMix 是”预录好的推子操作”:按一下”Boss 战”按钮,音乐推子自动
拉低 20%、音效推子抬高 10%,还带淡入淡出。
2.7 AudioBus = 音频接线板、MetaSound = 音频电路板
定义:UAudioBus(AudioBus.h:87,头注释原话 “An audio bus is an object which represents an audio patch cord”)是音频的”跳线”——声音送进总线,总线接进子混音;UMetaSoundSource(MetasoundSource.h:89)是程序化音频的 DSP 图——采样级实时计算(合成、调制、包络),与 SoundCue 的”波形编排”定位不同。
类比实验室:接线板与电路板
AudioBus = 电源接线板:所有设备插上去,统一供电(所有声音汇入总线统一处理)。
MetaSound = 自己焊电路板:不播放现成录音,而是实时”算”出声音——振荡器、
包络、滤波器按你连的线路发声。想合成引擎轰鸣?焊一块 MetaSound 电路板。
2.8 虚拟循环 = 远处声音”放假”、AudioManager = 音频管家
定义:FAudioVirtualLoop(AudioVirtualLoop.cpp)让远处循环声降频更新(更新间隔按距离在 0.1s~3.0s 插值)——声音还在逻辑上”播着”,但 CPU 不陪它每帧跑;”音频管理器”引擎没有内置(只有 FAudioDevice 设备层)——游戏自己封装(第 12 章)。
类比实验室:远处声音放假
远处的瀑布声:玩家听得到(音量小但存在),但引擎不会让它每帧精细计算——
放假模式:每 3 秒醒来检查一次”玩家靠近了吗?”,靠近了恢复全职工作。
100 个远处循环声 × 每帧计算 = CPU 灾难;100 个 × 每 3 秒一次 = 几乎免费。
2.9 概念 → 源码落点预告表
| 概念 | 类比 | 核心类/函数 | 章节 |
|---|---|---|---|
| 衰减 | 音量地图 | FSoundAttenuationSettings / UpdateAttenuation | Ch4 |
| 空间化 | 放进耳朵 | Get3DChannelMap / IAudioSpatialization | Ch5 |
| 遮挡 | 隔墙听声 | CheckOcclusion(LineTraceTestByChannel) | Ch6 |
| 混响 | 回声指纹 | MasterReverbSubmix / FSubmixEffectReverbSettings | Ch7 |
| 并发 | 电话占线 | FSoundConcurrencySettings / EvaluateConcurrency | Ch8 |
| SoundClass/Mix | 调音台推子 | USoundClass / USoundMix / UGameplayStatics | Ch9 |
| AudioBus/MetaSound | 接线板/电路板 | UAudioBus / UMetaSoundSource | Ch9 |
| 虚拟循环 | 远处放假 | FAudioVirtualLoop | Ch11 |
| AudioManager | 音频管家 | FAudioDeviceManager / 游戏侧自建 | Ch12 |
第 3 章 播放链路:一个声音的完整旅程
TL;DR|一个声音从游戏代码到耳朵走三线程四阶段:游戏线程触发(PlaySoundAtLocation
或 AudioComponent::Play → 距离门控 → 并发检查 → 入队)→ 音频线程求值(Update →
排序 → UpdateAttenuation 算出衰减/遮挡/混响参数 → Parse 生成波形实例 → new FMixerSource)
→ 渲染线程执行(每 10ms 级回调逐源处理:解码→滤波→空间化→混音→输出)→ 听者接收。
类比:点外卖——下单(游戏线程)、厨房配菜(音频线程)、炒菜出餐(渲染线程)。
3.1 两个入口:PlaySoundAtLocation vs AudioComponent::Play
UGameplayStatics::PlaySoundAtLocation |
UAudioComponent::Play |
|
|---|---|---|
| 需要组件 | 否(纯静态函数) | 是(场景组件) |
| 生命周期 | 播完即毁(FActiveSound 自动清理) | 跟随组件(可停止/淡出/换声) |
| 参数控制 | 有限(音量/音高/衰减资产) | 完整(SetVolumeMultiplier/FadeIn/参数接口) |
| 适用 | 一次性事件(开火/拾取/爆炸) | 持续声音(引擎/环境/对话) |
两个入口最终都构造同一个 FActiveSound(活动的声音实例)并 AddNewActiveSound 入队——殊途同归。
3.2 距离门控:听不见就不创建
FAudioDevice::PlaySoundAtLocation(AudioDevice.cpp:6705)先调 SoundIsAudible(:6318,按衰减 MaxDistance 判定):听不见 → 直接返回,不创建实例。
这是音频性能的第一道闸:世界里有 500 个声源事件,只有玩家周围的几十个真正创建了 FActiveSound。
3.3 FActiveSound:一个”活着的声音”
FActiveSound(ActiveSound.h)是音频线程上的声音实例:记录声源位置、音量/音高倍率、衰减资产引用、并发组、SoundNode 实例状态等。它是音频系统的”灵魂对象”——游戏线程说”播这个声音”,音频线程操作的就是它。
3.4 音频线程四步:求值
FAudioDevice::Update(AudioDevice.cpp:4725,音频线程每帧):
1 | Update |
FWaveInstance(波形实例)是”一个 FActiveSound 展开出的可播放流”——一个 SoundCue 可以展开出多个 WaveInstance(多节点同时响)。
3.5 渲染线程:逐源处理
FMixerDevice::OnProcessAudioStream(AudioMixerDevice.cpp:1599,每 10ms 级回调):
1 | // Compute the next block of audio in the source manager |
FMixerSourceManager::ComputeNextBlockOfSamples(AudioMixerSourceManager.cpp:3919)按 SourceId 逐个处理——每个声源每回调的 DSP 成本 = 解码 + 衰减增益 + 滤波器 + 空间化 + 混响发送,这是第 11 章性能预算的原子单位。
3.6 线程模型:GameThread / AudioThread / RenderThread
| 线程 | 干什么 | 同步方式 |
|---|---|---|
| 游戏线程 | 触发声音(Play/Stop)、改参数 | FAudioThread::RunCommandOnAudioThread(AudioThread.h:58)命令队列 |
| 音频线程 | FActiveSound 求值(衰减/遮挡/混响) | 命令队列 + 事件 |
| 音频渲染线程 | 逐源 DSP + 混合输出 | 回调驱动(OnProcessAudioStream) |
关键:游戏线程绝不直接碰音频数据——一切通过命令队列。所以”播放中改音量”是异步生效的(第 9 章的热更新机制)。
3.7 源码考古:PlaySoundAtLocation 全链
源码考古:想看真东西的人进。
① 静态入口,
GameplayStatics.cpp:1526(要点):
1
2
3
4
5
6
7
8
9 void UGameplayStatics::PlaySoundAtLocation(...)
{
FAudioDevice* AudioDevice = World->GetAudioDevice();
// 转发给音频设备;无设备(无音频)直接返回
if (AudioDevice)
{
AudioDevice->PlaySoundAtLocation(Sound, World, Location, Rotation, VolumeMultiplier, ...);
}
}这段代码在干什么:蓝图/C++ 的音频静态入口是个薄转发——真正的逻辑全在
FAudioDevice。这也是”引擎把音频抽象成设备层”的体现:游戏代码不直接碰
平台音频 API。② 设备层,
AudioDevice.cpp:6705(要点):
1
2
3
4
5
6
7
8
9
10
11 void FAudioDevice::PlaySoundAtLocation(...)
{
if (Sound == nullptr) return;
// 距离门控:听不见就不创建
if (!SoundIsAudible(Sound, World, Location, ...)) return;
// 构造活动声音实例
FActiveSound NewActiveSound;
NewActiveSound.Priority = Sound->Priority;
... // 填充位置/音量/衰减引用
AddNewActiveSound(MoveTemp(NewActiveSound), ...); // 入队到音频线程
}这段代码在干什么:设备层的三步——空检查 → 距离门控 → 构造入队。
SoundIsAudible用衰减的 MaxDistance 算”玩家能不能听到”:听不到直接 return,
连 FActiveSound 都不建。这就是”500 个声源只有几十个真活着”的机制。③ 组件路径,
AudioComponent.cpp:499-673(要点):
1
2
3
4
5
6
7
8 void UAudioComponent::Play() { PlayInternal(...); }
void UAudioComponent::PlayInternal(...)
{
// 构造 FActiveSound 并填充:衰减(三级覆盖)、优先级(bOverridePriority)、
// 并发组、参数(SetFloatParameter 等)
...
AudioDevice->AddNewActiveSound(NewSharedActiveSound, ...);
}这段代码在干什么:组件路径与静态路径的区别——组件携带完整上下文(衰减覆盖、
参数、生命周期),静态函数只带基本参数。**”需要控制的用组件,一次性的用静态函数”**
就是从这里来的。
第 4 章 衰减:声音的”音量地图”
TL;DR|衰减把”距离”翻译成”音量”:
FSoundAttenuationSettings(改名自 FAttenuationSettings)
定义六种距离曲线(线性/对数/逆距离…)与四种形状(球/胶囊/盒/锥),FalloffDistance
是”多快衰减”,MaxDistance同时是性能门控(听不见就跳过)。组件可以三级覆盖
(组件覆盖 > 资产 > 声音回退),播放中还能热更新。类比:音量地图——画多大、怎么
渐变,全在这张图上。
4.1 结构与资产
FBaseAttenuationSettings(Attenuation.h:56):形状与曲线基类——DistanceAlgorithm(六种曲线)、AttenuationShape(四种形状)、FalloffDistance、CustomAttenuationCurve;FSoundAttenuationSettings(SoundAttenuation.h:148):在基类上加空间化/遮挡/混响开关(第 5/6/7 章的开关都在这里);USoundAttenuation(:452):FSoundAttenuationSettings的资产容器。
避坑:
FAttenuationSettings是 UE 5.1 前的旧名——老教程的类名引用一律按FSoundAttenuationSettings处理。另外FBaseAttenuationSettings定义在Classes/Engine/Attenuation.h(不是 Classes/Sound/ 下),源码地图容易找错。
4.2 六种距离曲线
| DistanceAlgorithm | 形状 | 适用 |
|---|---|---|
| Linear | 直线衰减 | 直观调试 |
| Logarithmic | 对数衰减(快) | 短距离事件声 |
| Inverse | 逆距离(真实物理) | 枪声/爆炸(近处巨响) |
| LogReverse | 对数反向 | 远处仍有声 |
| NaturalSound | 自然声(带 dBAttenuationAtMax) | 环境音 |
| Custom | 自定义曲线 | 精确控制 |
4.3 四种形状
| 形状 | 语义 | 适用 |
|---|---|---|
| Sphere(球) | 各向均匀 | 通用 |
| Capsule(胶囊) | 轴向拉长 | 走廊/道路 |
| Box(盒) | 矩形区域 | 房间 |
| Cone(锥) | 定向(声源朝向) | 喇叭/枪口/对话 |
Cone 的玩法价值:对话只朝说话者前方传播(背后听不清)——这是”方向性”的一部分,与第 5 章的空间化互补(一个是音量、一个是声道)。
4.4 距离低通(空气吸收)
bAttenuateWithLPF(SoundAttenuation.h:162)+ LPFRadiusMin/Max/LPFFrequencyAtMin/Max(:281-293):距离越远高频衰减越多(空气吸收低频穿透)——**远处的声音”闷”**,这是真实感的重要来源,也是遮挡之外第二个”低通”旋钮。
4.5 组件三级覆盖与热更新
UAudioComponent::GetAttenuationSettingsToApply(AudioComponent.cpp:1342):
1 | ① bOverrideAttenuation = true → 用组件的 AttenuationOverrides(结构体覆盖) |
热更新:播放中用 SetAttenuationOverrides 会通过 SendCommandToActiveSounds(:1367-1388)把新设置异步同步到正在播放的 FActiveSound——**”声音播着改衰减”是支持的**(比如角色进入水下把衰减整体调小)。
4.6 监听器与 Focus
- 监听器:
SetListenerFocusParameters(GameplayStatics.cpp:1390)——“音频变焦”:把注意力集中在目标上,其他声音降权(第 10 章的玩法应用); - 距离优先级衰减:
bEnablePriorityAttenuation(SoundAttenuation.h:383-401)——远处声音的优先级也降(与第 8 章并发联动)。
4.7 源码考古:UpdateAttenuation 求值
源码考古:想看真东西的人进。
ActiveSound.cpp:2053-2213(要点):
1
2
3
4
5
6
7
8
9
10
11
12
13
14 void FActiveSound::UpdateAttenuation(...)
{
// 形状求值:球走 AttenuationEval(快速路径),其他形状走 Evaluate(通用)
if (AttenuationSettings->AttenuationShape == EAttenuationShape::Sphere)
VolumeMultiplier *= AttenuationSettings->AttenuationEval(...);
else
VolumeMultiplier *= AttenuationSettings->Evaluate(Origin, Location, ...);
// 混响发送:按距离算 ReverbSendLevel(EReverbSendMethod)
// 优先级衰减:远处降 Priority(与并发联动)
// Focus:听者聚焦目标时调整
// 遮挡:CheckOcclusion(第 6 章)
...
}这段代码在干什么:衰减求值 = 把”距离”翻译成四个参数(音量倍率、混响发送量、
优先级、遮挡量)。注意球形状走专门的快速路径(AttenuationEval)——球是最常用
形状,引擎为它优化。这一函数是第 4/6/7 章的交汇点:衰减、遮挡、混响、优先级
都在这里算好,渲染线程只消费结果。
第 5 章 空间化:把声音”放”进耳朵里
TL;DR|空间化把”方位角”变成”左右耳的信号差”:默认走声像(Panning)——方位角在
相邻扬声器间插值(EqualPower 等功率/Linear 线性);双耳渲染(HRTF)完全交给插件
(本 fork 无内置 HRTF DSP)。立体声声源用 StereoSpread 在空间里分开左右发射器;
近处声音用 NonSpatializedRadius 转 2D 防晕。类比:舞台音箱(Panning)vs
直接塞进耳朵的耳机(HRTF)。
5.1 从位置到方位角
FSoundSource::GetSpatializationParams(Audio.cpp:724)把声源世界坐标 + 听者位置/朝向 → 算出 EmitterPosition(声源相对听者的位置)、EmitterAzimuth(方位角)、Distance——空间化的全部输入。
5.2 Panning:相邻扬声器插值
FMixerDevice::Get3DChannelMap(AudioMixerDevice.cpp:2915):
1 | // 方位角 → 相邻扬声器索引 + 插值比例 |
扬声器方位角表:InitializeChannelAzimuthMap(AudioMixerChannelMaps.cpp:147)——立体声:左声道 270°、右声道 90°(从听者头部朝向算);5.1/7.1 有各自的表。声像 = 按方位角在表里找相邻扬声器、按比例分增益。
5.3 HRTF 路线:插件体系
IAudioSpatialization(IAudioExtensionPlugin.h:245-388)是空间化插件接口:ProcessAudio(InputData, OutputData) 接收”单声道 + 位置参数”,输出”双声道”。什么时候走插件(FMixerSource::UseObjectBasedSpatialization,AudioMixerSource.cpp:2337):
1 | ① 声道数 <= 插件支持上限(默认 1,可扩展 stereo) |
本 fork 事实:au.EnableBinauralAudioForAllSpatialSounds / au.DisableBinauralSpatialization(AudioDevice.cpp:121/128)只是开关——没有内置 HRTF DSP,双耳渲染完全由插件(Windows Spatial Audio / ResonanceAudio / Oculus 等)提供。没装插件 = 选了 HRTF 也白选(回退 Panning)。
5.4 立体声 3D:StereoSpread
FSoundSource::UpdateStereoEmitterPositions(Audio.cpp:625):立体声声源的左右声道在空间里分开(StereoSpread 决定分离宽度,叉积求左右偏移 :644-651),然后左右各算一次方位角——**立体声不是”两个单声道”,而是”空间里左右分开的两个发射器”**。
5.5 近距转 2D:NonSpatializedRadius
NonSpatializedRadiusStart/End(SoundAttenuation.h:254/258)→ NonSpatializedAmount(Audio.cpp:739-779)→ Get3DChannelMap 里 Lerp(pan, omni, OmniAmount)(:3051-3055):声音离耳朵太近时,声像定位反而让人头晕(”声音贴脸转脑袋”)——近距转 2D 让声音”充满”双耳,防晕。
5.6 Ambisonics:环境声场
FWaveInstance::bIsAmbisonics(Audio.h:315):仅支持 FOA(一阶,4 声道);编码走球坐标转换(AudioMixerSourceOutputBuffer.cpp:385),DSP 在 SignalProcessing/DSP/Ambisonics.h,解码由 SoundFieldRendering 模块完成。用途:环境底床(风/雨/人群)——用 Ambisonics 录制的声场比一堆单声道声源更真实。
5.7 源码考古:Get3DChannelMap
源码考古:想看真东西的人进。
AudioMixerDevice.cpp:2915-3010(要点):
1
2
3
4
5
6
7
8
9
10
11
12 void FMixerDevice::Get3DChannelMap(const FWaveInstance* InWaveInstance, ...)
{
// 1. 从方位角表找相邻扬声器(表在 InitializeChannelAzimuthMap)
// 2. 算插值比例 Fraction(方位角在两扬声器之间的位置)
// 3. 等功率/线性插值出两扬声器的增益
if (PanningMethod == EPanningMethod::EqualPower)
{
FMath::SinCos(&NextChannelPan, &PrevChannelPan, Fraction * 0.5f * PI);
}
// 4. 非空间化半径混合:Lerp(pan, omni, OmniAmount)
// 5. 输出声道增益图(每个输出声道一个增益)
}这段代码在干什么:声像的全部数学——方位角 → 相邻扬声器 → 增益对。
EqualPower 的 SinCos 插值保证”声音从左到右移动时总响度不变”(线性插值在
45° 处会有-3dB 哑点)。这个函数是”空间化的默认实现”——HRTF 插件不存在时
它就是 UE 空间音频的全部。
第 6 章 遮挡:隔墙听声
TL;DR|遮挡让”墙后的声音”变闷(低通滤波)变轻(音量衰减):
CheckOcclusion
用物理射线(LineTraceTestByChannel)检测声源与听者之间是否有墙,每 0.1 秒查一次,
结果用插值平滑(防”咔哒”)。类比:隔墙听邻居的电视——听得到在响,听不清在说啥。
6.1 双通道:闷 + 轻
遮挡的效果 = 两个旋钮(SoundAttenuation.h:345-353):
| 字段 | 作用 |
|---|---|
OcclusionLowPassFilterFrequency |
低通频率(闷的程度) |
OcclusionVolumeAttenuation |
音量衰减(轻的程度) |
OcclusionInterpolationTime |
过渡时间(遮挡进出平滑) |
6.2 检测:物理射线
FActiveSound::CheckOcclusion(ActiveSound.cpp:1599):
1 | // 每 OcclusionCheckInterval(默认 0.1s)做一次射线检测 |
要点:OcclusionTraceChannel(默认 Visibility 或 Camera)决定”什么算墙”;bUseComplexCollisionForOcclusion 决定用复杂碰撞还是简单盒;忽略属主 Actor(别被自己的身体挡住)。
6.3 异步路径与平滑
- 异步:
au.UseAsyncOcclusionTraces=1(ActiveSound.cpp:27)时走AsyncLineTraceByChannel(:1659)——射线在物理线程算,结果回调后应用到音频(避免音频线程被物理拖住); - 平滑:
CurrentOcclusionFilterFrequency/CurrentOcclusionVolumeAttenuation按OcclusionInterpolationTime插值(:1674-1694)——**没有插值的话,声音进出墙会”咔哒咔哒”**; - 落地:
FMixerSource::UpdateVolume→SetDistanceAttenuation(AudioMixerSource.cpp:1509)+ 低通SetLPFFrequency(AudioMixerSourceManager.cpp:2053)。
6.4 避坑与性能
- 被自己/头顶遮挡:射线从声源位置到听者位置,如果声源在玩家自己身上(第一人称枪声),射线会被玩家自己的碰撞挡住——
AddIgnoredActor(OwnerID)解决了一半,另一半是把声源位置放在枪口而非玩家中心; - 射线频率:0.1s 一次是默认——遮挡剧烈的场景(墙多)可以调
OcclusionCheckInterval或组件级覆盖; - 关闭:
au.DisableOcclusion=1(ActiveSound.cpp:20)——移动端性能兜底。
6.5 源码考古:CheckOcclusion 三件事
源码考古:想看真东西的人进。
ActiveSound.cpp:1599-1694(要点已在 6.2 展示)。补充插值段:
1
2
3
4 // 用插值时间平滑遮挡结果(防咔哒)
const float InterpTime = OcclusionInterpolationTime > 0.0f ? OcclusionInterpolationTime : 1.0f;
CurrentOcclusionFilterFrequency = FMath::FInterpTo(CurrentOcclusionFilterFrequency, TargetFrequency, DeltaTime, InterpTime);
CurrentOcclusionVolumeAttenuation = FMath::FInterpTo(CurrentOcclusionVolumeAttenuation, TargetVolume, DeltaTime, InterpTime);这段代码在干什么:遮挡不是”开/关”而是”渐变”——
FInterpTo让低通频率和音量
平滑过渡到目标值。**”进墙 0.3 秒内声音逐渐变闷变轻”就是这里实现的**。注意InterpTime的兜底:设 0 时用 1 秒,防止除零。
第 7 章 混响与室内感
TL;DR|混响把”房间”翻译成”回声指纹”:声音按距离算出一个 ReverbSendLevel 发送到
MasterReverbSubmix(板式混响 FPlateReverbFast),AAudioVolume 按玩家所在的声学区域
覆盖混响参数(Priority 仲裁重叠区),FInteriorSettings让室内外过渡带时间渐变。
类比:房间的回声指纹——山洞长回声、卫生间短回声、客厅没回声。
7.1 混响是什么:早反射 + 晚反射
混响 = 声源停止后房间里的声音残留:早反射(前 50ms,几面墙的直接反射,给人”房间大小”感)+ 晚反射(多次反射的尾音,给”材质”感)。UE 用 Audio::FPlateReverbFast(板式混响算法)模拟。
7.2 发送链:从声源到混响子混音
1 | bEnableReverbSend(SoundAttenuation.h:182) |
关键:混响是发送(Send)——原始声音直接进主混音,副本进混响子混音,混合后输出。WetLevel(干湿比)由 ReverbSendLevel 控制。
7.3 AAudioVolume:声学区域
AAudioVolume(AudioVolume.h:152)是声学区域(体积内生效):
| 成员 | 作用 |
|---|---|
Priority |
重叠区域仲裁(高优先级胜出) |
FReverbSettings(ReverbSettings.h:48) |
混响参数(ReverbEffect/Volume/FadeTime) |
FInteriorSettings(:75) |
室内外音量/LPF 渐变 |
SubmixSendSettings |
区域内子混音发送切换 |
FAudioDevice::UpdateAudioVolumeEffects(AudioDevice.cpp:4923)每帧按听者位置查询所在体积并插值。
避坑:老教程的
AReverbVolume(UE4 时代)在本 fork 已删除——功能并入AAudioVolume(FReverbSettings管混响、FInteriorSettings管室内外、Priority
管仲裁)。UAudioTrigger也已删除。见到这两个旧类名按”旧资料”处理。
7.4 室内外过渡
FActiveSound::HandleInteriorVolumes(ActiveSound.cpp:1708):声源与听者是否同体积 → InteriorVolume/ExteriorVolume/InteriorLPF 带时间渐变(InteriorTime/ExteriorTime)——**”出门瞬间声音从闷变亮”是渐变的,不是跳变的**。
7.5 房间”指纹”配方表(供第 10 章玩法用)
| 空间 | DecayTime | Density | 效果 |
|---|---|---|---|
| 小房间/卫生间 | 0.3~0.6s | 高 | 清脆短回声 |
| 大房间/大厅 | 1.0~1.5s | 中 | 明显尾音 |
| 山洞/隧道 | 2.0~3.0s | 低 | 长而空旷 |
| 室外(默认) | 0.2s | 高 | 几乎无回声 |
7.6 源码考古:发送点与室内外
源码考古:想看真东西的人进。
① 混响发送点,
AudioMixerSource.cpp:1636-1637:
1
2 // Send the source audio to the master reverb
MixerSourceVoice->SetSubmixSendInfo(MixerDevice->GetMasterReverbSubmix(), WaveInstance->ReverbSendLevel);这段代码在干什么:每个声源在渲染线程把自己的副本发送到混响子混音——
“发送量(ReverbSendLevel)”是音频线程在 UpdateAttenuation 算好的(按距离/曲线)。
**混响不改变原始声音,只叠加一个”回声副本”**。② 室内外渐变,
ActiveSound.cpp:1708-1769(要点):
1
2
3 // 声源与听者是否在同一音量区域 → 决定用 InteriorVolume 还是 ExteriorVolume
// 带 InteriorTime/ExteriorTime 渐变(FInterpTo)
// 跨区时低通取更小者(最闷的规则生效)这段代码在干什么:”室内外”不是开关而是两组渐变参数——进门时音量与低通
平滑过渡到室内值。跨区取”更闷”的规则保证从亮处看暗处时声音不会突然变亮。
第 8 章 并发与优先级:电话占线
TL;DR|混音器有预算(
MaximumConcurrentStreams),超预算时要裁决:FSoundConcurrencySettings
的 MaxCount + 七种 ResolutionRule(阻止/停最老/停最远/停最低优先级…)+ 四种作用域
(Group/Owner/OwnerPerSound/Sound);Ducking是”不挂断只调小”(语音压 BGM)。
全局兜底:GetVolumeWeightedPriority(音量×优先级)排序截断。类比:电话占线——
挂谁由规则定。
8.1 问题:混音器预算
AudioSettings->MaximumConcurrentStreams(AudioSettings.h:199)是全局同时发声上限(默认约 32-128 视平台)。GetSortedActiveWaveInstances(AudioDevice.cpp:4159)按 GetVolumeWeightedPriority()(Audio.h:482,音量 × 优先级)排序,超出预算的低优先级 WaveInstance 直接不启动。
8.2 FSoundConcurrencySettings:并发组
FSoundConcurrencySettings(SoundConcurrency.h:73):
| 字段 | 含义 |
|---|---|
MaxCount / PlatformMaxCount |
组内最大并发数(平台缩放) |
bLimitToOwner |
按发声 Actor 限流(每人只能有 N 个脚步声) |
ResolutionRule |
七种裁决规则(见 8.3) |
RetriggerTime |
拒播后的冷却(防连点刷屏) |
VolumeScale |
Ducking 音量(老声音避让) |
VoiceStealReleaseTime |
被偷声时的淡出时间(防咔哒) |
8.3 七种裁决规则(决策表)
| ResolutionRule | 行为 | 适用 |
|---|---|---|
PreventNew |
新声音直接拒播 | 关键声音保护(不打断) |
StopOldest |
停最老的 | 循环声(旧的停掉新的接上) |
StopFarthestThenPreventNew |
先停最远的,还满就拒新的 | 通用 |
StopFarthestThenOldest |
停最远的(默认) | 脚步声/枪声 |
StopLowestPriority |
停最低优先级的 | 优先级体系完善时 |
StopQuietest |
停最安静的 | 保持”响的声音” |
StopLowestPriorityThenPreventNew |
停低的,还满就拒 | 混合策略 |
8.4 四种作用域
EConcurrencyMode(SoundConcurrency.h:233):Group(全局一组)/ Owner(每个发声 Actor 一组)/ OwnerPerSound(每个 Actor × 每个声音)/ Sound(每个声音全局)。**”每人最多 3 个脚步声”= Owner 作用域**。
8.5 Ducking:不挂断只调小
VolumeScale(+ EConcurrencyVolumeScaleMode:Default/Distance/Priority):同组声音达到上限时,老声音不停止而是被调小(Ducking)——“语音通话时 BGM 自动变小”的标准实现(第 10 章玩法)。
8.6 求值链
FSoundConcurrencyManager::EvaluateConcurrency(SoundConcurrency.cpp:818)在 AddNewActiveSound 时求值:CanPlaySound(:654,MaxCount + RetriggerTime)→ 超限则 CreateAndEvictActiveSounds(:901,按规则挑选被驱逐者 + VoiceStealReleaseTime 淡出)。
8.7 全局预算与优先级
Priority:USoundBase::Priority(SoundBase.h:218)+ 组件bOverridePriority(AudioComponent.cpp:776-783)+ 距离优先级衰减(第 4.6 章)——优先级 = 被保住的资格;au.EnableRelativeRenderCostVoiceLimit(AudioDevice.cpp:205):按渲染成本(解码格式/声道数)而非纯数量计预算——移动端关键优化项(ADPCM 便宜、PCM 贵,同样预算能装更多 ADPCM);au.SetAudioChannelCount(:44):运行时改全局通道数(测试用)。
8.8 源码考古:EvaluateConcurrency
源码考古:想看真东西的人进。
SoundConcurrency.cpp:818-901(要点):
1
2
3
4
5
6
7
8
9 bool FSoundConcurrencyManager::EvaluateConcurrency(FActiveSound& NewActiveSound, ...)
{
// 1. 找到/创建并发组(按 EConcurrencyMode 作用域)
// 2. CanPlaySound:MaxCount 未满 → 直接通过
// 3. 满了 → 按 ResolutionRule 挑被驱逐者:
// StopOldest → 最老的;StopFarthest → 最远的;
// StopLowestPriority → FCompareActiveSounds 排序取最低
// 4. CreateAndEvictActiveSounds:停掉被驱逐者(带淡出)+ 新声音入组
}这段代码在干什么:并发裁决 = “组”(作用域)+”规则”(ResolutionRule)+”执行”
(偷声淡出)三段式。注意偷声带VoiceStealReleaseTime淡出——直接掐断会咔哒。
这就是”同屏 50 个枪声只响 20 个”的机制:谁响由优先级+距离+新旧共同决定。
第 9 章 使用篇:从零开始配置一个 3D 声源
TL;DR|把一个声音变成”听声辨位”的 3D 声源需要五步:素材准备(单声道优先、压缩选型)
→ 资产三件套(SoundWave → SoundCue → SoundAttenuation)→ 衰减配置(曲线/形状/范围)
→ 空间化配置(bSpatialize/近距转 2D)→ 摆放播放(组件 or 静态函数)。配完衰减后
再配遮挡与混响。本章末尾给一个”篝火声”完整配方。类比贯穿:音量地图 → 调音台推子 →
接线板。
9.1 素材准备:单声道是空间化的前提
空间化 = 把一个位置变成左右声道差——所以 3D 声源素材必须是单声道(Mono)。立体声素材做 3D 会用 StereoSpread 拆左右(第 5.4 章),效果不如单声道干净。多声道环绕素材不适合做 3D 声源。
压缩格式选型(内存估算公式:采样率 × 位深 × 声道 × 时长 / 8,标注:社区实测数据,非本仓库实测):
| 格式 | 体积 | 质量 | 适用 |
|---|---|---|---|
| 未压缩 PCM | 10.5MB/分钟(48kHz 16bit 单声道) | 最高 | 极短关键音(枪声) |
| ADPCM | 1/4 体积 | 可接受 | 中短音效(脚步/UI) |
| Vorbis | 1/10 体积 | 好 | 通用压缩 |
| Opus | 更小 | 好(流送友好) | 长环境音流送 |
流送开关:USoundWave::bStreaming(SoundWave.h:455,注释 “Here for legacy code”)——长音频(环境底床/音乐)开流送,内存只占一小块缓冲;短音效预载(第 11 章详述)。
9.2 资产三件套
1 | USoundWave(波形数据)──引用──▶ USoundCue(播放编排:节点图)──引用──▶ USoundAttenuation(衰减/空间化/遮挡/混响配置) |
USoundCue(SoundCue.h:90):FirstNode(:95)是节点图入口——放一个 WavePlayer 节点就成;USoundAttenuation(SoundAttenuation.h:453):纯配置资产,可被多个声音共享。
分层资产策略:通用衰减资产(”通用枪声衰减”)供同类声音共享——改一处,全局生效;特殊声音用专属资产。这比每声音手调强得多(第 12 章数据驱动的雏形)。
9.3 衰减资产配置
| 设置 | 推荐 | 理由 |
|---|---|---|
| DistanceAlgorithm | 枪声/爆炸:Inverse;环境音:NaturalSound;通用:Logarithmic | 见第 4.2 章曲线表 |
| AttenuationShape | 通用 Sphere;定向声源 Cone | Cone 给喇叭/对话 |
| FalloffDistance | 按玩法定(射击 5000-10000;对话 1000-2000) | 衰减过渡带 |
| MaxDistance | = FalloffDistance 终点 | 同时是性能门控边界 |
避坑:
MaxDistance不只是”听不到就不响”——它同时决定SoundIsAudible的
性能门控(听不见的声音引擎直接跳过创建)。衰减范围画大了,性能开销也大
(更多声音进入”可听”范围)。大世界请给”远处也能听到”的声音单独设计(如
枪声用 Inverse 曲线天然远处小声,而不是把 MaxDistance 拉爆)。
9.4 空间化配置
| 设置 | 推荐 | 说明 |
|---|---|---|
bSpatialize(SoundAttenuation.h:158) |
true | 3D 声源总开关 |
SpatializationAlgorithm(:210) |
Panning(默认)或 HRTF | HRTF 需要插件(第 5.3 章) |
BinauralRadius(:218) |
视玩法 | HRTF 生效半径 |
NonSpatializedRadiusStart/End(:254/:258) |
50/100 | 近处转 2D 防晕(贴脸声音不转脑袋) |
StereoSpread(:266) |
0.2~0.5 | 立体声素材的左右分离宽度 |
避坑:选了 SPATIALIZATION_HRTF 但没启用空间化插件 → 静默回退 Panning(第 5.3 章)——调试时用 au.3dVisualize.SpatialSources(AudioDebug.cpp:79)看哪些声源真的走了 HRTF。
9.5 遮挡与混响配置
- 遮挡(
SoundAttenuation.h:345-353):bEnableOcclusion=true+OcclusionTraceChannel(选 Visibility 或自定义通道)+ 低通频率(2000Hz)+ 音量衰减(0.3)+ 插值时间(0.3s); - 混响:
bEnableReverbSend=true(:182)——声音自动按距离发送到 MasterReverbSubmix(第 7 章)。
避坑:遮挡射线通道别用 ECC_WorldStatic 以外的奇怪通道——射线只测配置的通道,墙的碰撞通道必须匹配,否则”穿了墙还清脆”。
9.6 摆放与播放:组件 vs 静态函数
| 场景 | 用 | 原因 |
|---|---|---|
| 一次性事件(开火/拾取) | PlaySoundAtLocation |
无需持有,播完自动清理 |
| 持续声音(引擎/环境/对话) | UAudioComponent |
需要停止/淡出/换声/参数 |
| 需要参数化(音量/音高/自定义参数) | UAudioComponent |
完整参数接口 |
组件挂法:附加到发声 Actor(枪口组件、引擎位置)——Sound 属性 + bAutoActivate;代码用 Play/Stop/FadeIn/FadeOut(AudioComponent.h:500-583)。
9.7 SoundCue 节点图:播放编排
USoundNode(SoundNode.h:57)节点体系(ChildNodes + DECLARE_SOUNDNODE_ELEMENT 每实例状态 :26-54):
1 | WavePlayer(基础播放) |
玩法技巧:USoundNodeDistanceCrossFade(SoundNodeDistanceCrossFade.h:82)——脚步声的近/远两个变体交叉淡化,比单一声音+衰减真实得多。
9.8 参数化:运行时的”旋钮”
ISoundParameterControllerInterface(SoundParameterControllerInterface.h:24)是统一的参数接口:
1 | // AudioComponent 上直接用: |
FAudioParameter(AudioParameter.h:92,5.9 已迁到 AudioExtensions 模块)是参数的传输结构。同一套参数通路:SoundCue 的 WaveParam/ParamCrossFade 节点消费它,MetaSound 的 FRuntimeInput 也消费它——换引擎系统不改玩法代码。
9.9 音量分层:调音台推子
SoundClass 树(SoundClass.h:208):
1 | Master |
动态混音(GameplayStatics.h:881-937):SetBaseSoundMix / PushSoundMixModifier / PopSoundMixModifier / SetSoundMixClassOverride(带 bApplyToChildren=true)——进战斗推一个 Mix(音乐 -50%),出战斗 Pop。
9.10 动手实验室:篝火声(五步完整配方)
| 步骤 | 资产/设置 | 验收标准 |
|---|---|---|
| ① 素材 | 火苗 loop(单声道、Opus、流送)+ 2~3 个噼啪 one-shot(单声道、Vorbis) | 素材可独立播放 |
| ② Cue | Cue_Fire:Looping 包火苗;Cue_Crackle:Random 包噼啪 |
两 Cue 均响 |
| ③ 衰减 | Atten_Fire:Sphere、NaturalSound、FalloffDistance=1500、bSpatialize=true、NonSpatializedRadius 50/100、bEnableOcclusion=true、bEnableReverbSend=true |
走近变响、绕圈方向感正确、隔墙变闷 |
| ④ 摆放 | 篝火 Actor 上挂 AudioComponent(Sound=Cue_Fire,bAutoActivate);噼啪用蓝图定时 PlaySoundAtLocation(Cue_Crackle, 火堆位置) |
火苗持续响,噼啪随机响 |
| ⑤ 验收 | 玩家绕篝火走一圈:音量渐变、方向清晰、隔墙闷、室内回声 | 全部符合 = 空间音频四件套全生效 |
检查清单:au.3dVisualize.Attenuation=1 看衰减球;stat audio 看这个声音占了几个 WaveInstance;确认没被并发规则吞掉(第 8 章)。
第 10 章 玩法结合篇:事件驱动音频、多人同步、环境与区域
TL;DR|音频与玩法结合的三原则:事件驱动(玩法发事件,音频响应——不轮询)、
多人同步(UAudioComponent 无复制——用 unreliable NetMulticast RPC 播声音,服务器
只裁剪广播,不占游戏带宽)、环境区域(AAudioVolume 让”进山洞声音就变”自动发生)。
类比:音频是玩法的”信号灯”——玩家靠耳朵判断局势,你的代码负责让信号灯亮对。
10.1 事件驱动哲学:谁触发声音
原则:玩法逻辑发事件,音频系统响应事件——而不是音频自己轮询状态。
1 | ✅ 推荐:伤害结算 → OnDamaged 事件 → PlaySoundAtLocation(命中音, 位置) |
PlaySound2D / PlaySoundAtLocation(GameplayStatics.h:629/681)就是”事件出口”。一音多用:同一个枪声 Cue 被开火/命中/敌人/队友共用——资产复用是音频的带宽(这里指内容量)。
10.2 音频与玩法解耦:接口层
玩法代码不应该贴资产名(”PlaySoundAtLocation 传那个枪声资产”)——玩法发语义事件,音频层映射资产(第 12 章封装详述)。中间层方案:事件分发器(GameplayMessage 模式)、接口(IAbilityAudioEvent)、或简单的枚举参数。
“音频即信号”:玩家凭声音判断局势——脚步渐近(方向+距离)、枪声方位(听声辨位)、Boss 技能前摇音(预警)。空间音频做得好的游戏 = 玩家可以闭眼打。Friendslop 这类多人射击正是这个理念的极致。
10.3 多人同步:UAudioComponent 没有复制(本章核心)
已实测:UAudioComponent 没有 GetLifetimeReplicatedProps/DOREPLIFETIME——声音不会自动同步。标准做法:
服务器(事件发生:玩家 A 开火) 客户端 B(听到)
│ │
│ ① Multicast_PlaySound(unreliable RPC)│
│ ────────────────────────────────────▶ │
│ 参数:Cue 引用 + 世界位置 │ ② 本地 PlaySoundAtLocation
│ │ → 衰减/空间化/遮挡全套本地计算
│ (服务器本地也播——Multicast 语义) │ → 音画同步(各自本地计算延迟最低)
1 | UFUNCTION(NetMulticast, Unreliable) |
要点(与《UE 网络架构完全教程》Ch6 呼应):
- Unreliable:声音事件丢了就丢了(下一秒可能又来一个)——别用 Reliable 占游戏可靠带宽;
- 只传”事件 + 位置”:声音内容各客户端本地有(资产)——不占游戏带宽(声音数据不走网络,走资产分发);
- OwningActor 参数:
PlaySoundAtLocation的 OwningActor 影响服务器相关性裁剪(第 7 章 Relevancy)——传对 OwningActor,不然服务器会漏广播; - 策略差异:一次性事件(枪声)用 RPC;持续声音(引擎轰鸣)用”拥有者的 AudioComponent + 位置属性复制”或本地预测——别把循环声也 RPC 每帧发。
10.4 听者管理
- Listener 绑玩家 Pawn:默认监听器跟随主玩家——切观战/死亡时
SetListener切到目标; - 2D UI 音:
bIsUISound=true(SoundClass.h:103)的声音不空间化不衰减——菜单点击、血条警告走 UI SoundClass。
10.5 环境与区域:AAudioVolume
AAudioVolume(AudioVolume.h:152)是”声学区域”:
1 | 山洞体积(Priority=10):ReverbSettings=长回声(DecayTime 2.5s)+ InteriorSettings=闷 |
玩法价值:玩家进洞瞬间声音自动”变山洞”——空间感是自动的(UpdateAudioVolumeEffects 每帧按听者位置插值),不需要代码。水下(全部声音低通+慢速)、密闭舱室(强混响)、走廊(中混响+轻微遮挡)都是同一个配方。
10.6 空间化的玩法价值:听声辨位
| 玩法 | 音频实现 |
|---|---|
| 敌我定位 | 脚步声/枪声走完整空间化(HRTF 插件效果最佳) |
| 注意力引导 | SetGlobalListenerFocusParameters(GameplayStatics.cpp:1390):聚焦目标 → 其他声音降权(”音频变焦”) |
| 预警 | 远处枪声 = Inverse 衰减 + 低通(先听到闷响,近了才清脆) |
| 状态提示 | 心跳声(低血量)用 2D 但带节奏变化——“状态信号”与”空间信号”分开 |
10.7 动态混音:Boss 战压低 BGM
1 | 进战斗:PushSoundMixModifier(Mix_Combat) → Music -50%,SFX +20% |
语音 Ducking(第 8.5 章):语音 SoundClass 与 BGM 同组 Concurrency + VolumeScale——语音一响,BGM 自动变小(不挂断只调小)。
10.8 语音聊天:多人游戏的”最后一层空间音频”
- 网络层:
UVoiceChannel(VoiceChannel.h:19)——网络文档里”Voice 通道”的实体; - 采集/编解码:
FVoiceModule::CreateVoiceCapture/Encoder/Decoder(VoiceModule.h:70-91,Opus 实现); - 音频侧:
VoiPSoundClass(AudioSettings.h:156)——语音独立 SoundClass,可单独调音量。
玩法:队友语音做空间化(队友在左边说话,声音从左边来)——Friendslop 类游戏的沉浸感关键。
10.9 音乐系统
- bIsMusic(SoundClass.h:107):音乐独立 SoundClass 层(不衰减不遮挡);
- 2D 音乐 + 3D 环境音分层:音乐永远 2D(全景),环境音 3D(空间)——混音台思路;
- 状态驱动切换:低血量→紧张音乐、Boss→战斗音乐——用 SoundMix 渐变而非硬切(硬切 = 音乐”啪”地换,出戏)。
10.10 动手实验室:多人射击脚步声方案
| 需求 | 方案 |
|---|---|
| 脚步声事件 | 移动组件在”落地/步进”事件时触发(不轮询) |
| 多样化 | Cue:Random 包 3~4 个脚步变体 |
| 距离与方向 | 完整衰减(FalloffDistance 按步频调)+ 空间化(HRTF 插件) |
| 并发控制 | Concurrency:Owner 作用域、MaxCount=2、StopFarthestThenOldest(每人最多 2 个脚步同时响) |
| 蹲伏/奔跑 | SetFloatParameter(“StepSpeed”) 驱动 ParamCrossFade 切换变体 |
| 多人同步 | NetMulticast Unreliable RPC(第 10.3 章) |
| 队友/敌人区分 | 敌人脚步 Focus 高亮(SetGlobalListenerFocusParameters)、队友脚步降权 |
| 区域 | 门内门外 AAudioVolume:遮挡+混响自动 |
第 11 章 优化篇:并发预算、流送、虚拟循环、移动端
TL;DR|音频优化的三道闸:并发预算(设计期 Concurrency + 运行时通道上限,按渲染
成本计权重)、流送(长音流送短音预载,au.streamcaching 缓存)、虚拟循环(远处
循环声降频更新——“放假”)。移动端还有中断处理与低延迟路径。类比:电话占线 →
仓库管理 → 远处声音放假。
11.1 性能账本:音频 CPU 三花销
| 花销 | 内容 | 看什么 |
|---|---|---|
| 解码 | 压缩音频 → PCM(CPU 最重项) | Opus/Vorbis 解码开销 |
| 逐源 DSP | 衰减增益 + 滤波 + 空间化 + 混响发送 | 每源固定成本 |
| 插件 | HRTF 等双耳渲染 | 每源 3-5ms 预算(社区数据,非本仓库实测) |
stat audio(Audio.h:32-39):ActiveSounds(活动声音数)/ Sources(实际发声数)/ VirtualLoops(虚拟循环数)/ MaxChannels(上限)——第一眼就看这四个数。
11.2 并发预算(第一道闸)
1 | 设计期:每个声音资产配 Concurrency(MaxCount + 规则)——"枪声最多 8 个同时" |
实践:50 个敌人同时开枪 = 50 个枪声请求 → 并发组截到 8 个 + 全局预算再截——**玩家听到的永远是”最近的 + 最重要的”**。
11.3 流送(第二道闸)
- 原则:长环境音全部流送、短事件音全部预载(社区实测经验,非本仓库实测);
bStreaming(SoundWave.h:455)开流送;FAudioChunkCache(AudioStreamingCache.cpp)管理流送缓存,au.streamcaching.*CVar 家族(StreamCacheSizeOverrideMB:139 等)控制缓存大小;- 避坑:流送声音有”首次播放延迟”(读盘)——关键音效(枪声)千万别流送,开局爆音。
11.4 虚拟循环(第三道闸)
FAudioVirtualLoop(AudioVirtualLoop.cpp):
1 | 远处循环声(瀑布/风声/远处引擎): |
收益:100 个远处循环声 × 每帧更新 = 灾难;100 个 × 每 3 秒一次 = 几乎免费。虚拟循环是”大世界环境音”的默认解。
11.5 内存:预载 vs 流送
- 预载:
FAudioDevice::Precache(AudioDevice.h:625)——启动/关卡加载时把短音效解压进内存; - FActiveSound 无对象池:
new FMixerSource(AudioMixerDevice.cpp:1546)直接堆分配——引擎不做池(生命周期差异大/线程交接复杂),高频创建销毁(枪声连发)时自建池(第 12.10 章的 AudioManager 里做)。
11.6 遮挡与混响开销
OcclusionCheckInterval(ActiveSound.cpp:197,默认 0.1s):墙多场景调大到 0.2-0.3s;au.UseAsyncOcclusionTraces=1(:27):射线走异步(物理线程算,不卡音频);- 混响发送是”每源一次 SetSubmixSendInfo”——批量不会太贵,但别给装饰声开混响。
11.7 移动端
- 中断处理:来电/挂断自动静音(
FCoreDelegates::AudioInterruptionDelegate,CoreDelegates.h:394;au.DisableAudioSuspendOnAudioInterrupt可关); - 预算:移动端 HRTF 每源 3-5ms(社区数据)——移动端优先 Panning,HRTF 只给关键声源(队友语音);
- 渲染成本计权:
au.EnableRelativeRenderCostVoiceLimit=1让 ADPCM 声源”便宜”(同预算装更多)。
11.8 调试工具链
| 工具 | 看什么 |
|---|---|
au.3dVisualize.ActiveSounds(AudioDebug.cpp:55) |
场景里活动声音的 3D 球体(5 种显示模式) |
au.3dVisualize.Attenuation(:744) |
衰减形状(配合 SoundBase::bDebug 标记单个声音) |
au.3dVisualize.SpatialSources(:79) |
空间化声源 |
au.DumpActiveSounds(:676) |
全部活动声音清单(并发/预算诊断) |
stat audio |
核心计数器(11.1) |
| Audio Insights 插件 | UE_TRACE 时间线(声音生命周期/线程分布) |
11.9 症状 → 定位表
| 症状 | 大概率原因 | 检查 |
|---|---|---|
| 声音被吞 | 并发规则(PreventNew)或预算截断 | au.DumpActiveSounds + Concurrency 设置 |
| 爆音/卡顿 | 流送饥饿(流送缓存太小/磁盘慢) | au.streamcaching.* + 日志 |
| 掉帧 | 插件 DSP 太重(HRTF 全开) | 统计插件耗时 + 降级 Panning |
| 内存上涨 | 流送覆盖不全(长音没流送) | 检查 bStreaming 覆盖 |
| 方向感丢失 | bSpatialize 没开 / HRTF 插件没启用 | au.3dVisualize.SpatialSources |
| 遮挡不生效 | 射线通道不匹配 | OcclusionTraceChannel vs 墙的碰撞通道 |
11.10 动手实验室:开放世界 500 声源预算分解
| 类别 | 数量 | 策略 | 预算 |
|---|---|---|---|
| 关键事件(枪声/命中/语音) | ~20 | 预载 + 高优先级 + HRTF | 全量保 |
| 近处环境(脚步/道具) | ~50 | 预载 + Concurrency 组 | 正常 |
| 中距(敌人动作) | ~100 | 预载 + 并发截断 | 截到 40 |
| 远处循环(瀑布/风声) | ~300 | 流送 + 虚拟循环 | 几乎免费 |
性能测试五步法:① 基线(空场景 stat audio)→ ② 加压(500 声源场景)→ ③ 定位(DumpActiveSounds 找吞声)→ ④ 调参(并发/流送/虚拟循环)→ ⑤ 回归(对比基线)。
第 12 章 封装篇:AudioManager 设计模式、数据驱动、接口抽象
TL;DR|引擎没有”音频管理器”——只有设备层(FAudioDeviceManager/FAudioDevice/FAudioThread)
三件套。游戏需要自己的封装,分三层:配置数据层(UDeveloperSettings/DataAsset 存
“事件 → 资产”映射)、运行时调度层(WorldSubsystem 管生命周期与混音)、接口抽象层
(玩法代码只调 Play2D/PlayAtLocation/StopByTag,不贴资产名)。参考 Lyra 的
ULyraAudioSettings + ULyraAudioMixEffectsSubsystem。类比:**音频管家——玩法说
“我要脚步声”,管家负责”放哪个资产、多大声、什么时候停”**。
12.1 引擎里没有”音频管理器”
| 引擎组件 | 角色 |
|---|---|
FAudioDeviceManager(AudioDeviceManager.h) |
多音频设备生命周期管理 |
FAudioDevice(非 UObject) |
单设备的音频引擎(播放/求值/混合) |
FAudioThread(AudioThread.h:21) |
线程命令队列(RunCommandOnAudioThread :58) |
**引擎给的是”设备”不是”服务”**——就像网络篇说”引擎没有网关服务器”:设备层管”怎么响”,”什么时候响、响哪个、多大声”是游戏的责任。
12.2 为什么游戏需要封装
三个理由(对应三层):
- 玩法代码不该贴资产名(”PlaySoundAtLocation(枪声_Cue)”在 50 个地方 = 换枪声改 50 处)→ 配置数据层;
- 生命周期要管理(场景切换清场、Travel 清场、音量分层状态)→ 运行时调度层;
- 玩法只要语义(”我要脚步声”而不是”播这个资产”)→ 接口抽象层。
12.3 配置数据层:事件 → 资产映射
Lyra 实证:ULyraAudioSettings(LyraAudioSettings.h:31,UDeveloperSettings,config=Game)——开发者设置存软引用(延迟加载,不拖慢启动)。
通用设计:
1 | UCLASS(config=Game, defaultconfig) |
注意:引擎没有 UDataAsset 音频资产先例(USoundConcurrency/USoundAttenuation 是纯 UObject 配置资产)——数据驱动用 DeveloperSettings + 软引用(Lyra 模式),或自建 UDataAsset。
12.4 运行时调度层:WorldSubsystem
Lyra 实证:ULyraAudioMixEffectsSubsystem(LyraAudioMixEffectsSubsystem.h:41,UWorldSubsystem)——世界生命周期里应用 Mix。
通用设计:UGameAudioSubsystem : UWorldSubsystem(注意:引擎没有 AudioSubsystem 基类,用 UWorldSubsystem)——持有当前 Mix 状态、管理全局听者、处理场景切换清场。
12.5 统一事件 API:玩法只调语义
1 | // 接口层:玩法代码只认识这些 |
内部路由:查配置层映射 → UGameplayStatics::PlaySoundAtLocation 或 UAudioComponent——换资产只改配置,不动玩法代码。
12.6 参数路由:事件参数 → FAudioParameter
1 | 玩法:PlayAtLocation(Event_Footstep, Pos) + SetParam("StepSpeed", 2.0f) |
玩法语义与音频参数解耦:玩法说”奔跑”,音频层翻译成”StepSpeed=2.0”。
12.7 生命周期管理
| 时机 | 动作 |
|---|---|
| 场景切换/ServerTravel | FadeAll(0.5s 淡出)+ 停止非持久声音 |
| 玩家死亡/观战 | 听者切到目标 + 停止玩家专属声音 |
| 暂停菜单 | 音乐继续(2D)+ 音效暂停(按 SoundClass 分组控制) |
| 世界清理 | Subsystem 的 OnWorldBeginPlay/Deinitialize 挂钩 |
12.8 混音层封装:场景函数
1 | 进战斗:ApplyCombatMix() → PushSoundMixModifier(Mix_Combat) |
音量分级表(社区经验,非本仓库实测):整体/音乐/音效/语音四档独立音量(存 ini)——对应 SoundClass 树的四个推子。
12.9 数据驱动与热更新
- 热更新(引擎机制):播放中
SetAttenuationOverrides走SendCommandToActiveSounds(AudioComponent.cpp:1367-1388)——游戏侧复刻:改配置 → 通知 AudioManager → 广播到活动声音; - 开发工具:控制台命令
Audio.Debug.Event <Event>显示”这个事件 → 哪个资产 → 哪个 Concurrency”——配置层可查性是音频封装的隐藏价值。
12.10 动手实验室:200 行 AudioManager 骨架
1 | // ===== 配置数据层 ===== |
与 Lyra 结构对照:配置层 ↔ ULyraAudioSettings;调度层 ↔ ULyraAudioMixEffectsSubsystem;接口层 ↔ Lyra 的玩法侧调用点。
反模式清单:① God Object(一个类管所有音频逻辑);② 字符串拼资产路径(LoadObject<USoundBase>(nullptr, TEXT("/Game/Audio/..."))——用软引用映射);③ 玩法代码直接 new AudioComponent 不回收(生命周期泄漏);④ 每个声音单独手调衰减(不建共享资产)。
第 13 章 平台支持与性能调优
TL;DR|音频 CVar 分七组:空间化 / 遮挡 / 混响 / 流送 / 虚拟循环 / 并发 / 调试。
调优三板斧:stat audio看数字 →au.3dVisualize看画面 → 单旋钮调参。
常见坑十条(从”听不见”到”CPU 爆炸”)。
13.1 CVar 速查表(七组,写作时已 grep 核实默认值)
空间化:au.EnableBinauralAudioForAllSpatialSounds=0(AudioDevice.cpp:121)、au.DisableBinauralSpatialization=0(:128)、au.spatialization.SetCurrentSpatialPlugin(控制台命令,:179)
遮挡:au.DisableOcclusion=0(ActiveSound.cpp:20)、au.UseAsyncOcclusionTraces=false(:27)
混响:au.DisableReverbSubmix=0(AudioMixerSubmixEffectReverb.cpp:24)
流送:au.streamcaching.StreamCacheSizeOverrideMB=0(AudioStreamingCache.cpp:139)、au.streamcache.SoundWaveDefaultLoadingBehavior(SoundWave.cpp:65)、au.DisableAutomaticPrecache(AudioDevice.cpp:75)
虚拟循环:au.VirtualLoops.UpdateRate.Min/Max=0.1/3.0(AudioVirtualLoop.cpp:30-40)
并发/预算:au.SetAudioChannelCount=0(AudioDevice.cpp:44)、au.EnableRelativeRenderCostVoiceLimit=1(:205)、au.DisableStoppingVoices=0(:59)
调试:au.3dVisualize.Enabled=0(AudioDeviceManager.cpp:49)、au.3dVisualize.ActiveSounds=0(AudioDebug.cpp:55)、au.3dVisualize.Attenuation=0(:744)、au.DumpActiveSounds(:676,命令)
13.2 调优工作流:三板斧
1 | 1. 看数字 stat audio(ActiveSounds/Sources/VirtualLoops/MaxChannels) |
13.3 常见坑清单
- 听不见:
MaxDistance太小 / 并发PreventNew吞了 / 音量被 SoundClass 层叠压低——三步排查(先au.DumpActiveSounds看有没有创建); - 方向感丢失:
bSpatialize没开,或 HRTF 选了但插件没启用(第 5.3 章静默回退); - 爆音:流送缓存不足 / 大量同帧预载——枪声别流送;
- **遮挡”穿了墙”**:射线通道与墙碰撞通道不匹配(第 6.4 章);
- 进房间没混响:MasterReverbSubmix 默认资产没建 /
bEnableReverbSend没开 / AAudioVolume 没放; - 立体声素材空间化效果差:3D 声源用单声道素材(第 9.1 章);
- 多人声音不同步:UAudioComponent 不复制——忘了 RPC 广播(第 10.3 章);
- 切场景声音残留:AudioManager 没清场(第 12.7 章);
- 每帧改音量:
SetVolumeMultiplier高频调用走命令队列——批量/用插值(引擎有 Fade 内置); - 移动端爆 CPU:HRTF 全开 + 流送不足——移动端 Panning 优先 + ADPCM(第 11.7 章)。
第 14 章 局限、替代方案与展望
TL;DR|UE 空间音频的局限:无内置 HRTF/ITD(双耳渲染靠插件)、单一听者(分屏/观战
需手动切换)、Ambisonics 仅 FOA、无引擎级音频管理器(游戏自建)、FActiveSound
无池。替代路径:Wwise/FMOD 中间件、ResonanceAudio、Windows Spatial Audio。方向:
MetaSound 程序化音频 + Audio Gameplay Volumes 区域化。
14.1 局限与对策
| 局限 | 表现 | 对策 |
|---|---|---|
| 无内置 HRTF/ITD | 双耳渲染依赖插件 | 启用 Windows Spatial Audio/ResonanceAudio 插件;移动端 Panning 兜底 |
| 单一听者 | 分屏/观战只跟主玩家 | SetListener 手动切换(第 10.4 章) |
| Ambisonics 仅 FOA | 环境声场只有一阶 | 高阶声场用中间件 |
| 无引擎级音频管理器 | 生命周期/数据驱动要自建 | 第 12 章三层封装 |
| FActiveSound 无池 | 高频创建销毁有分配开销 | AudioManager 里自建池(第 11.5 章) |
14.2 替代路径
| 方案 | 定位 | 与 UE 内置的关系 |
|---|---|---|
| Wwise / FMOD | 专业音频中间件(音频管线/混音/素材管理) | 通过 AudioLink 接入(引擎无内置集成) |
| ResonanceAudio | 谷歌空间音频插件(HRTF/反射) | UE 插件(Plugins/Runtime/ResonanceAudio) |
| MicrosoftSpatialAudio | Windows 空间音频(HRTF) | UE 插件 |
| AudioModulation | 参数调制(引擎插件) | 声音参数的数据驱动调制 |
14.3 演进时间线与阅读进阶
| 时间 | 里程碑 |
|---|---|
| UE4 早期 | 平台专属音频(XAudio2/OpenAL 等) |
| 4.20+ | AudioMixer 统一渲染器 |
| 5.0~5.3 | MetaSound 引入、Audio Gameplay Volumes 起步 |
| 5.4~5.9 | 音频流缓存、Audio Insights、区域音频成熟 |
| 未来 | MetaSound 转正、空间音频插件生态(Oculus/PS5 3D Audio) |
进阶路线:Epic 官方文档(Sound Concurrency → MetaSounds → Audio Gameplay Volumes → 流缓存)→ 中文深挖(附录 C)→ 本仓库源码(附录 A 指路)→ 中间件对比(Wwise 的 Spatial Audio 文档)。
附录 A 源码地图
全部路径基于本仓库快照(2026-08-26,HEAD
6cea9bd20f8f)。行号只用于定位;代码演进后以函数名为准。
阅读顺序建议:先ActiveSound.cpp(求值核心)→AudioDevice.cpp(播放链路)→AudioMixerDevice.cpp(渲染线程)→ 按章节深入。
A.1 引擎核心(Runtime/Engine)
| 文件 | 关键内容 |
|---|---|
Private/ActiveSound.cpp |
FActiveSound 求值核心:UpdateWaveInstances :1081、UpdateAttenuation :2053、CheckOcclusion :1599、HandleInteriorVolumes :1708、OcclusionCheckInterval :197、au.DisableOcclusion :20、au.UseAsyncOcclusionTraces :27 |
Private/AudioDevice.cpp |
播放链路:PlaySoundAtLocation :6705、Update :4725、GetSortedActiveWaveInstances :4159、SoundIsAudible :6318、LocationIsAudible :6380、StartSources :4541、UpdateAudioVolumeEffects :4923、au.* CVar 区 :44-205 |
Public/AudioDevice.h |
FAudioDevice 声明、AddNewActiveSound :763、Precache :625/:639 |
Public/ActiveSound.h |
FActiveSound 类(音频线程声音实例) |
Public/Audio.h |
FWaveInstance :315(bIsAmbisonics)、GetVolumeWeightedPriority :482、STAT_* 计数器 :32-39 |
Public/AudioThread.h |
FAudioThread :21、RunCommandOnAudioThread :58、STATGROUP_AudioThreadCommands :19 |
Public/AudioDeviceManager.h |
FAudioDeviceManager(多设备管理)、au.3dVisualize.Enabled :49 |
Private/Audio.cpp |
FSoundSource:GetSpatializationParams :724、UpdateStereoEmitterPositions :625、NonSpatializedAmount :739-779 |
Private/AudioVirtualLoop.cpp |
FAudioVirtualLoop:Virtualize :60、CalculateUpdateInterval :96、UpdateRate CVar :30-40 |
Private/AudioStreamingCache.cpp |
FAudioChunkCache、au.streamcaching.* :32-91(StreamCacheSizeOverrideMB :139) |
Private/AudioDebug.cpp |
au.3dVisualize.ActiveSounds :55 / SpatialSources :79 / Attenuation :744、au.DumpActiveSounds :676 |
A.2 组件与游戏性(Runtime/Engine)
| 文件 | 关键内容 |
|---|---|
Classes/Components/AudioComponent.h |
UAudioComponent :167:Play :517、Stop :583、FadeIn :500、FadeOut :511、SetVolumeMultiplier :626、SetPitchMultiplier :630、SetFloatParameter :548、SetIntParameter :541、SetTriggerParameter :534、UInitialActiveSoundParams :146 |
Private/Components/AudioComponent.cpp |
PlayInternal :673、GetAttenuationSettingsToApply :1342(三级覆盖)、SetAttenuationOverrides 热更新 :1367-1388、bOverridePriority :776-783 |
Private/GameplayStatics.cpp |
PlaySoundAtLocation :1526、LocationIsAudible :1316、SetListenerFocusParameters :1390、SetBaseSoundMix :1752、SetSoundMixClassOverride :1877、PushSoundMixModifier :1771、PopSoundMixModifier :1915 |
Classes/Kismet/GameplayStatics.h |
音频静态入口:PlaySound2D :629、PlaySoundAtLocation :681、PlayDialogue2D :746、SetBaseSoundMix :881、SetSoundMixClassOverride :917、Push/PopSoundMixModifier :931/:937 |
Classes/Sound/SoundBase.h |
USoundBase :108:SoundClassObject :117、Priority :218、AttenuationSettings :222、ConcurrencySet :188、VirtualizationMode :170、bDebug :127 |
Classes/Sound/SoundWave.h |
USoundWave、bStreaming :455、au.streamcache.SoundWaveDefaultLoadingBehavior :65 |
Classes/Sound/SoundCue.h |
USoundCue :90、FirstNode :95 |
Classes/Sound/SoundNode.h |
USoundNode :57(ChildNodes :64、DECLARE_SOUNDNODE_ELEMENT :26-54);子类:WavePlayer/Attenuation/Random/Looping/Modulator/Oscillator/Concatenator/DistanceCrossFade :82/ParamCrossFade/Switch/Branch/Enveloper/Doppler/QualityLevel |
Classes/Sound/SoundAttenuation.h |
FSoundAttenuationSettings :148(bAttenuate :154、bSpatialize :158、bAttenuateWithLPF :162、bEnableOcclusion :174、bEnableReverbSend :182、SpatializationAlgorithm :210、BinauralRadius :218、OcclusionTraceChannel :234、NonSpatializedRadiusStart/End :254/:258、StereoSpread :266、LPF :281-293、Occlusion :345-353、ReverbSend :365-377、PriorityAttenuation :383-401)、USoundAttenuation :452 |
Classes/Engine/Attenuation.h |
FBaseAttenuationSettings :56(DistanceAlgorithm :63、AttenuationShape :68、FalloffDistance :95) |
Classes/Sound/SoundConcurrency.h |
FSoundConcurrencySettings :73(MaxCount :89、bLimitToOwner :93、ResolutionRule :109、RetriggerTime :113、VolumeScale :139、VoiceStealReleaseTime :170)、EMaxConcurrentResolutionRule :30-58、EConcurrencyMode :233 |
Private/SoundConcurrency.cpp |
FSoundConcurrencyManager:EvaluateConcurrency :818、CanPlaySound :654、CreateAndEvictActiveSounds :901 |
Classes/Sound/SoundClass.h |
USoundClass :208、FSoundClassProperties :54(Volume :60、bIsUISound :103、bIsMusic :107、bReverb :119) |
Classes/Sound/SoundMix.h |
USoundMix :170、FSoundClassAdjuster :129(bApplyToChildren :151) |
Classes/Sound/AudioVolume.h |
AAudioVolume :152(Priority :142、FReverbSettings :170、FInteriorSettings :174、SubmixSendSettings :178、SubmixOverrideSettings :182) |
Classes/Sound/ReverbSettings.h |
FReverbSettings :48 |
Classes/Sound/AudioBus.h |
UAudioBus :87 |
Classes/Sound/SoundSubmix.h |
USoundSubmixBase :173、USoundSubmix :339 |
Classes/Sound/AudioSettings.h |
MaximumConcurrentStreams :199、DefaultSoundClassName :140、VoiPSoundClass :156、MasterSubmix :160、ReverbSubmix :168 |
Classes/Sound/AmbientSound.h |
AAmbientSound :12 |
Classes/Engine/VoiceChannel.h |
UVoiceChannel :19(网络语音通道) |
Public/Audio/SoundParameterControllerInterface.h |
ISoundParameterControllerInterface :24(SetTriggerParameter :32、SetFloatParameter :42 等) |
A.3 音频混合器(Runtime/AudioMixer)
| 文件 | 关键内容 |
|---|---|
Private/AudioMixerDevice.cpp |
FMixerDevice:OnProcessAudioStream :1599、new FMixerSource :1546、FMixerSourceVoice :2888、Get3DChannelMap :2915(EqualPower :2997/Linear :3007、NonSpatialized Lerp :3051-3055)、PanningMethod :1173、MasterReverbSubmix :2082(默认资产 “MasterReverbSubmixDefault” :1891) |
Private/AudioMixerChannelMaps.cpp |
InitializeChannelAzimuthMap :147(扬声器方位角表) |
Private/AudioMixerSource.cpp |
FMixerSource:UseObjectBasedSpatialization :2337、UpdateEffects :1636(SetSubmixSendInfo)、UpdateVolume :1509 |
Private/AudioMixerSourceManager.cpp |
FMixerSourceManager:ComputeNextBlockOfSamples :3919、SetLPFFrequency :2053、au.* :42-106 |
Private/AudioMixerSourceOutputBuffer.cpp |
EncodeToSoundfieldFormats :385(Ambisonics 编码) |
Classes/SubmixEffects/AudioMixerSubmixEffectReverb.h |
FSubmixEffectReverbSettings :23(DecayTime=1.49s 等) |
Private/AudioMixerSubmix.cpp |
FMixerSubmix、au.BypassAllSubmixEffects :37 |
A.4 扩展与插件
| 文件 | 关键内容 |
|---|---|
Runtime/AudioExtensions/Public/IAudioExtensionPlugin.h |
IAudioSpatialization :245-388(ProcessAudio :357)、FSpatializationParams :68-124 |
Runtime/AudioExtensions/Public/AudioParameter.h |
FAudioParameter :92(5.9 迁至此模块) |
Runtime/Online/Voice/Public/VoiceModule.h |
FVoiceModule:CreateVoiceCapture :70、CreateVoiceEncoder :81、CreateVoiceDecoder :91 |
Plugins/Runtime/Metasound/Source/MetasoundEngine/Public/MetasoundSource.h |
UMetaSoundSource :89 |
Plugins/Runtime/Metasound/Source/MetasoundEngine/Public/MetasoundBuilderBase.h |
UMetaSoundBuilderBase:AddNode :174、ConnectNodes :197 |
Samples/Games/Lyra/Source/LyraGame/Audio/ |
ULyraAudioSettings :31、ULyraAudioMixEffectsSubsystem :41(封装范例) |
附录 B 术语表(中英对照)
| 英文 | 中文(本文用词) | 一句话解释 | 首次出现 |
|---|---|---|---|
| Attenuation | 衰减 | 距离 → 音量映射 | Ch2 |
| Spatialization | 空间化 | 方位角 → 声道增益 | Ch2 |
| Panning | 声像 | 扬声器之间的增益分配 | Ch2 |
| Occlusion | 遮挡 | 隔墙:低通 + 音量 | Ch2 |
| Reverb | 混响 | 房间回声(早反射+晚反射) | Ch2 |
| Decay | 衰变 | 混响衰减时间(勿与 Attenuation 混) | Ch7 |
| Concurrency | 并发 | 同时发声裁决 | Ch2 |
| ResolutionRule | 裁决规则 | 超限时挂谁(七种) | Ch8 |
| Ducking | 避让 | 不挂断只调小 | Ch8 |
| Listener | 听者 | 耳朵的位置与朝向 | Ch2 |
| SoundClass | (不译) | 音量分层推子树 | Ch2 |
| SoundMix | (不译) | 推子预设快照 | Ch2 |
| Submix | 子混音 | 音频处理链节点 | Ch2 |
| Bus | 总线 | 音频接线板 | Ch2 |
| HRTF | (不译) | 头部相关传输函数(双耳滤波) | Ch1 |
| ITD / ILD | (不译) | 双耳时间差 / 响度差 | Ch1 |
| Ambisonics | (不译) | 球谐声场(环境底床) | Ch5 |
| FOA | (不译) | 一阶 Ambisonics(4 声道) | Ch5 |
| Focus | (不译) | 听者聚焦(音频变焦) | Ch4 |
| Virtualization | 虚拟化 | 远处循环声降频(放假) | Ch2 |
| Streaming | 流送 | 音频按需加载 | Ch9 |
| Precache | 预载 | 启动/加载时解压进内存 | Ch11 |
| FActiveSound | (不译) | 音频线程上的声音实例 | Ch3 |
| FWaveInstance | (不译) | 展开出的可播放流 | Ch3 |
| Voice | 语音 | 语音聊天(VoiceChannel + Opus) | Ch10 |
| MetaSound | (不译) | 程序化音频 DSP 图 | Ch2 |
| AudioManager | 音频管理器 | 游戏侧封装(引擎无内置) | Ch2 |
| bIsUISound | (不译) | UI 音(不衰减不空间化) | Ch9 |
| bIsMusic | (不译) | 音乐(独立分层) | Ch9 |
附录 C 参考资料
Epic 官方(dev.epicgames.com):
- Sound Concurrency Reference Guide(5.6)—— 并发资产权威说明;
- Audio in Unreal Engine 5(5.8)—— 音频总览(Mixer 管线/SoundCue/Class/Mix/MetaSound);
- MetaSounds: The Next Generation Sound Sources(5.8)—— DSP 渲染图而非播放器;
- Audio Gameplay Volumes Overview(5.4)—— 新一代区域音频(组件化行为/优先级仲裁);
- Sound Attenuation in Unreal Engine(5.0)—— 衰减/非空间化半径/3D 立体声扩展/空气吸收;
- 音频流缓存概述(5.5 中文)—— 流缓存架构与 PrimeSoundForPlayback;
- *Console Variables Reference — au.*(5.8)—— 全部 au. CVar 索引;
- Audio Mixer Overview(4.27 中文)—— 统一跨平台渲染器背景。
中文社区(标注:以下数据非本仓库实测,写作时未能逐一访问核实):
- ixueyouxi《UE5.8 音频性能与内存优化:从一次池溢出说起》—— SoundCue 实例池溢出案例、内存估算公式、ADPCM/Vorbis/Opus 选型、长环境音流送;
- CSDN《UE5 Lyra 源码分析——Audio_Analysis 音频模块》(chouchouxia)—— 配置层+调度层分离的音频管理器范本(本文第 12 章参考);
- 语雀《游戏项目音频管理系统代码框架的认知与思考》—— 预加载/延迟释放/音量分级/SoundBank;
- k7dj《空间音频技术在游戏音效设计中的应用》—— HRTF 方向精度、移动端 3-5ms 预算、Ambisonics 环境+HRTF 对象混合方案;
- 知乎《Object-Based Audio 音频设计与混音案例》—— OBA 管线(资源制作与实时定位分离)。
附录 D 自测练习
入门级:
- 用三句话 + 一个类比向同事解释空间音频四件套(衰减/空间化/遮挡/混响各管什么)。
- 立体声素材和单声道素材做 3D 声源的区别是什么?为什么推荐单声道?
MaxDistance在音频里有两个作用——分别是什么?
进阶级:
- 画出从
PlaySoundAtLocation到扬声器的完整链路,标注三个线程各干了什么。 - 解释”衰减范围 = 性能门控”:为什么 MaxDistance 拉大性能会变差?
- 多人游戏里枪声怎么同步?为什么用 Unreliable 而不是 Reliable 的 RPC?
- 远处 300 个循环声怎么优化?虚拟循环的更新间隔怎么算?
- 为什么游戏需要自建 AudioManager?三层封装各解决什么问题?
源码级:
- 在
ActiveSound.cpp:1599(CheckOcclusion)找到射线检测的三个参数,解释AddIgnoredActor(OwnerID)的作用。 - 在
AudioMixerDevice.cpp:2915(Get3DChannelMap)推导:立体声(FL=270°/FR=90°)下方位角 0°(正前方)的增益对。 - 在
SoundConcurrency.cpp:818(EvaluateConcurrency)找到七种裁决规则的实现分支,解释StopFarthestThenOldest的”最远”怎么判定。 - 实验:配一个 3D 声源,用
au.3dVisualize.Attenuation=1看衰减形状;把它放进 AAudioVolume 后听混响变化;开 50 个并发声音用au.DumpActiveSounds观察被吞的规则。