// Copyright Epic Games, Inc. All Rights Reserved. #include "MetasoundTriggerOnThresholdNode.h" #include "MetasoundAudioBuffer.h" #include "MetasoundEnumRegistrationMacro.h" #include "MetasoundExecutableOperator.h" #include "MetasoundFacade.h" #include "MetasoundNodeRegistrationMacro.h" #include "MetasoundDataTypeRegistrationMacro.h" #include "MetasoundOperatorSettings.h" #include "MetasoundPrimitives.h" #include "MetasoundStandardNodesNames.h" #include "MetasoundTrigger.h" #include "MetasoundVertex.h" #include "MetasoundStandardNodesCategories.h" #define LOCTEXT_NAMESPACE "MetasoundStandardNodes" namespace Metasound { enum class EBufferTriggerType { RisingEdge, FallingEdge, AbsThreshold, }; DECLARE_METASOUND_ENUM(EBufferTriggerType, EBufferTriggerType::RisingEdge, METASOUNDSTANDARDNODES_API, FEnumBufferTriggerType, FEnumBufferTriggerTypeInfo, FBufferTriggerTypeReadRef, FEnumBufferTriggerTypeWriteRef); DEFINE_METASOUND_ENUM_BEGIN(EBufferTriggerType, FEnumBufferTriggerType, "BufferTriggerType") DEFINE_METASOUND_ENUM_ENTRY_NOTOOLTIP(EBufferTriggerType::RisingEdge, "RisingEdgeDescription", "Rising Edge"), DEFINE_METASOUND_ENUM_ENTRY_NOTOOLTIP(EBufferTriggerType::FallingEdge, "FallingEdgeDescription", "Falling Edge"), DEFINE_METASOUND_ENUM_ENTRY_NOTOOLTIP(EBufferTriggerType::AbsThreshold, "AbsThresholdDescription", "Abs Threshold") DEFINE_METASOUND_ENUM_END() namespace TriggerOnThresholdVertexNames { METASOUND_PARAM(OutPin, "Out", "Output"); METASOUND_PARAM(InPin, "In", "Input"); METASOUND_PARAM(InThresholdPin, "Threshold", "Trigger Threshold"); METASOUND_PARAM(InTriggerType, "Type", "Trigger Threshold Type"); } // This object is a set of templatized functions containing all the code that varies between int/float and Audio input in the Operator. // The base implementation is int/float, and there is a specialized template class for Audio below this one. // The reason we templatize ValueType and ThresholdType is because for Audio Buffers, they are not the same. // The actual operator class uses this as a more general set of functions that works for all the data types. template struct TTriggerOnThresholdHelper { // Generate is called in Execute, with a different PREDICATE depending on whether it's Rising or Falling Edge. // That PREDICATE is the only difference between the implementation of those two enum values. template static void Generate(const PREDICATE& ValueTester, const ValueType& InputValue, const ThresholdType& Threshold, ValueType& LastSample, FTriggerWriteRef& Out) { // block-rate only fires on the first frame if (!ValueTester(LastSample, Threshold) && ValueTester(InputValue, Threshold)) { Out->TriggerFrame(0); } // Remember the last sample for next block. LastSample = InputValue; } // Absolute Threshold has different implementation--the other one does not work for gain tracking a bipolar signal like audio (ex. range [-1.0, 1.0]). static void GenerateAbs(const ValueType& InputValue, const ThresholdType& Threshold, bool& bTriggered, FTriggerWriteRef& Out) { const float ThresholdSqr = Threshold * Threshold; const float CurrentSqr = InputValue * InputValue; // Just went above the threshold, so send the trigger if (CurrentSqr > ThresholdSqr && !bTriggered) { bTriggered = true; Out->TriggerFrame(0); } // Below the threshold, so it's safe to send another trigger next time else if (CurrentSqr < ThresholdSqr && bTriggered) { bTriggered = false; } } // In DeclareVertexInterface, the only change is the first TInputDataVertex and how it's constructed. // The Audio version cannot have a default value, but float/int need it. // For this node, the value of the default doesn't matter, as it needs to receive input from a connection elsewhere to function as a TriggerOnThreshold. static const FVertexInterface DeclareVertexInterface(const float& DefaultThreshold) { using namespace TriggerOnThresholdVertexNames; static const FVertexInterface Interface( FInputVertexInterface( TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(InPin), static_cast(0)), TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(InThresholdPin), DefaultThreshold), TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(InTriggerType), (int32)EBufferTriggerType::RisingEdge) ), FOutputVertexInterface( TOutputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(OutPin)) ) ); return Interface; } // In CreateOperator, the only thing that varies between Audio and non-audio input is whether there is a default value for the Input. static TDataReadReference CreateInput(const FBuildOperatorParams& InParams) { using namespace TriggerOnThresholdVertexNames; const FOperatorSettings& Settings = InParams.OperatorSettings; const FInputVertexInterfaceData& InputData = InParams.InputData; return InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME(InPin), Settings); } }; // Audio Buffer specialization. // For details about each function, see the general template class above this one. They look mostly the same, // but most of the differences are difficult or impossible to replicate without specialization. template<> struct TTriggerOnThresholdHelper { template static void Generate(const PREDICATE& ValueTester, const FAudioBuffer& Input, const float& Threshold, float& LastSample, FTriggerWriteRef& Out) { const float* InputBuffer = Input.GetData(); const int32 NumFrames = Input.Num(); float Previous = LastSample; for (int32 i = 0; i < NumFrames; ++i) { const float Current = *InputBuffer; // If Previous didn't trigger but current value does... fire! if (!ValueTester(Previous, Threshold) && ValueTester(Current, Threshold)) { Out->TriggerFrame(i); } Previous = Current; ++InputBuffer; } // Remember the last sample for next Audio Block. LastSample = Previous; } static void GenerateAbs(const FAudioBuffer& Input, const float& Threshold, bool& bTriggered, FTriggerWriteRef& Out) { const float* InputBuffer = Input.GetData(); const int32 NumFrames = Input.Num(); const float ThresholdSqr = Threshold * Threshold; for (int32 i = 0; i < NumFrames; ++i) { const float Current = *InputBuffer; const float CurrentSqr = Current * Current; // Just went above the threshold, so send the trigger if (CurrentSqr > ThresholdSqr && !bTriggered) { bTriggered = true; Out->TriggerFrame(i); } else if (CurrentSqr < ThresholdSqr && bTriggered) { bTriggered = false; } ++InputBuffer; } } static const FVertexInterface DeclareVertexInterface(const float& DefaultThreshold) { using namespace TriggerOnThresholdVertexNames; static const FVertexInterface Interface( FInputVertexInterface( TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(InPin)), TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(InThresholdPin), DefaultThreshold), TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(InTriggerType)) ), FOutputVertexInterface( TOutputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(OutPin)) ) ); return Interface; } static FAudioBufferReadRef CreateInput(const FBuildOperatorParams& InParams) { using namespace TriggerOnThresholdVertexNames; const FInputVertexInterfaceData& InputData = InParams.InputData; const FOperatorSettings& Settings = InParams.OperatorSettings; return InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME(InPin), Settings); } }; // The regular operator class. It's pretty standard from here, just templatized for all the types. template class TTriggerOnThresholdOperator : public TExecutableOperator> { public: static constexpr ThresholdType DefaultThreshold = 0.85f; TTriggerOnThresholdOperator(const FOperatorSettings& InSettings, const TDataReadReference& InBuffer, const TDataReadReference& InThreshold, const FBufferTriggerTypeReadRef& InTriggerType) : In(InBuffer) , Threshold(InThreshold) , TriggerType(InTriggerType) , Out(FTriggerWriteRef::CreateNew(InSettings)) {} void Execute() { Out->AdvanceBlock(); switch (*TriggerType) { case EBufferTriggerType::RisingEdge: default: TTriggerOnThresholdHelper::Generate(TGreater {}, *In, *Threshold, LastSample, Out); break; case EBufferTriggerType::FallingEdge: TTriggerOnThresholdHelper::Generate(TLess {}, *In, *Threshold, LastSample, Out); break; case EBufferTriggerType::AbsThreshold: TTriggerOnThresholdHelper::GenerateAbs(*In, *Threshold, bTriggered, Out); break; } } void Reset(const IOperator::FResetParams& InParams) { Out->Reset(); bTriggered = false; LastSample = 0; } virtual void BindInputs(FInputVertexInterfaceData& InOutVertexData) override { using namespace TriggerOnThresholdVertexNames; InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InThresholdPin), Threshold); InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InPin), In); InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InTriggerType), TriggerType); } virtual void BindOutputs(FOutputVertexInterfaceData& InOutVertexData) override { using namespace TriggerOnThresholdVertexNames; InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(OutPin), Out); } static const FVertexInterface DeclareVertexInterface() { return TTriggerOnThresholdHelper::DeclareVertexInterface(DefaultThreshold); } static const FNodeClassMetadata& GetNodeInfo() { auto InitNodeInfo = []() -> FNodeClassMetadata { const FText NodeDisplayName = METASOUND_LOCTEXT_FORMAT("TriggerOnThreshold_DisplayNamePattern", "Trigger On Threshold ({0})", GetMetasoundDataTypeDisplayText()); FNodeClassMetadata Info; Info.ClassName = { StandardNodes::Namespace, TEXT("TriggerOnThreshold"), GetMetasoundDataTypeName() }; Info.MajorVersion = 1; Info.MinorVersion = 0; Info.DisplayName = NodeDisplayName; Info.Description = METASOUND_LOCTEXT("TriggerOnThresholdNode_Description", "Trigger when input passes a given threshold."); Info.Author = PluginAuthor; Info.PromptIfMissing = PluginNodeMissingPrompt; Info.DefaultInterface = DeclareVertexInterface(); Info.CategoryHierarchy.Emplace(NodeCategories::Trigger); return Info; }; static const FNodeClassMetadata Info = InitNodeInfo(); return Info; } static TUniquePtr CreateOperator(const FBuildOperatorParams& InParams, FBuildResults& OutResults) { using namespace TriggerOnThresholdVertexNames; const FOperatorSettings& Settings = InParams.OperatorSettings; const FInputVertexInterfaceData& InputData = InParams.InputData; FBufferTriggerTypeReadRef Type = InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME(InTriggerType), Settings); TDataReadReference Input = TTriggerOnThresholdHelper::CreateInput(InParams); TDataReadReference Threshold = InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME(InThresholdPin), Settings); return MakeUnique>(InParams.OperatorSettings, Input, Threshold, Type); } protected: TDataReadReference In; TDataReadReference Threshold; FBufferTriggerTypeReadRef TriggerType; FTriggerWriteRef Out; bool bTriggered = false; ThresholdType LastSample = 0; }; // Mac Clang requires linkage on constexpr template <> constexpr float TTriggerOnThresholdOperator::DefaultThreshold = 0.85f; template <> constexpr float TTriggerOnThresholdOperator::DefaultThreshold = 0.85f; template <> constexpr int32 TTriggerOnThresholdOperator::DefaultThreshold = 1; template using TTriggerOnThresholdNode = TNodeFacade>; using FTriggerOnThresholdAudioNode = TTriggerOnThresholdNode; METASOUND_REGISTER_NODE(FTriggerOnThresholdAudioNode); using FTriggerOnThresholdFloatNode = TTriggerOnThresholdNode; METASOUND_REGISTER_NODE(FTriggerOnThresholdFloatNode); using FTriggerOnThresholdInt32Node = TTriggerOnThresholdNode; METASOUND_REGISTER_NODE(FTriggerOnThresholdInt32Node); } #undef LOCTEXT_NAMESPACE //MetasoundStandardNodes