一、前言:
希望本篇分享能抛砖引玉,激发大家更多的开发灵感。
在游戏的实际开发过程中,你是否经历过以下两个场景:
-
场景一:策划小姐姐跑过来说,我们需要美术特效跟随音频的变化去变化…
-
场景二:策划小姐姐又跑过来说,我们想做个游戏内录屏的功能,玩家点击录屏按钮会录一个带声音的视频…
仔细分析,我们不难发现,场景一中策划小姐姐的需求本质,其实就是我们经常讨论的音频的可视化实现。那么,场景一中如何实现Wwise音频可视化和场景二中的如何实现录制Wwise里输出的音频,这两个问题的本质又是什么?
没错,是如何获取Wwise底层的音频采样数据?
那么我们今天讨论的主题就变成了:如何获取Wwise底层的音频采样数据并发送到Unity中进行接收?
二、如何获取Wwise底层的音频采样数据并发送到Unity中接收?
我们可以把这个问题拆分为2个问题:
-
如何获取Wwise的底层音频采样数据?
-
获取到数据之后又如何发送到Unity中?
在本文探索的方案思路中,上述拆分之后的2个问题的回答如下:
-
如何获取Wwise的底层音频采样数据?
答:编写并构建你的Wwise Effect Plugins
-
获取到数据之后又如何发送到Unity中?
答:编写并构建数据中转插件,且称他为中转器。Unity中通过DllImport的形式,将中转器动态库库导入……
那么,根据上述2个问题的回答,下文将从Wwise Effect Plugins,数据中转器,Wwise、中转器和Unity之间的互联三个方面逐步探索。
1、Wwise Effect Plugins
我们可以把Wwise理解为是一个密封的黑盒子,我们所需要的采样数据就存在这个黑盒子里的某个地方。Wwise官方并没有开放相关的Api,因此我们从外面是无法把“手”伸进黑盒子里直接拿到数据的。所以,我们只能另辟蹊径,而编写插件就是那条蹊径。
由于我们编写的插件仅仅用来捕获Wwise底层数据用,因此插件的GUI层面不需要加入什么东西,所以当我们构建好开发Wwise效果器插件的vs工程之后,直接进入声音引擎逻辑编写部分。当然,如果你觉得必须要放点什么在插件面板上的话,那么,你可以……
比如:
扯远了,回到正题。
我们所编写的Effect Plugins就像一个“间谍”潜伏在Wwise这个黑盒生态系统中,时刻为我们拿到需要的情报(数据)。拿到之后势必要有一辆“运输车”去装载数据,那么,我们先构建这辆“运输车”。
直接进入声音引擎编写部分,在头文件声明:
在插件的构造函数中将其初始化为nullptr
进入插件的Init()函数中,为其申请一个大小为2048的浮点型数组的内存
为此wwise提供了AK_PLUGIN_ALLOC宏:
为什么大小是2048呢?
这个取值不是死的,取决于你的设置,因为我的音频输出设置为Stereo,采样点数为1024,所以我这里固定了大小为2048.
在插件的Term()函数中释放这部分内存:
Wwise提供了AK_PLUGIN_FREE宏:
这样便完成了“运输车”的构建。
那么,我们现在开始往车上装填数据。
进入插件的Execute(AkAudioBuffer* io_pBuffer)函数中:
在上述代码中,audioDataForSend数组中存入的是来自wwise所有通道的音频数据,并且整个存储的过程是不同通道交错访问存储的。也就是说,其中相邻的数据分别来自于不同的通道,举个例子,比如第10个数据来自于左通道,那么第9个和第11个数据来自于右通道。这样在两个for循环的不断刷新执行下,当outPosition的值等于2047的时候,便完成了当前音频帧的所有音频数据的“装车“。
这里,我以我拙劣的画技画了一幅数据“装车”的示意图:
数据“装车”之后,面临的问题是如何将数据运输到Unity中?实际上,Unity与Wwise这两个密闭的系统之间是没有“路”可以供“运输车”行驶的。那么,我们势必要动手开辟一条连接Unity与Wwise的通路,就像连接候机大厅与飞机之间的登机通道,或者连接两座悬崖绝壁的索道一样。
数据中转器便是连接Unity与Wwise的通路上的一个重要枢纽。
2、数据中转器
中转器是连接Unity与Wwise的一个重要枢纽,它以动态库的形式存在。关于动态库构建方面的知识本篇分享就不延展来说了,因为这里又涉及很多方面的内容。
中转器的职能简单来说就是:接收Wwise传来的数据,并扔到Unity过来接收的车上。
实际上,在本文提供的思路中,构建中转器动态库的C++代码中只需要两个相同参数类型的导出函数。
比如:
上述代码中,我们可以看到有2个相同参数类型的函数。虽然参数相同,但是他们的实际职责却是各不相同的。一个用于Wwise与中转器的连接,另一个则用于Unity与中转器的连接。详情看下文
3、Wwise、中转器、Unity三者之间的互联
Wwise&中转器
Wwise与中转器的连接过程涉及动态库的读取加载、函数指针的获取等等。
在Wwise插件编写声音引擎部分,声明一个函数类型指针,其类型与中转器中的函数一样:
在Execute(AkAudioBuffer* io_pBuffer) 函数中,在数据装车完成之后,加入以下代码:
上述代码中, 我们通过LoadLibrary() Windows 系统API(Linux系统下是dlopen())获取动态库句柄,通过GetProcAddress() 获取函数指针。这里我们获取中转器中GetSamplesFromWwise函数指针。通过函数指针完成函数的执行。
至此完成Wwise与中转器的连接,实现数据由Wwise到中转器的转移。
Unity&中转器
在完成Wwise与中转器的连接之后,我们考虑连接中转器与Unity。
Unity与中转器的连接涉及Unity动态库导入、外部库函数的定义以及非托管层与托管层之间数据的转换传输等等。
在C#中导入库并声明外部库函数:
非托管层与托管层之间的数据转换运算,C#提供了Marshal类 供我们使用。我们通过Marshal类 中的接口实现内存的申请,数据的copy等操作。调用Dllimport导入的外部库函数,实现c#层面对于音频数据的获取。
比如:
分配一个2048大小的float类型数组内存:
将一个数组拷贝到另一个数组:
释放:
实际上,我们为了方便在Unity项目中C#层面更快捷地接收Wwise的数据,可以将我们导入的库函数和Marshal类 中的接口一起封装成一个函数,方便我们使用,比如:
至此、就完成了Unity与中转器的连接。与此同时,一条Wwise通往Unity的数据“运输之路”修建而成,Wwise的音频数据便可传入Unity中接收、使用。
拿到了Wwise的音频数据之后,至于波谱、频谱、录屏等等功能需求的满足便易如反掌了。
举个例子,我们可以修改项目使用的录屏插件里的音频模块的逻辑,达到将来自Wwise的音频数据写入录制的视频中的目的。
你要捕获什么音频数据就把效果器插件挂到对应的总线上。比如,想获取仅仅来自音乐的音频数据,那就把插件挂到音乐系统输出的音乐总线上。那么,你录到的视频里面就只有音乐没有音效了。
三、总结:
以上所说的整套Unity中获取Wwise音频数据的流程,这里我又展示了我拙劣的画技,如下:
效果视频
以下视频,我将拿到的Wwise音频数据,在Unity中绘制了一下:
(国内观赏视频通道)
最后
考虑到性能、兼容性,跨平台开发等因素的影响,实际的开发过程还是比较复杂,比较麻烦的。
如本篇开头所说,希望本篇分享能起到抛砖引玉的作用,让大家在实际的游戏开发过程中能够产生更多好玩的想法或灵感。
如果你有更好的方案或想法,欢迎交流。
评论