大家好,我叫埃德·卡辛斯基,是一名来自俄罗斯圣彼得堡的声音设计师和音乐家,目前在做一个非常有趣且重视声音表现的项目:这是一个名为《牧师与鬼驱人(Priest vs. Poltergeist)》的线下多人VR游戏。通常重视声音意味着少不了声音方面的挑战,但这也让我学到了很多。借写博客的机会来跟各位分享一下。
简单介绍一下,《牧师与鬼驱人》是一款以决斗为主题的VR 游戏。其中,VR 玩家扮演 鬼驱人。PC 玩家则扮演 牧师,并通过鼠标和键盘来控制角色。这里的难点在于它是在同一台 PC 上运行的。不仅两名玩家要能听到来自不同输出设备(VR 耳机和 PC 耳机/扬声器)的声音,而且各种声音还需要有自己独立的定位、衰减、声笼、时机等设置。
试想一下在关卡中某个地方有把椅子掉到了地上。如果一名玩家离它只有几米,声音会听得很清楚。倘若另一玩家距离同样远,但跟椅子之间隔了一堵墙,那么落地声听起来就会比较干,音量也会低一些。因此,不但两名玩家要能从不同的输出设备听到落地声,而且还得根据其各自的当前位置和设置相应地加以调节。
当时我们试了好几种解决方案(包括 Unreal 的原生音频库),最终决定利用 Wwise 来加以实现。借助这款独立的游戏音频引擎,我们可以在设计工具中随心所欲地处理声音。甚至将声音发送到不同的 Audio Device,而这正是我们想要的。
为了将音频引擎的功能集成到 Unreal 中,Wwise 提供了相应的插件。理论上来说,它应当将 Wwise 引擎的所有功能传给游戏引擎以便在后者当中使用。然而,我们很快便碰了壁:Wwise的 Unreal 插件并没有涵盖我们所需的那种“指定在不同设备上播放声音”的功能。
最后我们别无选择,只好亲自动手在 C++ 中添加这项功能。
目的
插件的主组件 AkComponent 被添加在关卡中,既可用作 Listener,也可用作 Emitter。Emitter 可在关卡中的任何位置播放声音。Listener 则像双耳一样监听所有声音,并根据角色所在位置将声音输出到 Audio Device(Listener 通常与 Camera 组件绑定)。
这里有两个角色。每个角色都有一个 AkComponent 作为 Listener,来将声音同时输出到两个 Audio Device。总的来说,就像这样:
- Emitter 触发 Blueprint 函数 Post Ak Event,继而在 Wwise 中触发 Event。
- 该 Event 播放输出到两条 Audio Bus 的声音。
- Audio Bus 将声音发送到与之对应的 Wwise Audio Device(本例中为 System 或 System_VR)。
- 各个 Listener 关联与之对应的输出(Output Device 1 或 Output Device 2),并监听来自 Wwise 的声音。在声音抵达 Audio Bus 时,便在对应输出上进行播放。
我们要做的便是在 AkComponent 设置中添加两个文本字段,以便可根据需要填入声音输出的目标音频设备名称。
第 1 步:设置 Wwise
首先,我们要在 Wwise 创作工具中创建音频设备和音频总线。详细信息,请参阅原始文档(“总线通路”部分)。
- 在 Wwise 中为 VR 玩家添加 Audio Device。将类型设为 System,并命名为 System_VR。这个便是最终要在 AkComponent 设置下 Wwise Device Name 字段中键入的 Wwise Audio Device 的名称。
- 为 VR 玩家创建 Audio Bus(Master_VR_Aux_Bus),并将 System_VR 设为 Audio Device。
- 在 Wwise 中,声音只能发送到一条 Output Bus。但是,我们要针对两个角色播放声音。为此,需要使用 Auxilary Bus。
为了在两个 Audio Device 上播放同一声音,我们需要为 Master VR Audio Bus 创建一条嵌套的 Auxiliary Bus。结果应如下图所示:
这样我们便可将声音单独或一并发送给两个角色了。
***
创作工具部分准备就绪,接下来只需按照下列规则为各种声音设置总线即可:
- 牧师的声音 (PC):将 Master Audio Bus设为Output Bus,并将 Auxiliary Bus 保留为空。
- 鬼驱人的声音 (VR):将Master VR Audio Bus设为Output Bus,并将 Auxiliary Bus 保留为空。
- 通用声音:将Master Audio Bus设为Output Bus,并在 Auxiliary Bus 列表中添加 Master_VR_Aux_Bus。
如此, Wwise创作工具部分就准备就绪了。
第 2 步:设置 AkComponent
接下来我们要对付编译器,以便我们对插件所作的修改能够生效。如果不清楚如何将 Blueprint 工程转换为 C++ 工程并对其进行编译,请参阅以下文档:
https://docs.unrealengine.com/en-US/Programming/Development/CompilingProjects/index.html
https://docs.unrealengine.com/en-US/Programming/Introduction/index.html
在以下位置您可以找到我们将要处理的文件:
{Project Folder}/Plugins/Wwise/Source/AkAudio
AkAudioDevice.h
打开 /Public/AkAudioDevice.h,并在 public: 之后添加以下代码:
// connecting audio device with device from wwise
AkOutputDeviceID AddCustomOutput(FString AudioDevice, FString WwiseDevice, UAkComponent* in_pComponent);
// removing connection
AKRESULT RemoveCustomOutput(AkOutputDeviceID deviceId);
// searching audio device by substring
TTuple <AkUInt32, FString> SearchAudioDeviceIdByName(FString deviceName);
AKComponent.h
接着转到 /Classes/AkComponent.h,并在 public: 之后添加以下代码:
TTuple <AkUInt32, FString> FAkAudioDevice::SearchAudioDeviceIdByName(FString deviceName)
{
TTuple <AkUInt32, FString> result;
AkUInt32 deviceId = AK_INVALID_DEVICE_ID;
if (deviceName.Len() == 0) {
// getting default device
AK::GetWindowsDevice(-1, deviceId, NULL, AkDeviceState_Active);
auto deviceNameWstr = AK::GetWindowsDeviceName(-1, deviceId, AkDeviceState_Active);
result.Key = deviceId;
result.Value = FString(deviceNameWstr);
} else {
AkUInt32 immDeviceCount = AK::GetWindowsDeviceCount(AkDeviceState_Active);
for (AkUInt32 i = 0; i < immDeviceCount; ++i) {
AK::GetWindowsDevice(i, deviceId, NULL, AkDeviceState_Active);
auto deviceNameWstr = AK::GetWindowsDeviceName(i, deviceId, AkDeviceState_Active);
if (FString(deviceNameWstr).Contains(deviceName)) {
result.Key = deviceId;
result.Value = FString(deviceNameWstr);
break;
}
}
}
return result;
}
AkOutputDeviceID FAkAudioDevice::AddCustomOutput(FString AudioDevice, FString WwiseDevice, UAkComponent* in_pComponent)
{
TTuple <AkUInt32, FString> Device;
AkOutputDeviceID deviceId = AK_INVALID_DEVICE_ID;
FString WwiseDeviceName = "System";
AKRESULT res = AK_Fail;
if (AudioDevice.Len() == 0 && WwiseDevice.Len() == 0) {
return deviceId;
}
if (WwiseDevice.Len() > 0) {
WwiseDeviceName = WwiseDevice;
}
Device = SearchAudioDeviceIdByName(*AudioDevice);
if (Device.Key) {
AkOutputSettings outputSettings(*WwiseDeviceName, Device.Key);
auto gameObjID = in_pComponent->GetAkGameObjectID();
res = AK::SoundEngine::AddOutput(outputSettings, &deviceId, &gameObjID, 1);
}
FString componentName = in_pComponent->GetName();
if (res != AK_Success) {
UE_LOG(LogAkAudio, Error, TEXT("Error attaching of AkComponent \"%s\" to \"%s\" <-> \"%s\". Error \"%d"), *componentName, *AudioDevice, *WwiseDeviceName, res);
} else {
UE_LOG(LogAkAudio, Warning, TEXT("AkComponent \"%s\" attached to \"%s\" <-> \"%s\" "), *componentName, *Device.Value, *WwiseDeviceName);
}
return deviceId;
}
AKRESULT FAkAudioDevice::RemoveCustomOutput(AkOutputDeviceID deviceId)
{
return AK::SoundEngine::RemoveOutput(deviceId);
}
AKComponent.h
接着转到 /Classes/AkComponent.h,并在 public: 之后添加以下代码:
/**
* Name of Audio Device in Wwise. If empty, "System" is using
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay, Category = "AkComponent")
FString WwiseDeviceName;
/**
* Name of Audio Device in OS. If empty, default device is using
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay, Category = "AkComponent")
FString AudioDeviceName;
AkOutputDeviceID OutputID;
AkComponent.cpp
最后转到 /Private/AkComponent.cpp,搜索两个空函数 PostRegisterGameObject 和 PostUnregisterGameObject,并将其替换为:
void UAkComponent::PostRegisterGameObject()
{
FAkAudioDevice* AkAudioDevice = FAkAudioDevice::Get();
if (AudioDeviceName.Len() > 0 || WwiseDeviceName.Len() > 0) {
OutputID = AkAudioDevice->AddCustomOutput(AudioDeviceName, WwiseDeviceName, this);
}
}
void UAkComponent::PostUnregisterGameObject()
{
FAkAudioDevice* AkAudioDevice = FAkAudioDevice::Get();
if (AkAudioDevice && OutputID != AK_INVALID_DEVICE_ID) {
AkAudioDevice->RemoveCustomOutput(OutputID);
}
}
恭喜你到达这里!接下来还需要在 Visual Studio 中编译工程并运行 Unreal Editor ,就可以验证成果了。
如果前面全部操作无误的话,Unreal Editor 的 AkComponent 设置中会出现两个新的文本字段。利用这两个字段,我们可以将音频输出设备与 Wwise音频设备互连。
- Wwise Device Name - 本例中为 System 或 System_VR。
- Audio Device Name - 硬件设备名称,比如 Sony Headphones。其实只要输入 Headphones 应该就足以找到实际设备了。
无论有没有找到设备,Unreal Editor 的 Output Log 中都会显示相应的结果:
第 3 步:收尾工作
最后一步是设置与角色对应的 Listener。首先,为每个 Pawn/Character 创建一个 AkComponent。然后,在各自的 AkComponent 设置中键入 Audio Device 的名称。
注意(非常重要):每个 Emitter 都需要设置 Listener,否则整套设置便会不起作用。对此,有专门的 Blueprint 函数 Set Listeners。
比方说,可以利用BeginPlay事件来执行这个操作:
就这么简单!最后可以在 Unreal Editor 中测试一下播放效果,看看两个 Audio Device 是否都能正常运行。这时应当会在 Output log 中看到以下内容,并同时听到来自两个输出设备的声音。
有兴趣的话,不妨看下我的 Soundcloud。如果有什么问题,可以发消息给我。
评论