// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "Containers/CircularQueue.h" #include "CoreMinimal.h" #include "MetasoundArrayNodes.h" #include "MetasoundExecutableOperator.h" #include "MetasoundFacade.h" #include "MetasoundNodeInterface.h" #include "MetasoundTrigger.h" #define LOCTEXT_NAMESPACE "MetasoundFrontend_RandomArrayGet" namespace Metasound { namespace ArrayNodeRandomGetVertexNames { static const TCHAR* InputTriggerNextName = TEXT("Next"); static const TCHAR* InputTriggerResetName = TEXT("Reset"); static const TCHAR* InputRandomArrayName = TEXT("In Array"); static const TCHAR* InputWeightsName = TEXT("Weights"); static const TCHAR* InputSeedName = TEXT("Seed"); static const TCHAR* InputNoRepeatOrderName = TEXT("No Repeats"); static const TCHAR* InputEnableSharedStateName = TEXT("Enable Shared State"); static const TCHAR* OutputTriggerOnNextName = TEXT("On Next"); static const TCHAR* OutputTriggerOnResetName = TEXT("On Reset"); static const TCHAR* OutputValueName = TEXT("Value"); static const FText InputTriggerNextTooltip = LOCTEXT("TriggerNextTooltip", "Trigger to get the next value in the randomized array."); static const FText InputTriggerResetTooltip = LOCTEXT("TriggerResetTooltip", "Trigger to reset the seed for the randomized array."); static const FText InputRandomArrayTooltip = LOCTEXT("RandomArrayTooltip", "Input array to randomized."); static const FText InputWeightsTooltip = LOCTEXT("WeightsTooltip", "Input array of weights to use for random selection. Will repeat if this array is shorter than the input array to select from."); static const FText InputSeedTooltip = LOCTEXT("SeedTooltip", "Seed to use for the the random shuffle."); static const FText InputNoRepeatOrderTooltip = LOCTEXT("NoRepeatOrderTooltip", "The number of elements to track to avoid repeating in a row."); static const FText InputEnableSharedStateTooltip = LOCTEXT("EnableSharedStateTooltip", "Set to enabled shared state across instances of this metasound."); static const FText OutputTriggerOnNextTooltip = LOCTEXT("TriggerOnNextTooltip", "Triggers when the \"Next\" input is triggered."); static const FText OutputTriggerOnResetTooltip = LOCTEXT("TriggerOnResetTooltip", "Triggers when the \"Shuffle\" input is triggered or if the array is auto-shuffled."); static const FText OutputValueTooltip = LOCTEXT("ValueTooltip", "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(); 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 { uint32 SharedStateId = 0; 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(uint32 InSharedStateId); void SetSeed(uint32 InSharedStateId, int32 InSeed); void SetNoRepeatOrder(uint32 InSharedStateId, int32 InNoRepeatOrder); void SetRandomWeights(uint32 InSharedStateId, const TArray& InRandomWeights); void ResetSeed(uint32 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( TInputDataVertexModel(InputTriggerNextName, InputTriggerNextTooltip), TInputDataVertexModel(InputTriggerResetName, InputTriggerResetTooltip), TInputDataVertexModel(InputRandomArrayName, InputRandomArrayTooltip), TInputDataVertexModel(InputWeightsName, InputWeightsTooltip), TInputDataVertexModel(InputSeedName, InputSeedTooltip, -1), TInputDataVertexModel(InputNoRepeatOrderName, InputNoRepeatOrderTooltip, 1), TInputDataVertexModel(InputEnableSharedStateName, InputEnableSharedStateTooltip, false) ), FOutputVertexInterface( TOutputDataVertexModel(OutputTriggerOnNextName, OutputTriggerOnNextTooltip), TOutputDataVertexModel(OutputTriggerOnResetName, OutputTriggerOnResetTooltip), TOutputDataVertexModel(OutputValueName, OutputValueTooltip) ) ); return DefaultInterface; } static const FNodeClassMetadata& GetNodeInfo() { auto CreateNodeClassMetadata = []() -> FNodeClassMetadata { FName DataTypeName = GetMetasoundDataTypeName(); FName OperatorName = "Random Get"; FText NodeDisplayName = FText::Format(LOCTEXT("RandomGetArrayOpDisplayNamePattern", "Random Get ({0})"), GetMetasoundDataTypeDisplayText()); FText NodeDescription = LOCTEXT("RandomGetArrayDescription", "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 FCreateOperatorParams& InParams, TArray>& OutErrors) { using namespace ArrayNodeRandomGetVertexNames; using namespace MetasoundArrayNodesPrivate; const FInputVertexInterface& Inputs = InParams.Node.GetVertexInterface().GetInputInterface(); FTriggerReadRef InTriggerNext = InParams.InputDataReferences.GetDataReadReferenceOrConstructWithVertexDefault(Inputs, InputTriggerNextName, InParams.OperatorSettings); FTriggerReadRef InTriggerReset = InParams.InputDataReferences.GetDataReadReferenceOrConstructWithVertexDefault(Inputs, InputTriggerResetName, InParams.OperatorSettings); FArrayDataReadReference InInputArray = InParams.InputDataReferences.GetDataReadReferenceOrConstructWithVertexDefault(Inputs, InputRandomArrayName, InParams.OperatorSettings); FArrayWeightReadReference InInputWeightsArray = InParams.InputDataReferences.GetDataReadReferenceOrConstructWithVertexDefault(Inputs, InputWeightsName, InParams.OperatorSettings); FInt32ReadRef InSeedValue = InParams.InputDataReferences.GetDataReadReferenceOrConstructWithVertexDefault(Inputs, InputSeedName, InParams.OperatorSettings); FInt32ReadRef InNoRepeatOrder = InParams.InputDataReferences.GetDataReadReferenceOrConstructWithVertexDefault(Inputs, InputNoRepeatOrderName, InParams.OperatorSettings); FBoolReadRef bInEnableSharedState = InParams.InputDataReferences.GetDataReadReferenceOrConstructWithVertexDefault(Inputs, InputEnableSharedStateName, InParams.OperatorSettings); return MakeUnique>(InParams, InTriggerNext, InTriggerReset, InInputArray, InInputWeightsArray, InSeedValue, InNoRepeatOrder, bInEnableSharedState); } TArrayRandomGetOperator( const FCreateOperatorParams& 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) , bEnableSharedState(bInEnableSharedState) , TriggerOnNext(FTriggerWriteRef::CreateNew(InParams.OperatorSettings)) , TriggerOnReset(FTriggerWriteRef::CreateNew(InParams.OperatorSettings)) , OutValue(TDataWriteReferenceFactory::CreateAny(InParams.OperatorSettings)) #if WITH_METASOUND_DEBUG_ENVIRONMENT , GraphName(*InParams.Environment.GetValue("GraphName")) #endif // WITH_METASOUND_DEBUG_ENVIRONMENT { // 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; PrevNoRepeatOrder = FMath::Max(*NoRepeatOrder, 0); WeightsArray = *InputWeightsArray; const ArrayType& InputArrayRef = *InputArray; PrevArraySize = InputArrayRef.Num(); if (PrevArraySize > 0) { if (*bEnableSharedState) { // Get the environment variable for the unique ID of the sound SharedStateUniqueId = InParams.Environment.GetValue(TEXT("SoundUniqueId")); check(SharedStateUniqueId != INDEX_NONE); bIsPreviewSound = InParams.Environment.GetValue(TEXT("IsPreviewSound")); 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); } else { ArrayRandomGet = MakeUnique(PrevSeedValue, PrevArraySize, WeightsArray, PrevNoRepeatOrder); } } #if WITH_METASOUND_DEBUG_ENVIRONMENT else { 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 } virtual ~TArrayRandomGetOperator() = default; virtual FDataReferenceCollection GetInputs() const override { using namespace ArrayNodeRandomGetVertexNames; FDataReferenceCollection Inputs; Inputs.AddDataReadReference(InputTriggerNextName, TriggerNext); Inputs.AddDataReadReference(InputTriggerResetName, TriggerReset); Inputs.AddDataReadReference(InputRandomArrayName, InputArray); Inputs.AddDataReadReference(InputWeightsName, InputWeightsArray); Inputs.AddDataReadReference(InputSeedName, SeedValue); Inputs.AddDataReadReference(InputNoRepeatOrderName, NoRepeatOrder); Inputs.AddDataReadReference(InputEnableSharedStateName, bEnableSharedState); return Inputs; } virtual FDataReferenceCollection GetOutputs() const override { using namespace ArrayNodeRandomGetVertexNames; FDataReferenceCollection Outputs; Outputs.AddDataReadReference(OutputTriggerOnNextName, TriggerOnNext); Outputs.AddDataReadReference(OutputTriggerOnResetName, TriggerOnReset); Outputs.AddDataReadReference(OutputValueName, OutValue); return Outputs; } void Execute() { TriggerOnNext->AdvanceBlock(); TriggerOnReset->AdvanceBlock(); const ArrayType& InputArrayRef = *InputArray; // If the array size has changed, we need to reinit before getting the next value if (PrevArraySize != InputArrayRef.Num()) { PrevArraySize = InputArrayRef.Num(); if (PrevArraySize != 0) { if (!ArrayRandomGet.IsValid()) { ArrayRandomGet = MakeUnique(PrevSeedValue, PrevArraySize, WeightsArray, PrevNoRepeatOrder); } ArrayRandomGet->Init(PrevSeedValue, PrevArraySize, WeightsArray, PrevNoRepeatOrder); } } if (PrevArraySize == 0) { if (!bHasLoggedEmptyArrayWarning) { #if WITH_METASOUND_DEBUG_ENVIRONMENT UE_LOG(LogMetaSound, Verbose, TEXT("Array Random Get: empty array input (Graph '%s')"), *GraphName); #endif // WITH_METASOUND_DEBUG_ENVIRONMENT bHasLoggedEmptyArrayWarning = true; } return; } // Check for a seed change if (PrevSeedValue != *SeedValue) { PrevSeedValue = *SeedValue; if (SharedStateUniqueId != INDEX_NONE) { FSharedStateRandomGetManager& RGM = FSharedStateRandomGetManager::Get(); RGM.SetSeed(SharedStateUniqueId, PrevSeedValue); } else { check(ArrayRandomGet.IsValid()); ArrayRandomGet->SetSeed(PrevSeedValue); } } if (PrevNoRepeatOrder != *NoRepeatOrder) { PrevNoRepeatOrder = *NoRepeatOrder; if (SharedStateUniqueId != INDEX_NONE) { FSharedStateRandomGetManager& RGM = FSharedStateRandomGetManager::Get(); RGM.SetNoRepeatOrder(SharedStateUniqueId, PrevNoRepeatOrder); } else { check(ArrayRandomGet.IsValid()); ArrayRandomGet->SetNoRepeatOrder(PrevNoRepeatOrder); } } WeightsArray = *InputWeightsArray; if (SharedStateUniqueId != INDEX_NONE) { FSharedStateRandomGetManager& RGM = FSharedStateRandomGetManager::Get(); RGM.SetRandomWeights(SharedStateUniqueId, WeightsArray); } else { check(ArrayRandomGet.IsValid()); ArrayRandomGet->SetRandomWeights(WeightsArray); } // Don't do anything if our array is empty if (InputArrayRef.Num() == 0) { return; } TriggerReset->ExecuteBlock( [&](int32 StartFrame, int32 EndFrame) { }, [this](int32 StartFrame, int32 EndFrame) { if (SharedStateUniqueId != INDEX_NONE) { 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) { const ArrayType& InputArrayRef = *InputArray; int32 OutRandomIndex = INDEX_NONE; if (SharedStateUniqueId != INDEX_NONE) { FSharedStateRandomGetManager& RGM = FSharedStateRandomGetManager::Get(); OutRandomIndex = RGM.NextValue(SharedStateUniqueId); } else { check(ArrayRandomGet.IsValid()); OutRandomIndex = ArrayRandomGet->NextValue(); } check(OutRandomIndex != INDEX_NONE); // 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); } ); } private: // Inputs FTriggerReadRef TriggerNext; FTriggerReadRef TriggerReset; FArrayDataReadReference InputArray; TDataReadReference InputWeightsArray; FInt32ReadRef SeedValue; FInt32ReadRef NoRepeatOrder; FBoolReadRef bEnableSharedState; // Outputs FTriggerWriteRef TriggerOnNext; FTriggerWriteRef TriggerOnReset; TDataWriteReference OutValue; #if WITH_METASOUND_DEBUG_ENVIRONMENT FString GraphName; #endif // WITH_METASOUND_DEBUG_ENVIRONMENT // Data TUniquePtr ArrayRandomGet; TArray WeightsArray; int32 PrevSeedValue = INDEX_NONE; int32 PrevNoRepeatOrder = INDEX_NONE; uint32 SharedStateUniqueId = INDEX_NONE; int32 PrevArraySize = 0; bool bIsPreviewSound = false; bool bHasLoggedEmptyArrayWarning = false; }; template class TArrayRandomGetNode : public FNodeFacade { public: TArrayRandomGetNode(const FNodeInitData& InInitData) : FNodeFacade(InInitData.InstanceName, InInitData.InstanceID, TFacadeOperatorClass>()) { } virtual ~TArrayRandomGetNode() = default; }; } #undef LOCTEXT_NAMESPACE