游戏音频技术备忘 (五)Wwise Unreal Engine 集成代码浅析 二

时间:2021-07-17 15:14:54
AkAmbientSound类的实现 Unreal Engine提供了一个基本对象的构造器ObjectInitializer,一般来说用户创建的类总是拥有很多变量,因此 AkAmbientSound 首先覆写了 ObjectInitializer ,为该类的若干变量赋初始值,通过ObjectInitializer的子对象创建模板创建了一个AkComponent并作为根组件保存下来,同时初始化3D衰减倍率因子为1,表示不进行衰减范围缩放。
 
函数PostLoad()名称上带有Post,实际上和Wwise的Post概念无关,是一个父类AActor的成员函数。
 
同样的PostInitializeComponents() 是一个 父类AActor 进行组件初始化函数,在这里调用 AkComponent的一个 UpdateAkReverbVolumeList()函数实现检查当前 AkAmbientSound是否处在一个混响体积之中。
 
StartAmbientSound()函数调用了StartPlaying()函数,通过这个函数的实现可以一窥基本的Wwise播放机制,即通过调用AkAudioDevice对象的一系列函数实现播放, AkAudioDevice是Wwise与Unreal Engine整合的一个单例类,作为Wwise引擎层之上的抽象层负责把大部分Unreal Engine规范的功能通过Wwise API实现,并依照 Wwise API规范对整个音频模块进行管控。
 
AkAudioBank类的实现 可以看到该类分别提供了同步与异步加载/卸载Bank的四个函数,由于Wwise运行在另一线程,因此能实现一部分异步处理需求,由于.bnk数据的加载完全由Wwise自管理,因此在磁盘IO与内存优化方面只能主要依靠对.bnk数据的合理划分,当然Wwise也提供了若干全局变量用以控制读写磁盘与内存的频度。
 
AkAudioDevice类 的实现 作为最重要的整合代码部分,完成了连接两套软件系统的工作。在头文件包含部分可以看到Wwise自身还提供进一步的插件方案,由于传统数字音频技术建构的DAW+Plugins解决方案模式非常成熟,Wwise也借鉴这种设计思路,把许多信号处理功能封装为单一库,这样用户可以灵活依据自身需求搭建整个音频信号处理方案,并且依据Wwise插件规范大量已有音频插件可以通过重编部分代码实现接入Wwise。
 
随后进行引擎初始化,创建底层IO系统,分配内存(可以看到在PC/XB360/XBONE平台上分别根据各自特性实现虚拟内存分配和APU内存分配);AkVectorToFVector()函数实现把Wwise所用的坐标系转换为Unreal的翻转左手坐标系;RegisterGameObj_WithName()函数实现将Unreal对象在Wwise内注册以便Wwise进行实例管理;Init()、Update()、TearDown()函数分别实现在该层范畴内的音频硬件的启动更新与关闭,默认不在专用服务器上创建音频硬件,SOUNDFRAME为Wwise和Unreal Engine提供基于服务器客户端模型的实时模拟/监测系统,实际上打开Wwise编辑器与Unreal Editor 实时连接 功能之后,相当于又创建了一套独立于当前 Wwise和Unreal Engine  的声音系统,这时就能实现在Wwise编辑器内调节各类参数与配置,并实时反馈到 Unreal Engine内,具体原理可参考https://www.audiokinetic.com/library/edge/?source=Help&id=using_soundframe;更新函数的实现通过调用Wwise API的RenderAudio()与UpdateListeners()函数进行音频渲染与听者数据更新;关闭函数首先卸载所有Bank数据,释放硬件占用,随后的几个函数分别实现结束通信服务,注销游戏对象,结束底层引擎和流式管理器等功能。
 
UpdateListeners()和SetListener()函数分别实现听者对象的更新与设定,用于3D声音功能;AkAudioBank类的Bank加载卸载相关函数最终调用Wwise API里Bank相关函数,如果需要得到更多Bank加载卸载的信息可以查找AkBankCallbackInfo相关内容,Wwise回调相关内容都在 Engine\Plugins\Wwise\ThirdPart\include\AK\AkCallback.h/cpp里(例如可以利用背景音乐段落标记回调消息实现控制游戏内相关玩法逻辑)
 
