// Copyright Epic Games, Inc. All Rights Reserved. #include "ADMSpatialization.h" #include "ADMDirectOutChannel.h" #include "ADMSpatializationLog.h" #include "ADMSpatializationModule.h" #include "AudioMixerDevice.h" #include "Math/Transform.h" #include "OSCClient.h" #include "OSCMessage.h" #include "OSCTypes.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(ADMSpatialization) void UADMEngineSubsystem::SetSendIPAddress(const FString& InIPAddress, int32 Port) { using namespace UE::ADM::Spatialization; const FString EndpointStr = FString::Printf(TEXT("%s:%i"), *InIPAddress, Port); FIPv4Endpoint Endpoint; if (FIPv4Endpoint::Parse(EndpointStr, Endpoint)) { FModule& Module = FModuleManager::Get().LoadModuleChecked("ADMSpatialization"); FADMSpatializationFactory& SpatFactory = Module.GetFactory(); SpatFactory.SetSendIPEndpoint(Endpoint); } else { UE_LOG(LogADMSpatialization, Error, TEXT("Failed to parse specified ADM Spatialization client endpoint '%s'. Client IP not updated for ADM Spatialization."), *EndpointStr); } } namespace UE::ADM::Spatialization { namespace SpatPrivate { FString SendEndpointCVar; FAutoConsoleVariableRef CVarOSCADMSendEndpoint( TEXT("au.ADM.Spatialization.OSCSendEndpoint"), SendEndpointCVar, TEXT("Override send (client) endpoint for ADM spatializer messaging (in the form 127.0.0.1:8000).") TEXT("Default: Empty (Does not override project setting)"), ECVF_Default); int32 PositionAddressOffsetCVar = -1; FAutoConsoleVariableRef CVarOSCADMPositionAddressOffset( TEXT("au.ADM.Spatialization.OSCPositionAddressOffset"), PositionAddressOffsetCVar, TEXT("Applies an index offset to all object Ids translated to OSC position source addresses.") TEXT("Default: -1 (Use system default offset)"), ECVF_Default); static const FString SystemName(TEXT("ADM Spatialization")); } // namespace SpatPrivate FADMClient::FADMClient(const FIPv4Endpoint& InEndpoint, const int32 InObjectIndexOffset) : ObjectIndexOffset(InObjectIndexOffset) { FIPv4Endpoint IPEndpoint = InEndpoint; if (!SpatPrivate::SendEndpointCVar.IsEmpty()) { FIPv4Endpoint::Parse(SpatPrivate::SendEndpointCVar, IPEndpoint); } ClientProxy = UE::OSC::IClientProxy::Create(SpatPrivate::SystemName); ClientProxy->SetSendIPEndpoint(IPEndpoint); } int32 FADMClient::GetObjectIndexOffset() const { if (SpatPrivate::PositionAddressOffsetCVar >= 0) { return SpatPrivate::PositionAddressOffsetCVar; } else { return ObjectIndexOffset; } } FOSCAddress FADMClient::CreateConfigAddress(int32 InObjIndex, FString InMethod) const { TArray Containers { TEXT("adm"), TEXT("obj"), FString::FromInt(InObjIndex + GetObjectIndexOffset()), TEXT("config") }; FOSCAddress OSCAddress; OSCAddress.Set(MoveTemp(Containers), MoveTemp(InMethod)); return OSCAddress; } FOSCAddress FADMClient::CreatePositionAddress(int32 InObjIndex, FString InMethod) const { TArray Containers { TEXT("adm"), TEXT("obj"), FString::FromInt(InObjIndex + GetObjectIndexOffset()) }; FOSCAddress OSCAddress; OSCAddress.Set(MoveTemp(Containers), MoveTemp(InMethod)); return OSCAddress; } void FADMClient::InitObjectIndex(int32 InObjIndex, bool bCartesian) { using namespace UE::OSC; if (ClientProxy.IsValid()) { FOSCAddress OSCAddress = CreateConfigAddress(InObjIndex, TEXT("cartesian")); FOSCMessage Msg(MoveTemp(OSCAddress), { FOSCData((int32)(bCartesian)) }); ClientProxy->SendMessage(Msg); UE_LOG(LogADMSpatialization, Verbose, TEXT("InitObjectIndex: %d"), InObjIndex + GetObjectIndexOffset()); } } void FADMClient::SetPosition(int32 InObjIndex, const FVector& InPosition) { using namespace UE::OSC; if (ClientProxy.IsValid()) { FVector ADMPosition = UnrealToADMCoordinates(InPosition); FOSCAddress OSCAddress = CreatePositionAddress(InObjIndex, TEXT("xyz")); FOSCMessage Msg(MoveTemp(OSCAddress), { FOSCData((float)ADMPosition.X) , FOSCData((float)ADMPosition.Y), FOSCData((float)ADMPosition.Z) }); ClientProxy->SendMessage(Msg); } } bool FADMClient::IsSet() const { return ClientProxy.IsValid(); } FString FADMSpatializationFactory::GetDisplayName() { return SpatPrivate::SystemName; } bool FADMSpatializationFactory::SupportsPlatform(const FString& PlatformName) { return true; } TAudioSpatializationPtr FADMSpatializationFactory::CreateNewSpatializationPlugin(FAudioDevice* OwningDevice) { return MakeShared(); } UClass* FADMSpatializationFactory::GetCustomSpatializationSettingsClass() const { return UADMSpatializationSourceSettings::StaticClass(); }; bool FADMSpatializationFactory::IsExternalSend() { // Because this plugin sends all spatialized sources to direct outputs, // indicate to the engine that it acts as an external send. return true; } int32 FADMSpatializationFactory::GetMaxSupportedChannels() { return 1; } void FADMSpatialization::Initialize(const FAudioPluginInitializationParams InitializationParams) { FModule& Module = FModuleManager::Get().LoadModuleChecked("ADMSpatialization"); FADMSpatializationFactory& SpatFactory = Module.GetFactory(); const FIPv4Endpoint Endpoint = SpatFactory.GetSendIPEndpoint(); int32 NumDirectOutChannels = 0; Audio::FMixerDevice* AudioMixerDevice = static_cast(InitializationParams.AudioDevicePtr); if (!ensureMsgf(AudioMixerDevice, TEXT("Unable to initialize FADMSpatialization, null audio device"))) { return; } Audio::IAudioMixerPlatformInterface* MixerPlatform = AudioMixerDevice->GetAudioMixerPlatform(); if (!ensureMsgf(MixerPlatform, TEXT("Unable to initialize FADMSpatialization, null mixer platform"))) { return; } NumBedChannels = AudioMixerDevice->GetNumDeviceChannels(); NumDirectOutChannels = AudioMixerDevice->GetNumDirectOutChannels(); NumSources = InitializationParams.NumSources; SampleRate = InitializationParams.SampleRate; checkf(NumSources >= 0 && NumSources <= AudioMixerDevice->GetMaxSources(), TEXT("NumSources is expected to be in the range of 0 to max sources for the mixer device")); checkf(SampleRate == AudioMixerDevice->GetSampleRate(), TEXT("SampleRate is expected to match the mixer device sample rate")); DirectOuts.Reset(InitializationParams.NumSources); for (uint32 Index = 0; Index < InitializationParams.NumSources; Index++) { DirectOuts.Emplace(Index, InitializationParams.BufferLength, MixerPlatform); } SourceIdChannelMap.Empty(InitializationParams.NumSources); SetClient(FADMClient(Endpoint, NumBedChannels)); } void FADMSpatialization::Shutdown() { DirectOuts.Reset(); SourceIdChannelMap.Reset(); } void FADMSpatialization::SetClient(FADMClient&& InClient) { Client = MoveTemp(InClient); } bool FADMSpatialization::IsSpatializationEffectInitialized() const { return true; } void FADMSpatialization::OnInitSource(const uint32 SourceId, const FName& AudioComponentUserId, const uint32 NumChannels, USpatializationPluginSourceSettingsBase* InSettings) { checkf((int32)SourceId < NumSources, TEXT("SourceId is expected to be less than the initialized max number of sources")); if (ensure(!SourceIdChannelMap.Contains(SourceId))) { int32 ChannelIndex = INDEX_NONE; for (int32 Index = 0; Index < DirectOuts.Num(); ++Index) { if (!DirectOuts[Index].GetIsActive()) { ChannelIndex = Index; break; } } if (DirectOuts.IsValidIndex(ChannelIndex)) { DirectOuts[ChannelIndex].SetIsActive(true); DirectOuts[ChannelIndex].SetSourceId(SourceId); SourceIdChannelMap.Emplace(SourceId, ChannelIndex); // The client uses the channel index as the object index per // the ADM OSC spec Client.InitObjectIndex(ChannelIndex); UE_LOG(LogADMSpatialization, Verbose, TEXT("OnInitSource: %d [%d]"), SourceId, ChannelIndex); } else { UE_LOG(LogADMSpatialization, Warning, TEXT("Failed to find available direct out channel for SourceId: %d"), SourceId); } } } void FADMSpatialization::OnReleaseSource(const uint32 SourceId) { checkf((int32)SourceId < NumSources, TEXT("SourceId is expected to be less than the initialized max number of sources")); if (ensure(SourceIdChannelMap.Contains(SourceId))) { const int32 ChannelIndex = SourceIdChannelMap[SourceId]; if (ensure(DirectOuts.IsValidIndex(ChannelIndex))) { DirectOuts[ChannelIndex].SetIsActive(false); DirectOuts[ChannelIndex].SetSourceId(INDEX_NONE); } SourceIdChannelMap.Remove(SourceId); } } void FADMSpatialization::ProcessAudio(const FAudioPluginSourceInputData& InputData, FAudioPluginSourceOutputData& OutputData) { checkf(InputData.SourceId < NumSources, TEXT("SourceId is expected to be less than the initialized max number of sources")); if (SourceIdChannelMap.Contains(InputData.SourceId)) { const int32 ChannelIndex = SourceIdChannelMap[InputData.SourceId]; if (DirectOuts.IsValidIndex(ChannelIndex) && DirectOuts[ChannelIndex].GetIsActive()) { DirectOuts[ChannelIndex].ProcessDirectOut(InputData); FTransform ListenerTransform = FTransform(InputData.SpatializationParams->ListenerPosition); FVector ListenerRelEmitterPos = ListenerTransform.Inverse().TransformPosition(InputData.SpatializationParams->EmitterWorldPosition); Client.SetPosition(ChannelIndex, ListenerRelEmitterPos.GetSafeNormal()); } } } void FADMSpatialization::OnAllSourcesProcessed() { for (FSourceDirectOut& DirectOut : DirectOuts) { if (!DirectOut.GetIsActive()) { // Send silience out non-active outputs to retain time alignment with other outputs DirectOut.ProcessSilence(); } } } } // namespace UE::ADMSpatialization