// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "Containers/CircularQueue.h" #include "Interfaces/MetasoundFrontendSourceInterface.h" #include "MetasoundArrayNodes.h" #include "MetasoundExecutableOperator.h" #include "MetasoundFacade.h" #include "MetasoundNodeInterface.h" #include "MetasoundParamHelper.h" #include "MetasoundTrigger.h" #define LOCTEXT_NAMESPACE "MetasoundFrontend" namespace Metasound { namespace ArrayNodeRandomGetVertexNames { METASOUND_PARAM(InputTriggerNextValue, "Next", "Trigger to get the next value in the randomized array.") METASOUND_PARAM(InputTriggerResetSeed, "Reset", "Trigger to reset the seed for the randomized array.") METASOUND_PARAM(InputRandomArray, "In Array", "Input array to randomized.") METASOUND_PARAM(InputWeights, "Weights", "Input array of weights to use for random selection. Will repeat if this array is shorter than the input array to select from.") METASOUND_PARAM(InputSeed, "Seed", "Seed to use for the random shuffle.") METASOUND_PARAM(InputNoRepeatOrder, "No Repeats", "The number of elements to track to avoid repeating in a row.") METASOUND_PARAM(InputEnableSharedState, "Enable Shared State", "Set to enabled to share state across instances of this MetaSound.") METASOUND_PARAM(OutputTriggerOnNext, "On Next", "Triggers when the \"Next\" input is triggered.") METASOUND_PARAM(OutputTriggerOnReset, "On Reset", "Triggers when the \"Shuffle\" input is triggered or if the array is auto-shuffled.") METASOUND_PARAM(ShuffleOutputValue, "Value", "Value of the current shuffled element.") } class METASOUNDFRONTEND_API FArrayRandomGet { public: FArrayRandomGet() = default; FArrayRandomGet(int32 InSeed, int32 InMaxIndex, const TArray& InWeights, int32 InNoRepeatOrder); ~FArrayRandomGet() = default; void Init(int32 InSeed, int32 InMaxIndex, const TArray& InWeights, int32 InNoRepeatOrder); void SetSeed(int32 InSeed); void SetNoRepeatOrder(int32 InNoRepeatOrder); void SetRandomWeights(const TArray& InRandomWeights); void ResetSeed(); int32 NextValue(); int32 GetNoRepeatOrder() const { return NoRepeatOrder; } int32 GetMaxIndex() const { return MaxIndex; } private: float ComputeTotalWeight(); // The current index into the array of indicies (wraps between 0 and ShuffleIndices.Num()) TArray PreviousIndices; TUniquePtr> PreviousIndicesQueue; int32 NoRepeatOrder = INDEX_NONE; // Array of indices (in order 0 to Num) int32 MaxIndex = 0; TArray RandomWeights; // Random stream to use to randomize the shuffling FRandomStream RandomStream; }; struct InitSharedStateArgs { FGuid SharedStateId; int32 Seed = INDEX_NONE; int32 NumElements = 0; int32 NoRepeatOrder = 0; bool bIsPreviewSound = false; TArray Weights; }; class METASOUNDFRONTEND_API FSharedStateRandomGetManager { public: static FSharedStateRandomGetManager& Get(); void InitSharedState(InitSharedStateArgs& InArgs); int32 NextValue(const FGuid& InSharedStateId); void SetSeed(const FGuid& InSharedStateId, int32 InSeed); void SetNoRepeatOrder(const FGuid& InSharedStateId, int32 InNoRepeatOrder); void SetRandomWeights(const FGuid& InSharedStateId, const TArray& InRandomWeights); void ResetSeed(const FGuid& InSharedStateId); private: FSharedStateRandomGetManager() = default; ~FSharedStateRandomGetManager() = default; FCriticalSection CritSect; TMap> RandomGets; }; template class TArrayRandomGetOperator : public TExecutableOperator> { public: using FArrayDataReadReference = TDataReadReference; using FArrayWeightReadReference = TDataReadReference>; using WeightsArrayType = TArray; using ElementType = typename MetasoundArrayNodesPrivate::TArrayElementType::Type; using FElementTypeWriteReference = TDataWriteReference; static const FVertexInterface& GetDefaultInterface() { using namespace ArrayNodeRandomGetVertexNames; static const FVertexInterface DefaultInterface( FInputVertexInterface( TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(InputTriggerNextValue)), TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(InputTriggerResetSeed)), TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(InputRandomArray)), TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(InputWeights)), TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(InputSeed), -1), TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(InputNoRepeatOrder), 1), TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(InputEnableSharedState), false) ), FOutputVertexInterface( TOutputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(OutputTriggerOnNext)), TOutputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(OutputTriggerOnReset)), TOutputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(ShuffleOutputValue)) ) ); return DefaultInterface; } static const FNodeClassMetadata& GetNodeInfo() { auto CreateNodeClassMetadata = []() -> FNodeClassMetadata { FName DataTypeName = GetMetasoundDataTypeName(); FName OperatorName = "Random Get"; FText NodeDisplayName = METASOUND_LOCTEXT_FORMAT("RandomArrayGetNode_OpDisplayNamePattern", "Random Get ({0})", GetMetasoundDataTypeDisplayText()); FText NodeDescription = METASOUND_LOCTEXT("RandomArrayGetNode_Description", "Randomly retrieve data from input array using the supplied weights."); FVertexInterface NodeInterface = GetDefaultInterface(); return MetasoundArrayNodesPrivate::CreateArrayNodeClassMetadata(DataTypeName, OperatorName, NodeDisplayName, NodeDescription, NodeInterface); }; static const FNodeClassMetadata Metadata = CreateNodeClassMetadata(); return Metadata; } static TUniquePtr CreateOperator(const FBuildOperatorParams& InParams, FBuildResults& OutResults) { using namespace ArrayNodeRandomGetVertexNames; using namespace MetasoundArrayNodesPrivate; const FInputVertexInterfaceData& InputData = InParams.InputData; FTriggerReadRef InTriggerNext = InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME(InputTriggerNextValue), InParams.OperatorSettings); FTriggerReadRef InTriggerReset = InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME(InputTriggerResetSeed), InParams.OperatorSettings); FArrayDataReadReference InInputArray = InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME(InputRandomArray), InParams.OperatorSettings); FArrayWeightReadReference InInputWeightsArray = InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME(InputWeights), InParams.OperatorSettings); FInt32ReadRef InSeedValue = InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME(InputSeed), InParams.OperatorSettings); FInt32ReadRef InNoRepeatOrder = InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME(InputNoRepeatOrder), InParams.OperatorSettings); FBoolReadRef bInEnableSharedState = InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME(InputEnableSharedState), InParams.OperatorSettings); return MakeUnique>(InParams, InTriggerNext, InTriggerReset, InInputArray, InInputWeightsArray, InSeedValue, InNoRepeatOrder, bInEnableSharedState); } TArrayRandomGetOperator( const FBuildOperatorParams& InParams, const FTriggerReadRef& InTriggerNext, const FTriggerReadRef& InTriggerReset, const FArrayDataReadReference& InInputArray, const TDataReadReference& InInputWeightsArray, const FInt32ReadRef& InSeedValue, const FInt32ReadRef& InNoRepeatOrder, const FBoolReadRef& bInEnableSharedState) : TriggerNext(InTriggerNext) , TriggerReset(InTriggerReset) , InputArray(InInputArray) , InputWeightsArray(InInputWeightsArray) , SeedValue(InSeedValue) , NoRepeatOrder(InNoRepeatOrder) , TriggerOnNext(FTriggerWriteRef::CreateNew(InParams.OperatorSettings)) , TriggerOnReset(FTriggerWriteRef::CreateNew(InParams.OperatorSettings)) , OutValue(TDataWriteReferenceFactory::CreateAny(InParams.OperatorSettings)) , bEnableSharedState(bInEnableSharedState) { using namespace Frontend; SharedStateUniqueId = InParams.Node.GetInstanceID(); Reset(InParams); } virtual ~TArrayRandomGetOperator() = default; virtual void BindInputs(FInputVertexInterfaceData& InOutVertexData) override { using namespace ArrayNodeRandomGetVertexNames; InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InputTriggerNextValue), TriggerNext); InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InputTriggerResetSeed), TriggerReset); InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InputRandomArray), InputArray); InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InputWeights), InputWeightsArray); InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InputSeed), SeedValue); InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InputNoRepeatOrder), NoRepeatOrder); InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InputEnableSharedState), bEnableSharedState); } virtual void BindOutputs(FOutputVertexInterfaceData& InOutVertexData) override { using namespace ArrayNodeRandomGetVertexNames; InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(OutputTriggerOnNext), TriggerOnNext); InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(OutputTriggerOnReset), TriggerOnReset); InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(ShuffleOutputValue), OutValue); } virtual FDataReferenceCollection GetInputs() const override { // This should never be called. Bind(...) is called instead. This method // exists as a stop-gap until the API can be deprecated and removed. checkNoEntry(); return {}; } virtual FDataReferenceCollection GetOutputs() const override { // This should never be called. Bind(...) is called instead. This method // exists as a stop-gap until the API can be deprecated and removed. checkNoEntry(); return {}; } void Reset(const IOperator::FResetParams& InParams) { using namespace Frontend; #if WITH_METASOUND_DEBUG_ENVIRONMENT if (InParams.Environment.Contains(SourceInterface::Environment::GraphName)) { GraphName = *InParams.Environment.GetValue(SourceInterface::Environment::GraphName); } #endif // WITH_METASOUND_DEBUG_ENVIRONMENT bIsPreviewSound = InParams.Environment.GetValue(SourceInterface::Environment::IsPreview); // Check to see if this is a global shuffler or a local one. // Global shuffler will use a namespace to opt into it. PrevSeedValue = *SeedValue; WeightsArray = *InputWeightsArray; const ArrayType& InputArrayRef = *InputArray; PrevArraySize = InputArrayRef.Num(); PrevNoRepeatOrder = FMath::Clamp(*NoRepeatOrder, 0, PrevArraySize - 1); *OutValue = TDataTypeFactory::CreateAny(InParams.OperatorSettings); TriggerOnNext->Reset(); TriggerOnReset->Reset(); } bool UseSharedState() const { return *bEnableSharedState && bSharedStateInitialized; } void Execute() { TriggerOnNext->AdvanceBlock(); TriggerOnReset->AdvanceBlock(); const ArrayType& InputArrayRef = *InputArray; if (InputArrayRef.Num() == 0) { #if WITH_METASOUND_DEBUG_ENVIRONMENT if (!bHasLoggedEmptyArrayWarning) { UE_LOG(LogMetaSound, Verbose, TEXT("Array Random Get: empty array input (Graph '%s')"), *GraphName); bHasLoggedEmptyArrayWarning = true; } #endif // WITH_METASOUND_DEBUG_ENVIRONMENT return; } TriggerReset->ExecuteBlock( [&](int32 StartFrame, int32 EndFrame) { }, [this](int32 StartFrame, int32 EndFrame) { if (IsStateInitializationNeeded()) { InitializeState(PrevArraySize); } if (UseSharedState()) { FSharedStateRandomGetManager& RGM = FSharedStateRandomGetManager::Get(); RGM.ResetSeed(SharedStateUniqueId); } else { check(ArrayRandomGet.IsValid()); ArrayRandomGet->ResetSeed(); } TriggerOnReset->TriggerFrame(StartFrame); } ); TriggerNext->ExecuteBlock( [&](int32 StartFrame, int32 EndFrame) { }, [this](int32 StartFrame, int32 EndFrame) { ExecuteTriggerNext(StartFrame); } ); } private: void ExecuteTriggerNext(int32 StartFrame) { const ArrayType& InputArrayRef = *InputArray; int32 OutRandomIndex = INDEX_NONE; const bool bIsStateReinitializationNeeded = IsStateInitializationNeeded(); const bool bIsArraySizeChanged = PrevArraySize != InputArrayRef.Num(); const bool bSeedValueChanged = PrevSeedValue != *SeedValue; const bool bNoRepeatOrderChanged = PrevNoRepeatOrder != *NoRepeatOrder; const bool bWeightsArrayChanged = WeightsArray != *InputWeightsArray; // Update cached values if changed if (bIsArraySizeChanged) { PrevArraySize = InputArrayRef.Num(); } if (bSeedValueChanged) { PrevSeedValue = *SeedValue; } if (bNoRepeatOrderChanged) { PrevNoRepeatOrder = *NoRepeatOrder; } if (bWeightsArrayChanged) { WeightsArray = *InputWeightsArray; } // Reinitialize state (with new values) if needed if (bIsStateReinitializationNeeded) { InitializeState(PrevArraySize); } // Update other state (which was not necessarily changed by new state initialization) if (bSeedValueChanged) { if (UseSharedState()) { FSharedStateRandomGetManager& RGM = FSharedStateRandomGetManager::Get(); RGM.SetSeed(SharedStateUniqueId, PrevSeedValue); } else { check(ArrayRandomGet.IsValid()); ArrayRandomGet->SetSeed(PrevSeedValue); } } if (bNoRepeatOrderChanged) { if (UseSharedState()) { FSharedStateRandomGetManager& RGM = FSharedStateRandomGetManager::Get(); RGM.SetNoRepeatOrder(SharedStateUniqueId, PrevNoRepeatOrder); } else { check(ArrayRandomGet.IsValid()); ArrayRandomGet->SetNoRepeatOrder(PrevNoRepeatOrder); } } if (bWeightsArrayChanged) { if (UseSharedState()) { FSharedStateRandomGetManager& RGM = FSharedStateRandomGetManager::Get(); RGM.SetRandomWeights(SharedStateUniqueId, WeightsArray); } else { check(ArrayRandomGet.IsValid()); ArrayRandomGet->SetRandomWeights(WeightsArray); } } // Get next value if (UseSharedState()) { FSharedStateRandomGetManager& RGM = FSharedStateRandomGetManager::Get(); OutRandomIndex = RGM.NextValue(SharedStateUniqueId); } else { check(ArrayRandomGet.IsValid()); OutRandomIndex = ArrayRandomGet->NextValue(); } check(OutRandomIndex != INDEX_NONE); #if WITH_METASOUND_DEBUG_ENVIRONMENT UE_LOG(LogMetaSound, Verbose, TEXT("Array Random Get: Index chosen: '%u'"), OutRandomIndex); #endif // WITH_METASOUND_DEBUG_ENVIRONMENT // The input array size may have changed, so make sure it's wrapped into range of the input array *OutValue = InputArrayRef[OutRandomIndex % InputArrayRef.Num()]; TriggerOnNext->TriggerFrame(StartFrame); } bool IsStateInitializationNeeded() { const ArrayType& InputArrayRef = *InputArray; const bool bIsArrayNonEmpty = InputArrayRef.Num() != 0; // Skip reinit if the array is empty because it represents an invalid state for this node. const bool bIsArraySizeChanged = PrevArraySize != InputArrayRef.Num(); // Need to reinit for array size changes. const bool bIsSharedStateEnablementInconsistent = (*bEnableSharedState != bSharedStateInitialized); // Need to reinit if the shared state enablement has been updated. const bool bIsNonSharedStateInitializationNeeded = !*bEnableSharedState && !ArrayRandomGet.IsValid(); // For the first initialization of the non shared state random get (bIsSharedStateEnablementInconsistent will take care of that for shared state) return (bIsArrayNonEmpty && (bIsArraySizeChanged || bIsSharedStateEnablementInconsistent || bIsNonSharedStateInitializationNeeded)); } void InitializeState(int32 InArraySize) { bSharedStateInitialized = false; if (InArraySize > 0) { if (*bEnableSharedState) { // Get the environment variable for the unique ID of the sound check(SharedStateUniqueId.IsValid()); FSharedStateRandomGetManager& RGM = FSharedStateRandomGetManager::Get(); InitSharedStateArgs Args; Args.SharedStateId = SharedStateUniqueId; Args.Seed = PrevSeedValue; Args.NumElements = PrevArraySize; Args.NoRepeatOrder = PrevNoRepeatOrder; Args.bIsPreviewSound = bIsPreviewSound; Args.Weights = WeightsArray; RGM.InitSharedState(Args); bSharedStateInitialized = true; } else { ArrayRandomGet = MakeUnique(PrevSeedValue, PrevArraySize, WeightsArray, PrevNoRepeatOrder); } } else { ArrayRandomGet = MakeUnique(); #if WITH_METASOUND_DEBUG_ENVIRONMENT UE_LOG(LogMetaSound, Verbose, TEXT("Array Random Get: Can't retrieve random elements from an empty array in graph '%s'"), *GraphName); #endif // WITH_METASOUND_DEBUG_ENVIRONMENT } } // Inputs FTriggerReadRef TriggerNext; FTriggerReadRef TriggerReset; FArrayDataReadReference InputArray; TDataReadReference InputWeightsArray; FInt32ReadRef SeedValue; FInt32ReadRef NoRepeatOrder; // Outputs FTriggerWriteRef TriggerOnNext; FTriggerWriteRef TriggerOnReset; TDataWriteReference OutValue; #if WITH_METASOUND_DEBUG_ENVIRONMENT FString GraphName; bool bHasLoggedEmptyArrayWarning = false; #endif // WITH_METASOUND_DEBUG_ENVIRONMENT // Data TUniquePtr ArrayRandomGet; TArray WeightsArray; int32 PrevSeedValue = INDEX_NONE; int32 PrevNoRepeatOrder = INDEX_NONE; FGuid SharedStateUniqueId; int32 PrevArraySize = 0; bool bIsPreviewSound = false; FBoolReadRef bEnableSharedState; bool bSharedStateInitialized = false; }; template class TArrayRandomGetNode : public FNodeFacade { public: TArrayRandomGetNode(const FNodeInitData& InInitData) : FNodeFacade(InInitData.InstanceName, InInitData.InstanceID, TFacadeOperatorClass>()) { } virtual ~TArrayRandomGetNode() = default; }; } #undef LOCTEXT_NAMESPACE