Найти тему
Ed Kashinsky

Имплементация двух аудио-устройств с помощью Wwise в Unreal Engine

Прежде всего, позвольте представиться, меня зовут Эд Кашинский и я - саунд-дизайнер и музыкант из Санкт-Петербурга. Сейчас я работаю над интересным и довольно нетипичным в плане  имплементации звука проектом. Это VR-игра Priest vs. Poltergeist VR, которая построена на базе локального мультиплеера: игра запускается на одном компьютере в ней есть два персонажа, и они оба персонажа присутствуют в одном мире. Один герой управляется с помощью VR-устройства, другой классически мышкой и клавиатурой. Звук для них должен также должен воспроизводиться на разных устройствах (наушники у VR и колонки/наушники на компьютере) и с разными настройками (позиционирование, аттенюация, окклюзия и тд). Если, например, на карте где-то падает стул, игрок, который находится ближе и например слева от него, должен услышать удар громче и в правом наушнике, а кто дальше, соотвественно тише.

-2

Поискав разные решения, в том числе стандартное от Unreal, мы остановились на Wwise. Он единственный аудио движок, который позволяет работать с несколькими устройствами в реальном времени.

Для работы с Unreal Engine у Wwise есть специальный плагин. Однако, не смотря на то, что это официальное решение, в нем нет возможности управлять аудио устройствами. Поэтому нам ничего не оставалось, как закатать рукава и реализовать эту возможность вручную с помощью С++.

Преамбула

В плагине для Unreal у Wwise основной компонент AkComponent - его можно добавлять как компонент к объектам на карте. Он может выступать в роли, как излучателя (emitter), так и слушателя (listener). Эмиттер воспроизводит звук (в примере выше - это как раз стул), а листнер, это что-то вроде ушей, который слушает всех источников и воспроизводит звук согласно положению персонажа на карте (его обычно прикрепляют к камере). 

В общем виде схема работы представлена на картинке:

-3
  • Эммитер запускает блюпринтовую функцию Post Ak Event, которая запускает ивент в Wwise. 
  • Этот ивент запускает звук. Звук в свою очередь отправляется на 2 разных audio buses 
  • Каждый audio bus отправляет звук на свое wwise-устройство (у нас они называются System и System_VR)
  • Каждый листнер соединяется со своим аудиоустройством (Output Device 1 и Output Device 2) и слушает звук из wwise. Когда звук приходит на audio buses, он воспроизводится каждый на своем устройстве.

И наша задача здесь заключается в том, чтобы листнер мог подключаться к wwise-устройству и отправлять звук на аудио-устройству. Для этого мы добавим 2 текстовых поля в AkComponent: в одном будем указывать wwise-устройство (Wwise Device Name), а в другом аудио-устройство (Audio Device Name)

-4

Итак, начнем!

Настройка Wwise

  • Начнем с того, что добавим wwise-устройство для VR. Выберем ему тип System и назовем “System_VR” (Это название нужно будет указывать в поле Wwise Device Name у AkComponent)
-5
  • Теперь создадим Audio Bus для VR и укажем ему устройство System_VR в качестве Audio Device
-6
  • Любой звук в Wwise мы можем отправить только на один Output Bus. И для того, чтобы один звук мог воспроизводиться на двух устройствах одновременно, нам необходимо использовать Auxiliary Buses.
-7

Поэтому создадим Auxiliary Bus для VR шины. Должна получиться следующая иерархия:

-8

Теперь для каждого звука мы можем выставлять на какое устройство мы можем отправить его. Для примера как это сделать:

  • Звук, который слышит только PC: выставляем Master Audio Bus у Output Bus, Auxiliary Buses оставляем пустым
  • Звук, который слышит только VR: выставляем Master Audio VR Bus у Output Bus, Auxiliary Buses оставляем пустым
  • Звук, который слышат оба персонажа: выставляем Master Audio Bus у Output Bus и Master_VR_Aux_Bus в списке Auxiliary Bus  
-9

На этом со стороны Wwise все готово!

Настройка AkComponent

Для начала нам нужно разобраться с компилятором, ибо чтобы наши изменения в плагине заработали, нам нужно пересобрать его. Подробно о том как сделать ваш проект 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);
-10

AkAudioDevice.cpp

Заходим в /Private/AkAudioDevice.cpp и добавляем следующий код в конце файла. Здесь происходит реализация методов указанных в .h-файле

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;
-11

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);
}
}
-12

И если вы дошли этого текста, то я вас поздравляю! Вам осталось только скомпилировать проект и запустить Unreal Engine. Теперь у AkComponent должны появится 2 поля, с помощью которых можно соединять аудио устройство с устройством в wwise:

  • Wwise Device Name - название устройства в Wwise (System или System_VR)
  • Audio Device Name - название аудио устройства. Тут не обязательно точное совпадение, может быть подстрока (например, “наушники”)

В случае успешного или неудачного подключения в логе вы увидите соответствующие сообщения:

-13

Применение новых возможностей

Перейдем наконец к знакомым всем блюпринтам. Чтобы падающий стул зазвучал, остался один шаг. Для начала нужно определить листнеров персонажей. Создадим AkComponent для каждого персонажа и укажем нужные аудиодевайсы:

-14

Теперь для каждого эмиттера мы должны явно указывать листнеров. Для этого существует функция SetListeners. У Target нужно обязательно указать эммитер, у массива листнеров нужно указать AkComponent'ы листнеров персонажей. Чтобы сделать это, я обычно делаю что-то типа такого:

-15

Дальше запускаем игру и слушаем звук эмиттера на двух устройствах обоих листнеров. В случае проблем проверьте Output Log:

-16

На этом все, заглядывайте ко мне на Soundcloud. Если будут вопросы, пишите мне в Facebook.