UE4のOnlineSubsystemOculusで空間音声のボイスチャットをしてみる

OnlineSubsystemOculusが空間音声に対応していないようだったので無理やり対応してみた。
空間音声になると声で方向が分かっていい。

# 環境

UE4.22.3

# やったこと

ボイスチャットは接続している前提([UE4のOnlineSubsystemをさわってみた - Qiita](https://qiita.com/mechamogera/items/312333b165b4b9940f09)参照)。

## OnlineSubsystemOculusをプロジェクトのプラグインにコピー

[UE4 Built-In Pluginの改造方法 [紫苑ノ遊戯三昧]](https://yugezanmai.life/game-engine/unreal-engine4/ue4-articles/plugin-remodeling)を参照して以下のようにコピー

コピー元：[Engineのパス]/Engine/Plugins/Online/OnlineSubsystemOculus
コピー先：[プロジェクトのパス]/Plugins/Online/OnlineSubsystemOculus

## OnlineSubsystemOculusの改造

[Voice Chat Spatialization - UE4 AnswerHub](https://answers.unrealengine.com/questions/204108/voice-chat-spatialization.html)を参照した。

以下のファイルを変更してOnlineSubsystemOculusでボイスチャットに使っているAudioComponentにアクセスできるようにし、Sound Attenuationも設定できるようにした。
AudioComponentのPlay()実行後にSoundAttenuationを設定しても効かなさそうだった（Stop() => Play()してもダメそうだった）。

* [プロジェクトのパス]/Plugins/Online/OnlineSubsystemOculus/Source/Private/OnlineVoiceOculus.h

```c++
class FOnlineVoiceOculus : public IOnlineVoice
{

public:
    ...
    UAudioComponent* FindOrCreateAudioComponent(const FUniqueNetId& UniqueId, bool& isCreate); // === 追加 === 

    ...
    static USoundAttenuation* SoundAttenuation; // === 追加 === 
```

* [プロジェクトのパス]/Plugins/Online/OnlineSubsystemOculus/Source/Private/OnlineVoiceOculus.cpp

```c++
USoundAttenuation* FOnlineVoiceOculus::SoundAttenuation = nullptr; // === 追加 === 

// === 以下のメソッド追加 === 
UAudioComponent*  FOnlineVoiceOculus::FindOrCreateAudioComponent(const FUniqueNetId& UniqueId, bool& isCreate)
{
	auto OculusId = static_cast<const FUniqueNetIdOculus&>(UniqueId);
	FRemoteTalkerDataOculus& QueuedData = RemoteTalkerBuffers.FindOrAdd(OculusId);
	isCreate = false;

	if (QueuedData.AudioComponent == nullptr || QueuedData.AudioComponent->IsPendingKill())
	{
		QueuedData.AudioComponent = CreateVoiceAudioComponent(OCULUS_VOICE_SAMPLE_RATE, OCULUS_NUM_VOICE_CHANNELS);
		if (QueuedData.AudioComponent)
		{
			QueuedData.AudioComponent->AddToRoot(); // make sure this doesn't get deleted by the GC
			QueuedData.AudioComponent->OnAudioFinishedNative.AddRaw(this, &FOnlineVoiceOculus::OnAudioFinished);
			QueuedData.AudioComponent->bAllowSpatialization = true;
			if (SoundAttenuation)
			{
				UE_LOG_ONLINE_VOICE(Warning, TEXT("FindOrCreate SetSoundAttenuationSettings"));
				QueuedData.AudioComponent->AttenuationSettings = SoundAttenuation;
			}
		}

		UE_LOG_ONLINE_VOICE(Log, TEXT("Create VoiceAudioComponent %s"), *UniqueId.ToString());
		isCreate = true;
	}

	return QueuedData.AudioComponent;
}

...

void FOnlineVoiceOculus::ProcessRemoteVoicePackets()
{
    ...
		if (ElementsWritten > 0)
		{
			// Mark this remote talker as talking
			if (!RemoteTalker.bIsTalking)
			{
				RemoteTalker.bIsTalking = true;
				TriggerOnPlayerTalkingStateChangedDelegates(RemoteTalker.TalkerId.ToSharedRef(), true);
			}

			FRemoteTalkerDataOculus& QueuedData = RemoteTalkerBuffers.FindOrAdd(RemoteTalkerId);

			QueuedData.LastSeen = CurrentTime;

			if (QueuedData.AudioComponent == nullptr || QueuedData.AudioComponent->IsPendingKill())
			{
				QueuedData.AudioComponent = CreateVoiceAudioComponent(OCULUS_VOICE_SAMPLE_RATE, OCULUS_NUM_VOICE_CHANNELS);
				if (QueuedData.AudioComponent)
				{
					QueuedData.AudioComponent->AddToRoot(); // make sure this doesn't get deleted by the GC
					QueuedData.AudioComponent->OnAudioFinishedNative.AddRaw(this, &FOnlineVoiceOculus::OnAudioFinished);
					//  === 以下の行を追加 === 
					QueuedData.AudioComponent->bAllowSpatialization = true;
					if (SoundAttenuation)
					{
						UE_LOG_ONLINE_VOICE(Warning, TEXT("SetSoundAttenuationSettings"));
						QueuedData.AudioComponent->AttenuationSettings = SoundAttenuation;
					}
					//  === 追加ここまで ===
				}
			}
   ...
}

```

以下のクラスを追加してブループリントから使うメソッドを実装した。

* [プロジェクトのパス]/Plugins/Online/OnlineSubsystemOculus/Source/Classes/OculusMyVoice.h

```c++
#pragma once

#include "UObject/Object.h"
#include "Net/OnlineBlueprintCallProxyBase.h"
#include "Interfaces/OnlineSessionInterface.h"
#include "Components/AudioComponent.h"
#include "Sound/SoundAttenuation.h"
#include "OculusMyVoice.generated.h"

UCLASS()
class UOculusMyVoice : public UBlueprintFunctionLibrary
{
	GENERATED_UCLASS_BODY()

	UFUNCTION(BlueprintCallable, Category = "Oculus|Voice")
	static UAudioComponent* FindOrCreateVoiceAudioComponent(FString OculusID, bool& IsCreate);

	UFUNCTION(BlueprintCallable, Category = "Oculus|Voice")
	static void SetSoundAttenuationForVoiceAudioComponent(USoundAttenuation* Attenuation);

	UFUNCTION(BlueprintCallable, Category = "Oculus|Voice")
	static void UpdateAudioComponentTransform(UAudioComponent* Component);
};
```

* [プロジェクトのパス]/Plugins/Online/OnlineSubsystemOculus/Source/Private/OculusMyVoice.cpp

```c++
#include "OculusMyVoice.h"
#include "Online.h"
#include "OnlineVoiceOculus.h"

UOculusMyVoice::UOculusMyVoice(class FObjectInitializer const &init) : Super(init)
{
}

UAudioComponent* UOculusMyVoice::FindOrCreateVoiceAudioComponent(FString OculusID, bool& IsCreate)
{
	IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
	if (!OnlineSub)
	{
		return nullptr;
	}

	auto IdentityInterface = OnlineSub->GetIdentityInterface();
	if (!IdentityInterface.IsValid())
	{
		return nullptr;
	}

	auto VoiceInterface = OnlineSub->GetVoiceInterface();
	if (!VoiceInterface.IsValid())
	{
		return nullptr;
	}

	UE_LOG(LogTemp, Warning, TEXT("FindOrCreateVoiceAudioComponent Step1"));

	auto uniqueNetId = IdentityInterface->CreateUniquePlayerId(OculusID);
	if (uniqueNetId.IsValid())
	{
		UE_LOG(LogTemp, Warning, TEXT("FindOrCreateVoiceAudioComponent Step2"));
		auto oculusVoiceInterface = StaticCastSharedPtr<FOnlineVoiceOculus>(VoiceInterface);
		return oculusVoiceInterface->FindOrCreateAudioComponent(*uniqueNetId, IsCreate);
	}
	return nullptr;
}

void UOculusMyVoice::SetSoundAttenuationForVoiceAudioComponent(USoundAttenuation* Attenuation)
{
	UE_LOG(LogTemp, Warning, TEXT("Set FOnlineVoiceOculus::SoundAttenuation"))
	FOnlineVoiceOculus::SoundAttenuation = Attenuation;
}

void UOculusMyVoice::UpdateAudioComponentTransform(UAudioComponent* Component)
{
	Component->OnUpdateTransform(EUpdateTransformFlags::PropagateFromParent);
}
```

## SoundAttenuation作成

* Editor上でAdd New>Sounds>Sound Attenuationで作成
* 作成したSound AttenuationのAttenuation FunctionをCustomにしてCustom Attenuation CurveでTime:0.0、Valuie:1.0とTime:1.0、Value:0.01にキーを打つ。
 * 遠く離れて声が聞こえなくなってから再び声が聞こえるようになると聞こえていなかった分の声が聞こえて音声が遅延する。このためとりあえず遠く離れてもボリュームが0にならないようにする。

![SoundAttenuationCap.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16768/3fe54255-5769-d335-79d6-e1b906de56d9.png)

## Blueprintの設定

* GameInstanceクラスのEvent Initで作成したSound AttenuationをOnlineSubsystemOculusに設定する。
 * とりあえずゲーム開始時の早いうち（AudioComponentが作成されない間）に設定するのがいいのかなということでGameInstanceで設定してみた。

![GameInstanceCap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16768/7b1541fb-8c6f-b7d0-9b21-772b7b053b7c.png)

* 空間音声をアタッチしたいActorのEvent BeginPlayでAudioComponentを取得してアタッチ
 * SetupVoiceAudioComponentはBeginPlayから呼び出し
 * OculusIDは適当に取得しておいたものを利用
 * CameraコンポーネントはAudioComponentをアタッチしたいコンポーネント

![PawnBeginPlayCap.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16768/a1b7b32d-8938-e375-9758-d259c0bdc36e.png)

* ActorのEvent Tickで作成したSound AttenuationのTransformを更新する
 * これをしないと位置が移動しても音声のある場所が更新されなかった
 * [Moving Attenuation Sounds - UE4 AnswerHub](https://answers.unrealengine.com/questions/701837/moving-attenuation-sounds.html)参照

![TickCap.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16768/45d1ca10-8d2c-6062-2a1f-fed0a997f89d.png)