PostEvent()函数实现最基本的“播放”行为(实际上应该称之为Event触发行为或传递Event消息请求比较合理,因为很多时候某些Event也包含停止播放声音这一请求,当然用Event封装的行为也非常多,例如一个游戏暂停时各类声音的播放状态改变、全局声音效果的变化都可以封装在一个Event里);AkReverbVolume相关函数通过一些简单的逻辑实现混响体积区域计数、当前所在 混响体积区域 判别、优先级排布等功能;后续若干函数为AkGameplayStatics类和AkComponent类的大部分成员函数的实际实现,基本上都通过参数传递的方式实现最终调用Wwise API以达成目标,可以看到许多代码里为了兼容各种非拉丁语系语言参数写了很多WCHAR相关逻辑;SetAuxSends()和SetGameObjectOutputBusVolume()函数用以在Unreal Engine内对音频信号的发送轨与音量进行调节,一般来说这种调节最好就完成在Wwise编辑器内,当然如同 AttenuationScalingFactor变量的存在,多一种能调节的方案总是好的(大概?)。
 
GetAkComponent()函数也能创建一个 AkComponent,类同于 SpawnAkComponentAtLocation()函数,二者主要的差异在于创建的Ak组件从属对象不同, GetAkComponent()更常用于某一行为产生了发出声音需求,并需要后续继续对 Ak组件 进行控制的情况,而 SpawnAkComponentAtLocation()则如其名,当然也可以 先SpawnAkComponentAtLocation()再AttachTo()来完成同一效果(深究效能二者会有差异,如果出现高频次的创建销毁需求就需要稍微考虑下用哪个函数了)。
 
EnsureInitialized()函数也是初始化用途,在这个函数的实现里对内存池、消息队列、栈大小进行了初始化赋值,在默认值满足不了实际情况时可以修改,当然随之而来的性能变化也是需要考虑的,可以看到针对不同平台有不同的初始化逻辑,一般部署测试时产生的某些问题可以考虑在这个函数上找找答案。
 
ProfilerCapture()相关函数实现监测功能的创建数据存储与停止(两千行代码一瞬而过……)。
 
AkAudioModule类的实现 创建了AkAudioDevice单例对象。
 
AkComponent类的实现 大部分函数没有什么特别之处,都是各个功能的具体实现,SetOcclusion()函数的实现比较有意思,实际进行声障声笼计算的时候,会在听者与 AkComponent之间进行一个LineTrace,当产生HitResult时即时在遮挡物上创建若干点,这些点的坐标依据为遮挡物的BoundingBox,实际上大部分情况下由于遮挡物为不规则多面体, BoundingBox的体积一般会大于遮挡物体积,随后会再进行以点的数量作为计数的LineTrace来计算是否还有无遮挡的路径,结合该数值就可以计算出一种比较粗略的遮挡率(大部分情况下结合Wwise编辑器内 声障声笼 的全局属性配置可以创造出大致的遮挡效果,如追求更精细的遮挡效果可以改良这种设计)。由于这种空间化处理是一个Tick型的实现, AkComponent 默认有一个变量 OcclusionRefreshInterval = 0.2f,即设定了两次遮挡效果检测计算的间隔,出于优化CPU占用表现的目的,可以把 OcclusionRefreshInterval值的设置扩展为一种相对更灵活的方案暴露给编辑器层面。OCCLUSION_FADE_RATE这一值用于设定遮挡效果的淡入淡出时间,默认为2即1/2秒内淡入淡出遮挡效果,在现有集成代码实现里这是一个全局定值,可以从SetOcclusion()函数对当前处于工作状态的遮挡效果的处理实现部分看到对其的使用。
 
 
现实世界声音的扩散反射吸收相对来说可以划归为几个略复杂的数学模型,空间化效果的实现与发声体的实现紧密相关,理论上对于声波的 扩散反射吸收可以借鉴渲染领域近年热度比较高的Real-time Ray Tracing相关技术来实现,但由于该种技术对硬件高度的依赖性,暂时还看不到相关的落地技术(可以了解一下Nvidia的OptiX与AMD的TrueAudio相关内容)。Wwise提供的 空间化 方案比较传统,仅通过在信号处理链上加入混响处理器来模拟空间效果,对于空间变换带来的效果变化则通过人工配置来实现(参考AkReverbVolume相关),不过更精细更自动化的空间效果也可以通过Wwise支持的若干第三方插件来实现。
 
