// Copyright Epic Games, Inc. All Rights Reserved. #include "Internationalization/Text.h" #include "MetasoundExecutableOperator.h" #include "MetasoundNodeRegistrationMacro.h" #include "MetasoundDataTypeRegistrationMacro.h" #include "MetasoundPrimitives.h" #include "MetasoundStandardNodesNames.h" #include "MetasoundTrigger.h" #include "MetasoundTime.h" #include "MetasoundAudioBuffer.h" #include "DSP/BufferVectorOperations.h" #include "DSP/Delay.h" #include "DSP/Dsp.h" #include "MetasoundFacade.h" #include "MetasoundStandardNodesCategories.h" #include "MetasoundVertex.h" #define LOCTEXT_NAMESPACE "MetasoundStandardNodes_ITDPannerNode" namespace Metasound { namespace ITDPannerVertexNames { const FVertexName& GetInputAudioName() { static FVertexName Name = TEXT("In"); return Name; } const FText& GetInputAudioDescription() { static FText Desc = LOCTEXT("ITDPannerNodeInDesc", "The input audio to spatialize."); return Desc; } const FVertexName& GetInputPanAngleName() { static FVertexName Name = TEXT("Angle"); return Name; } const FText& GetInputPanAngleDescription() { static FText Desc = LOCTEXT("ITDPannerNodePanAngleDesc", "The sound source angle in degrees. 90 degrees is in front, 0 degrees is to the right, 270 degrees is behind, 180 degrees is to the left."); return Desc; } const FVertexName& GetInputDistanceFactorName() { static FVertexName Name = TEXT("Distance Factor"); return Name; } const FText& GetInputDistanceFactorDescription() { static FText Desc = LOCTEXT("ITDPannerNodeDistanceFactorDesc", "The normalized distance factor (0.0 to 1.0) to use for ILD (Inter-aural level difference) calculations. 0.0 is near, 1.0 is far. The further away something is the less there is a difference in levels (gain) between the ears."); return Desc; } const FVertexName& GetInputHeadWidthName() { static FVertexName Name = TEXT("Head Width"); return Name; } const FText& GetInputHeadWidthDescription() { static FText Desc = LOCTEXT("ITDPannerNodeHeadWidthDesc", "The width of the listener head to use for ITD calculations in centimeters."); return Desc; } const FVertexName& GetOutputAudioLeftName() { static FVertexName Name = TEXT("Out Left"); return Name; } const FText& GetOutputAudioLeftDescription() { static FText Desc = LOCTEXT("ITDPannerNodeOutputLeftDescription", "Left channel audio output."); return Desc; } const FVertexName& GetOutputAudioRightName() { static FVertexName Name = TEXT("Out Right"); return Name; } const FText& GetOutputAudioRightDescription() { static FText Desc = LOCTEXT("ITDPannerNodeOutputRightDescription", "Right channel audio output."); return Desc; } } class FITDPannerOperator : public TExecutableOperator { public: static const FNodeClassMetadata& GetNodeInfo(); static const FVertexInterface& GetVertexInterface(); static TUniquePtr CreateOperator(const FCreateOperatorParams& InParams, FBuildErrorArray& OutErrors); FITDPannerOperator(const FOperatorSettings& InSettings, const FAudioBufferReadRef& InAudioInput, const FFloatReadRef& InPanningAngle, const FFloatReadRef& InDistanceFactor, const FFloatReadRef& InHeadWidth); virtual FDataReferenceCollection GetInputs() const override; virtual FDataReferenceCollection GetOutputs() const override; void Execute(); private: void UpdateParams(bool bIsInit); FAudioBufferReadRef AudioInput; FFloatReadRef PanningAngle; FFloatReadRef DistanceFactor; FFloatReadRef HeadWidth; FAudioBufferWriteRef AudioLeftOutput; FAudioBufferWriteRef AudioRightOutput; float CurrAngle = 0.0f; float CurrX = 0.0f; float CurrY = 0.0f; float CurrDistanceFactor = 0.0f; float CurrHeadWidth = 0.0f; float CurrLeftGain = 0.0f; float CurrRightGain = 0.0f; float CurrLeftDelay = 0.0f; float CurrRightDelay = 0.0f; float PrevLeftGain = 0.0f; float PrevRightGain = 0.0f; Audio::FDelay LeftDelay; Audio::FDelay RightDelay; }; FITDPannerOperator::FITDPannerOperator(const FOperatorSettings& InSettings, const FAudioBufferReadRef& InAudioInput, const FFloatReadRef& InPanningAngle, const FFloatReadRef& InDistanceFactor, const FFloatReadRef& InHeadWidth) : AudioInput(InAudioInput) , PanningAngle(InPanningAngle) , DistanceFactor(InDistanceFactor) , HeadWidth(InHeadWidth) , AudioLeftOutput(FAudioBufferWriteRef::CreateNew(InSettings)) , AudioRightOutput(FAudioBufferWriteRef::CreateNew(InSettings)) { LeftDelay.Init(InSettings.GetSampleRate(), 0.5f); RightDelay.Init(InSettings.GetSampleRate(), 0.5f); const float EaseFactor = Audio::FExponentialEase::GetFactorForTau(0.1f, InSettings.GetSampleRate()); LeftDelay.SetEaseFactor(EaseFactor); RightDelay.SetEaseFactor(EaseFactor); CurrAngle = FMath::Clamp(*PanningAngle, 0.0f, 360.0f); CurrDistanceFactor = FMath::Clamp(*DistanceFactor, 0.0f, 1.0f); CurrHeadWidth = FMath::Max(*InHeadWidth, 0.0f); UpdateParams(true); PrevLeftGain = CurrLeftGain; PrevRightGain = CurrRightGain; } FDataReferenceCollection FITDPannerOperator::GetInputs() const { using namespace ITDPannerVertexNames; FDataReferenceCollection InputDataReferences; InputDataReferences.AddDataReadReference(GetInputAudioName(), AudioInput); InputDataReferences.AddDataReadReference(GetInputPanAngleName(), PanningAngle); InputDataReferences.AddDataReadReference(GetInputDistanceFactorName(), DistanceFactor); InputDataReferences.AddDataReadReference(GetInputHeadWidthName(), HeadWidth); return InputDataReferences; } FDataReferenceCollection FITDPannerOperator::GetOutputs() const { using namespace ITDPannerVertexNames; FDataReferenceCollection OutputDataReferences; OutputDataReferences.AddDataReadReference(GetOutputAudioLeftName(), AudioLeftOutput); OutputDataReferences.AddDataReadReference(GetOutputAudioRightName(), AudioRightOutput); return OutputDataReferences; } void FITDPannerOperator::UpdateParams(bool bIsInit) { // **************** // Update the x-y values const float CurrRadians = (CurrAngle / 360.0f) * 2.0f * PI; FMath::SinCos(&CurrY, &CurrX, CurrRadians); // **************** // Update ILD gains const float HeadRadiusMeters = 0.005f * CurrHeadWidth; // (InHeadWidth / 100.0f) / 2.0f; // InX is -1.0 to 1.0, so get it in 0.0 to 1.0 (i.e. hard left, hard right) const float Fraction = (CurrX + 1.0f) * 0.5f; // Feed the linear pan value into a equal power equation float PanLeft; float PanRight; FMath::SinCos(&PanRight, &PanLeft, 0.5f * PI * Fraction); // If distance factor is 1.0 (i.e. far away) this will have equal gain, if distance factor is 0.0 it will be normal equal power pan. CurrLeftGain = FMath::Lerp(PanLeft, 0.5f, CurrDistanceFactor); CurrRightGain = FMath::Lerp(PanRight, 0.5f, CurrDistanceFactor); // ********************* // Update the ITD delays // Use pythagorean theorem to get distances const float DistToLeftEar = FMath::Sqrt((CurrY * CurrY) + FMath::Square(HeadRadiusMeters + CurrX)); const float DistToRightEar = FMath::Sqrt((CurrY * CurrY) + FMath::Square(HeadRadiusMeters - CurrX)); // Compute delta time based on speed of sound constexpr float SpeedOfSound = 343.0f; const float DeltaTimeSeconds = (DistToLeftEar - DistToRightEar) / SpeedOfSound; if (DeltaTimeSeconds > 0.0f) { LeftDelay.SetEasedDelayMsec(1000.0f * DeltaTimeSeconds, bIsInit); RightDelay.SetEasedDelayMsec(0.0f, bIsInit); } else { LeftDelay.SetEasedDelayMsec(0.0f, bIsInit); RightDelay.SetEasedDelayMsec(-1000.0f * DeltaTimeSeconds, bIsInit); } } void FITDPannerOperator::Execute() { float NewHeadWidth = FMath::Max(*HeadWidth, 0.0f); float NewAngle = FMath::Clamp(*PanningAngle, 0.0f, 360.0f); float NewDistanceFactor = FMath::Clamp(*DistanceFactor, 0.0f, 1.0f); if (!FMath::IsNearlyEqual(NewAngle, CurrHeadWidth) || !FMath::IsNearlyEqual(NewDistanceFactor, CurrAngle) || !FMath::IsNearlyEqual(NewHeadWidth, CurrDistanceFactor)) { CurrHeadWidth = NewHeadWidth; CurrAngle = NewAngle; CurrDistanceFactor = NewDistanceFactor; UpdateParams(false); } const float* InputBufferPtr = AudioInput->GetData(); int32 InputSampleCount = AudioInput->Num(); float* OutputLeftBufferPtr = AudioLeftOutput->GetData(); float* OutputRightBufferPtr = AudioRightOutput->GetData(); // Feed the input audio into the left and right delays for (int32 i = 0; i < InputSampleCount; ++i) { OutputLeftBufferPtr[i] = LeftDelay.ProcessAudioSample(InputBufferPtr[i]); OutputRightBufferPtr[i] = RightDelay.ProcessAudioSample(InputBufferPtr[i]); } // Now apply the panning if (FMath::IsNearlyEqual(PrevLeftGain, CurrLeftDelay)) { Audio::MultiplyBufferByConstantInPlace(OutputLeftBufferPtr, InputSampleCount, PrevLeftGain); Audio::MultiplyBufferByConstantInPlace(OutputRightBufferPtr, InputSampleCount, PrevRightGain); } else { Audio::FadeBufferFast(OutputLeftBufferPtr, InputSampleCount, PrevLeftGain, CurrLeftGain); Audio::FadeBufferFast(OutputRightBufferPtr, InputSampleCount, PrevRightGain, CurrRightGain); PrevLeftGain = CurrLeftGain; PrevRightGain = CurrRightGain; } } const FVertexInterface& FITDPannerOperator::GetVertexInterface() { using namespace ITDPannerVertexNames; static const FVertexInterface Interface( FInputVertexInterface( TInputDataVertexModel(GetInputAudioName(), GetInputAudioDescription()), TInputDataVertexModel(GetInputPanAngleName(), GetInputPanAngleDescription(), 90.0f), TInputDataVertexModel(GetInputDistanceFactorName(), GetInputDistanceFactorDescription(), 0.0f), TInputDataVertexModel(GetInputHeadWidthName(), GetInputHeadWidthDescription(), 34.0f) ), FOutputVertexInterface( TOutputDataVertexModel(GetOutputAudioLeftName(), GetOutputAudioLeftDescription()), TOutputDataVertexModel(GetOutputAudioRightName(), GetOutputAudioRightDescription()) ) ); return Interface; } const FNodeClassMetadata& FITDPannerOperator::GetNodeInfo() { auto InitNodeInfo = []() -> FNodeClassMetadata { FNodeClassMetadata Info; Info.ClassName = { StandardNodes::Namespace, TEXT("ITD Panner"), TEXT("") }; Info.MajorVersion = 1; Info.MinorVersion = 0; Info.DisplayName = LOCTEXT("Metasound_ITDPannerDisplayName", "ITD Panner"); Info.Description = LOCTEXT("Metasound_ITDPannerNodeDescription", "Pans an input audio signal using an inter-aural time delay method."); Info.Author = PluginAuthor; Info.PromptIfMissing = PluginNodeMissingPrompt; Info.DefaultInterface = GetVertexInterface(); Info.CategoryHierarchy.Emplace(NodeCategories::Spatialization); return Info; }; static const FNodeClassMetadata Info = InitNodeInfo(); return Info; } TUniquePtr FITDPannerOperator::CreateOperator(const FCreateOperatorParams& InParams, FBuildErrorArray& OutErrors) { const FDataReferenceCollection& InputCollection = InParams.InputDataReferences; const FInputVertexInterface& InputInterface = GetVertexInterface().GetInputInterface(); using namespace ITDPannerVertexNames; FAudioBufferReadRef AudioIn = InputCollection.GetDataReadReferenceOrConstruct(GetInputAudioName(), InParams.OperatorSettings); FFloatReadRef PanningAngle = InputCollection.GetDataReadReferenceOrConstructWithVertexDefault(InputInterface, GetInputPanAngleName(), InParams.OperatorSettings); FFloatReadRef DistanceFactor = InputCollection.GetDataReadReferenceOrConstructWithVertexDefault(InputInterface, GetInputDistanceFactorName(), InParams.OperatorSettings); FFloatReadRef HeadWidth = InputCollection.GetDataReadReferenceOrConstructWithVertexDefault(InputInterface, GetInputHeadWidthName(), InParams.OperatorSettings); return MakeUnique(InParams.OperatorSettings, AudioIn, PanningAngle, DistanceFactor, HeadWidth); } class FITDPannerNode : public FNodeFacade { public: /** * Constructor used by the Metasound Frontend. */ FITDPannerNode(const FNodeInitData& InitData) : FNodeFacade(InitData.InstanceName, InitData.InstanceID, TFacadeOperatorClass()) { } }; METASOUND_REGISTER_NODE(FITDPannerNode) } #undef LOCTEXT_NAMESPACE