Wwise的3D声音实现基于VBAP算法(随附3D声音相关paper一篇:http://lib.tkk.fi/Diss/2001/isbn9512255324/article1.pdf),这种算法实际上是利用声音信号在不同的扬声器上的音量比例(或者称为声音的分布比较准确)来模拟出声音的位置信息(产生了一个叫做vitrual sound source的概念),这就完全受制于扬声器的实际位置(虽然理论上是可以把扬声器任意地布置,实际上由于一般的二分频扬声器的设计特点,很难真正重现各类发声源在声音产生与运动时的声学效果,因此也出现了很多更精确的方案); Wwise不包含对 IID与ITD现象的实现(人类的听觉系统对声音的定位一部分是基于双耳接收到的声音信号的强度与时间差异的,对于Wwise可以通过第三方Binaural相关插件来解决);Wwise也未实现人体本身作为一个吸收体对声音的影响,有一种基于HRTF(头部相关变换函数)的实现可以解决一部分由此引起的问题,但是受制于其原理带来的性能瓶颈也并不能作为一种适合各类游戏声音的解决方案(现在最常见的HRTF 实现都是基于合成思路,把预先采集的IR阵列数据依照听者与声源的相对角度来合成最终声音信号,对于Wwise可以通过各第三方HRTF插件来解决这一问题,新版本Wwise已包含了一个来自微软的HRTF插件);Wwise 未包含多普勒效应的实现,对于运动声源将会难以模拟由位置变化带来的频率变化(不过由于多普勒效应 原理相对简单,可以通过在游戏引擎层面实现 多普勒效应因子的计算,并通过Wwise的RTPC功能来进行音调控制,随附OpenAL的实现用以参考(Doppler Shift部分):https://www.openal.org/documentation/OpenAL_Programmers_Guide.pdf)。对于VBAP的不足之处,Wwise在2016版本开始支持Ambisonics技术用以补充,Ambisonics本身是一种基于球谐函数的声音空间化方案(可参考略显抽象的Wiki:https://en.wikipedia.org/wiki/Ambisonics),其收录的是一个具体空间点所接受到的声音,因此Ambisonics技术适合于表现听者位置的声音图景,而正如同于全景照片,Ambisonics技术无法重现听者运动时声源位置的相对变化,因此对于游戏,大部分情况下Ambisonics仅适合于环境整体的氛围声塑造(不过基于实践经验来看,用Wwise基于VBAP算法的3D声音功能播放Ambisonics这样看似矛盾的处理方案,反倒能产生一些在声音位置层面更平滑的声音效果,一个小trick)。
 
 
AkGameplayStatics类的实现  各个 Gameplay 相关函数的具体实现,可以作为一个了解Unreal函数库写法的示例来学习,代码里出现了UE_LOG这样的断言宏、FActorIterator这样Unreal自身的类STL迭代器对象、TSet类型的散列容器等,绝大多数代码都比较直观简单。
 
AkReverbVolume类的实现 有一个GetLifetimeReplicatedProps()函数的覆写,可以在 Unreal Engine 文档里查阅对象生命周期机制及Tick相关了解一些概念,由于混响体积对象需要始终存在,因此这里调用DOREPLIFETIME宏来实现 混响体积对象的存续。
 
AkSettings类的实现 Wwise工程、Bank数据生成路径、Wwise CLI位置等都在这里实现字符串拼接、校验。
 
AkUnrealIOHookDeferred类的实现 Windows平台默认延迟底层钩子在这里实现,接口AK::StreamMgr::IAkFileLocationResolver通过一个简单的路径关联逻辑实现文件位置解析,AK::StreamMgr::IAkIOHookDeferred使用平台API实现IO,Init()实现流式设备的创建。
 
Interp四类的实现 实现Unreal Engine Matinee动画模块中的Ak Event和Ak RTPC Track,各函数用途可参照 Matinee其他插值轨道类型,基本为增删改关键帧与播放器逻辑相关功能。