// Copyright Epic Games, Inc. All Rights Reserved. #include "MetasoundSource.h" #include "Algo/AnyOf.h" #include "Algo/Find.h" #include "Algo/Transform.h" #include "AssetRegistry/AssetRegistryModule.h" #include "AudioDeviceManager.h" #include "Containers/Ticker.h" #include "IAudioParameterInterfaceRegistry.h" #include "Interfaces/ITargetPlatform.h" #include "Interfaces/MetasoundOutputFormatInterfaces.h" #include "Interfaces/MetasoundFrontendSourceInterface.h" #include "Internationalization/Text.h" #include "MetasoundAssetBase.h" #include "MetasoundAssetManager.h" #include "MetasoundAudioFormats.h" #include "MetasoundBuilderSubsystem.h" #include "MetasoundDocumentBuilderRegistry.h" #include "MetasoundDocumentInterface.h" #include "MetasoundDynamicOperatorTransactor.h" #include "MetasoundEngineAsset.h" #include "MetasoundEngineEnvironment.h" #include "MetasoundEnvironment.h" #include "MetasoundFrontendController.h" #include "MetasoundFrontendDataTypeRegistry.h" #include "MetasoundFrontendDocumentBuilder.h" #include "MetasoundFrontendDocumentIdGenerator.h" #include "MetasoundFrontendQuery.h" #include "MetasoundFrontendQuerySteps.h" #include "MetasoundFrontendTransform.h" #include "MetasoundGenerator.h" #include "MetasoundGlobals.h" #include "MetasoundLog.h" #include "MetasoundOperatorBuilderSettings.h" #include "MetasoundOperatorCacheSubsystem.h" #include "MetasoundOperatorSettings.h" #include "MetasoundParameterTransmitter.h" #include "MetasoundPrimitives.h" #include "MetasoundReceiveNode.h" #include "MetasoundSettings.h" #include "MetasoundTrace.h" #include "MetasoundTrigger.h" #include "MetasoundUObjectRegistry.h" #include "UObject/ObjectSaveContext.h" #include "UObject/ScriptInterface.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(MetasoundSource) #if WITH_EDITOR #include "Misc/DataValidation.h" #endif // WITH_EDITOR #if WITH_EDITORONLY_DATA #include "EdGraph/EdGraph.h" #endif // WITH_EDITORONLY_DATA #define LOCTEXT_NAMESPACE "MetaSound" namespace Metasound { namespace ConsoleVariables { bool bEnableExperimentalRuntimePresetGraphInflation = false; } namespace SourcePrivate { static constexpr float DefaultBlockRateConstant = 100.f; static constexpr float DefaultSampleRateConstant = 48000.f; static bool IsCookedForEditor(const FArchive& InArchive, const UObject* InObj) { #if WITH_EDITORONLY_DATA return ((InArchive.GetPortFlags() & PPF_Duplicate) == 0) && InObj->GetPackage()->HasAnyPackageFlags(PKG_Cooked); #else //WITH_EDITORONLY_DATA return false; #endif //WITH_EDITORONLY_DATA } static const FLazyName TriggerName = "Trigger"; // Holds onto a global static TSet for tracking which error/warning logs have been // trigger in order to avoid log spam. bool HasNotBeenLoggedForThisObject(const UMetaSoundSource& InMetaSound, uint32 InLogLineNumber) { using FObjectAddressAndLineNum = TTuple; static TSet LoggedSet; bool bIsAlreadyInSet = false; LoggedSet.Add(FObjectAddressAndLineNum(&InMetaSound, InLogLineNumber), &bIsAlreadyInSet); return !bIsAlreadyInSet; } Frontend::FMetaSoundAssetRegistrationOptions GetInitRegistrationOptions() { Frontend::FMetaSoundAssetRegistrationOptions RegOptions; RegOptions.bForceReregister = false; #if !WITH_EDITOR // When without editor, don't AutoUpdate or ResolveDocument at runtime. This only happens at cook or save. // When with editor, those are needed because sounds are not necessarily saved before previewing. RegOptions.bAutoUpdate = false; #endif // !WITH_EDITOR if (const UMetaSoundSettings* Settings = GetDefault()) { RegOptions.bAutoUpdateLogWarningOnDroppedConnection = Settings->bAutoUpdateLogWarningOnDroppedConnection; } return RegOptions; } class FParameterRouter { struct FQueueState { TWeakPtr> DataChannel; bool bWriterAvailable = true; }; public: using FAudioDeviceIDAndInstanceID = TTuple; TSharedPtr> FindOrCreateDataChannelForReader(Audio::DeviceID InDeviceID, uint64 InstanceID) { constexpr bool bIsForWriter = false; return FindOrCreateDataChannel(InDeviceID, InstanceID, bIsForWriter); } TSharedPtr> FindOrCreateDataChannelForWriter(Audio::DeviceID InDeviceID, uint64 InstanceID) { constexpr bool bIsForWriter = true; return FindOrCreateDataChannel(InDeviceID, InstanceID, bIsForWriter); } private: TSharedPtr> FindOrCreateDataChannel(Audio::DeviceID InDeviceID, uint64 InstanceID, bool bIsForWriter) { FScopeLock Lock(&DataChannelMapCS); FAudioDeviceIDAndInstanceID Key = {InDeviceID, InstanceID}; const bool bIsForReader = !bIsForWriter; if (FQueueState* State = DataChannels.Find(Key)) { // Allow multiple readers to be returned because FMetaSoundGenerators are recreated when they come out of virtualization. // Only allow a single writer to be returned because FMetaSoundParameterTransmitters are only created once const bool bIsAvailable = bIsForReader || (State->bWriterAvailable && bIsForWriter); if (bIsAvailable) { TSharedPtr> Channel = State->DataChannel.Pin(); if (Channel.IsValid()) { if (bIsForWriter) { State->bWriterAvailable = false; } return Channel; } } } TSharedPtr> NewChannel = MakeShared>(); FQueueState NewState; NewState.DataChannel = NewChannel; if (bIsForWriter) { NewState.bWriterAvailable = false; } DataChannels.Add(Key, NewState); return NewChannel; } FCriticalSection DataChannelMapCS; TSortedMap DataChannels; }; void CreateUObjectProxies(const Frontend::IDataTypeRegistry& InRegistry, FName InVertexTypeName, bool bClearUObjectPointers, FAudioParameter& InOutParamToInit) { using namespace Metasound; switch (InOutParamToInit.ParamType) { case EAudioParameterType::Object: { TSharedPtr ProxyPtr = InRegistry.CreateProxyFromUObject(InVertexTypeName, InOutParamToInit.ObjectParam); InOutParamToInit.ObjectProxies.Emplace(MoveTemp(ProxyPtr)); if (bClearUObjectPointers) { InOutParamToInit.ObjectParam = nullptr; } } break; case EAudioParameterType::ObjectArray: { const FName ElementTypeName = CreateElementTypeNameFromArrayTypeName(InVertexTypeName); for (TObjectPtr& Object : InOutParamToInit.ArrayObjectParam) { TSharedPtr ProxyPtr = InRegistry.CreateProxyFromUObject(ElementTypeName, Object); InOutParamToInit.ObjectProxies.Emplace(MoveTemp(ProxyPtr)); } if (bClearUObjectPointers) { InOutParamToInit.ArrayObjectParam.Reset(); } } break; default: break; } } FAudioParameter MakeAudioParameter(const Frontend::IDataTypeRegistry& InRegistry, FName InParamName, FName InTypeName, const FMetasoundFrontendLiteral& InLiteral, bool bCreateUObjectProxies) { constexpr bool bClearUObjectPointers = false; FAudioParameter Params; Params.ParamName = InParamName; Params.TypeName = InTypeName; switch (InLiteral.GetType()) { case EMetasoundFrontendLiteralType::Boolean: { if (Params.TypeName == TriggerName) { Params.ParamType = EAudioParameterType::Trigger; } else { Params.ParamType = EAudioParameterType::Boolean; } ensure(InLiteral.TryGet(Params.BoolParam)); } break; case EMetasoundFrontendLiteralType::BooleanArray: { Params.ParamType = EAudioParameterType::BooleanArray; ensure(InLiteral.TryGet(Params.ArrayBoolParam)); } break; case EMetasoundFrontendLiteralType::Integer: { Params.ParamType = EAudioParameterType::Integer; ensure(InLiteral.TryGet(Params.IntParam)); } break; case EMetasoundFrontendLiteralType::IntegerArray: { Params.ParamType = EAudioParameterType::IntegerArray; ensure(InLiteral.TryGet(Params.ArrayIntParam)); } break; case EMetasoundFrontendLiteralType::Float: { Params.ParamType = EAudioParameterType::Float; ensure(InLiteral.TryGet(Params.FloatParam)); } break; case EMetasoundFrontendLiteralType::FloatArray: { Params.ParamType = EAudioParameterType::FloatArray; ensure(InLiteral.TryGet(Params.ArrayFloatParam)); } break; case EMetasoundFrontendLiteralType::String: { Params.ParamType = EAudioParameterType::String; ensure(InLiteral.TryGet(Params.StringParam)); } break; case EMetasoundFrontendLiteralType::StringArray: { Params.ParamType = EAudioParameterType::StringArray; ensure(InLiteral.TryGet(Params.ArrayStringParam)); } break; case EMetasoundFrontendLiteralType::UObject: { Params.ParamType = EAudioParameterType::Object; UObject* Object = nullptr; ensure(InLiteral.TryGet(Object)); Params.ObjectParam = Object; if (bCreateUObjectProxies) { CreateUObjectProxies(InRegistry, InTypeName, bClearUObjectPointers, Params); } } break; case EMetasoundFrontendLiteralType::UObjectArray: { Params.ParamType = EAudioParameterType::ObjectArray; ensure(InLiteral.TryGet(MutableView(Params.ArrayObjectParam))); if (bCreateUObjectProxies) { CreateUObjectProxies(InRegistry, InTypeName, bClearUObjectPointers, Params); } } break; default: break; } return Params; } } // namespace SourcePrivate FGeneratorInstanceInfo::FGeneratorInstanceInfo(uint64 InAudioComponentID, uint64 InInstanceID, TWeakPtr InGenerator) : AudioComponentID(InAudioComponentID), InstanceID(InInstanceID), Generator(InGenerator) { } } // namespace Metasound FAutoConsoleVariableRef CVarMetaSoundEnableExperimentalRUntimePresetGraphInflation( TEXT("au.MetaSound.Experimental.EnableRuntimePresetGraphInflation"), Metasound::ConsoleVariables::bEnableExperimentalRuntimePresetGraphInflation, TEXT("Enables experimental feature of MetaSounds which reduces overhead of preset graphs\n") TEXT("Default: false"), ECVF_Default); // FAudioParameterCollector is used during preset graph inflation to collect audio parameters // overriden by preset graphs. This collection process is needed because preset graph inflation // circumvents the normal storage location of default inputs (They are stored on the registered // IGraph). class UMetaSoundSource::FAudioParameterCollector { // Utilitiy for sorting parameters by ParamName struct FSortPredicate { FORCEINLINE bool operator()(const FAudioParameter& InLHS, const FAudioParameter& InRHS) const { return InLHS.ParamName.FastLess(InRHS.ParamName); } }; // Utility for accessing the ParamName from a audio parameter struct FProjection { FORCEINLINE FName operator()(const FAudioParameter& InParam) const { return InParam.ParamName; } }; public: // FAudioParameterCollection merges new parameter into the provided array. The array is manipulated // in-place and so care must be taken that the `FAudioParameterCollector` does not attempt to access // invalid parameter arrays. FAudioParameterCollector(TArray& InOutParameters) : Parameters(InOutParameters) { Algo::Sort(Parameters, FSortPredicate()); } // Do not allow the FAudioParameterCollector to escape scope in order to prevent accidental // invalid access to Parameter array reference. FAudioParameterCollector(const FAudioParameterCollector&) = delete; FAudioParameterCollector(FAudioParameterCollector&&) = delete; FAudioParameterCollector& operator=(const FAudioParameterCollector&) = delete; FAudioParameterCollector& operator=(FAudioParameterCollector&&) = delete; // Merge in parameters from this preset. void CollectPresetOverrides(const TSet& InInputsInheritingDefault, const Metasound::TSortedVertexNameMap& InInputMap) { using namespace Metasound; for (const TPair& Pair : InInputMap) { // Avoid adding parameters which already exist in the ParameterArray int32 InsertPos = Algo::LowerBoundBy(Parameters, Pair.Key, FProjection(), FSortPredicate()); const bool bDoesNotAlreadyExist = (InsertPos >= Parameters.Num()) || (Pair.Key != Parameters[InsertPos].ParamName); if (bDoesNotAlreadyExist) { // Avoid adding parameters which are NOT overriding a default. if (!InInputsInheritingDefault.Contains(Pair.Key)) { Parameters.Insert(Pair.Value.DefaultParameter, InsertPos); } } } } private: TArray& Parameters; }; UMetaSoundSource::UMetaSoundSource(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , FMetasoundAssetBase() { // Default Root Graph uses static ID to distinguish between a default constructed document // (invalid ID) and CDO. A MetaSoundSource asset should only be constructed using the Document // Builder API to avoid ID collisions, but underlying UObjects must always be deterministically // generated using NewObject for serialization (and for CDOs). RootMetasoundDocument.RootGraph.ID = FGuid(0x4D657461, 0x536F756E, 0x64536F75, 0x72636500); bProcedural = true; bRequiresStopFade = true; NumChannels = 1; } const UClass& UMetaSoundSource::GetBaseMetaSoundUClass() const { return *UMetaSoundSource::StaticClass(); } const UClass& UMetaSoundSource::GetBuilderUClass() const { return *UMetaSoundSourceBuilder::StaticClass(); } const FMetasoundFrontendDocument& UMetaSoundSource::GetConstDocument() const { return RootMetasoundDocument; } #if WITH_EDITOR void UMetaSoundSource::PostEditUndo() { Super::PostEditUndo(); Metasound::Engine::FAssetHelper::PostEditUndo(*this); } void UMetaSoundSource::PreDuplicate(FObjectDuplicationParameters& DupParams) { Super::PreDuplicate(DupParams); Metasound::Engine::FAssetHelper::PreDuplicate(this, DupParams); } void UMetaSoundSource::PostDuplicate(EDuplicateMode::Type InDuplicateMode) { Super::PostDuplicate(InDuplicateMode); Metasound::Engine::FAssetHelper::PostDuplicate(this, InDuplicateMode); } void UMetaSoundSource::PostEditChangeProperty(FPropertyChangedEvent& InEvent) { Super::PostEditChangeProperty(InEvent); if (InEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(UMetaSoundSource, OutputFormat)) { PostEditChangeOutputFormat(); } if (InEvent.GetMemberPropertyName() == GET_MEMBER_NAME_CHECKED(UMetaSoundSource, SampleRateOverride) || InEvent.GetMemberPropertyName() == GET_MEMBER_NAME_CHECKED(UMetaSoundSource, BlockRateOverride) || InEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(UMetaSoundSource, QualitySetting) ) { PostEditChangeQualitySettings(); } } bool UMetaSoundSource::CanEditChange(const FProperty* InProperty) const { if (!Super::CanEditChange(InProperty)) { return false; } // Allow changes to quality if we don't have any overrides. if (InProperty->GetFName() == GET_MEMBER_NAME_CHECKED(UMetaSoundSource, QualitySetting)) { const TArray Platforms = FDataDrivenPlatformInfoRegistry::GetSortedPlatformNames(EPlatformInfoType::AllPlatformInfos); const int32 DefaultBlockRate = BlockRateOverride.GetDefault(); const float DefaultSampleRate = BlockRateOverride.GetDefault(); if (DefaultBlockRate > 0 && DefaultSampleRate > 0) { return false; } for (const FName Platform : Platforms) { if (BlockRateOverride.GetValueForPlatform(Platform) != DefaultBlockRate ) { return false; } if (!FMath::IsNearlyEqual(SampleRateOverride.GetValueForPlatform(Platform), DefaultSampleRate)) { return false; } } } return true; } EDataValidationResult UMetaSoundSource::IsDataValid(FDataValidationContext& Context) const { const EDataValidationResult Result = Metasound::Engine::FAssetHelper::IsDataValid(*this, RootMetasoundDocument, Context); return CombineDataValidationResults(Result, Super::IsDataValid(Context)); } void UMetaSoundSource::PostEditChangeOutputFormat() { using namespace Metasound::Engine; EMetaSoundBuilderResult Result = EMetaSoundBuilderResult::Failed; UMetaSoundSourceBuilder& SourceBuilder = FDocumentBuilderRegistry::GetChecked().FindOrBeginBuilding(*this); SourceBuilder.SetFormat(OutputFormat, Result); if (Result == EMetaSoundBuilderResult::Succeeded) { // Update the data in this UMetaSoundSource to reflect what is in the metasound document. ConformObjectToDocument(); // Use the editor form of register to ensure other editors' // MetaSounds are auto-updated if they are referencing this graph. if (EditorGraph) { EditorGraph->RegisterGraphWithFrontend(); } MarkPackageDirty(); } } void UMetaSoundSource::PostEditChangeQualitySettings() { // Refresh the SampleRate (which is what the engine sees from the operator settings). SampleRate = GetOperatorSettings(CachedAudioDeviceSampleRate).GetSampleRate(); // Always refresh the GUID with the selection. if (const UMetaSoundSettings* Settings = GetDefault()) { auto FindByName = [&Name = QualitySetting](const FMetaSoundQualitySettings& Q) -> bool { return Q.Name == Name; }; if (const FMetaSoundQualitySettings* Found = Settings->GetQualitySettings().FindByPredicate(FindByName)) { QualitySettingGuid = Found->UniqueId; } } } #endif // WITH_EDITOR bool UMetaSoundSource::ConformObjectToDocument() { using namespace Metasound::Engine; using namespace Metasound::Frontend; bool bDidAlterObjectData = false; // Update the OutputFormat and NumChannels to match the audio format interface // on the root document. const FOutputAudioFormatInfoMap& FormatInfo = GetOutputAudioFormatInfo(); for (const FOutputAudioFormatInfoPair& Pair : FormatInfo) { if (RootMetasoundDocument.Interfaces.Contains(Pair.Value.InterfaceVersion)) { if ((OutputFormat != Pair.Key) || (NumChannels != Pair.Value.OutputVertexChannelOrder.Num())) { OutputFormat = Pair.Key; NumChannels = Pair.Value.OutputVertexChannelOrder.Num(); bDidAlterObjectData = true; } break; } } return bDidAlterObjectData; } FTopLevelAssetPath UMetaSoundSource::GetAssetPathChecked() const { return Metasound::Engine::FAssetHelper::GetAssetPathChecked(*this); } void UMetaSoundSource::BeginDestroy() { OnNotifyBeginDestroy(); Super::BeginDestroy(); } void UMetaSoundSource::PreSave(FObjectPreSaveContext InSaveContext) { Super::PreSave(InSaveContext); Metasound::Engine::FAssetHelper::PreSaveAsset(*this, InSaveContext); } void UMetaSoundSource::Serialize(FArchive& InArchive) { using namespace Metasound::SourcePrivate; Super::Serialize(InArchive); Metasound::Engine::FAssetHelper::SerializeToArchive(*this, InArchive); // Load/Save cooked data. if (InArchive.IsCooking() || (FPlatformProperties::RequiresCookedData() && InArchive.IsLoading()) || IsCookedForEditor(InArchive, this)) { const FName PlatformName = InArchive.CookingTarget() ? *InArchive.CookingTarget()->IniPlatformName() : FName(FPlatformProperties::IniPlatformName()); SerializeCookedQualitySettings(PlatformName, InArchive); } } bool UMetaSoundSource::GetQualitySettings( const FName InPlatformName, Metasound::SourcePrivate::FCookedQualitySettings& OutQualitySettings) const { #if WITH_EDITORONLY_DATA // Query Project settings. if (const UMetaSoundSettings* Settings = GetDefault()) { if (const FMetaSoundQualitySettings* Found = Settings->GetQualitySettings().FindByPredicate([&QT = QualitySetting](const FMetaSoundQualitySettings& Q) -> bool { return Q.Name == QT; })) { // Allow partial applications of settings, if some are non-zero. if (const float Value = Found->BlockRate.GetValueForPlatform(InPlatformName); Value > 0.f) { UE_LOG(LogMetaSound, VeryVerbose, TEXT("Metasound [%s] using Quality '%s', BlockRate=%3.3f" ), *GetName(), *QualitySetting.ToString(), Value); OutQualitySettings.BlockRate = Value; } if (const int32 Value = Found->SampleRate.GetValueForPlatform(InPlatformName); Value > 0) { UE_LOG(LogMetaSound, VeryVerbose, TEXT("Metasound [%s] using Quality '%s', SampleRate=%d" ), *GetName(), *QualitySetting.ToString(), Value); OutQualitySettings.SampleRate = Value; } } } // Query overrides defined on this asset. if (const float SerializedBlockRate = BlockRateOverride.GetValueForPlatform(InPlatformName); SerializedBlockRate > 0.0f) { UE_LOG(LogMetaSound, VeryVerbose, TEXT("Metasound [%s] BlockRate Override: %3.3f"), *GetName(), SerializedBlockRate); OutQualitySettings.BlockRate = SerializedBlockRate; } if (const int32 SerializedSampleRate = SampleRateOverride.GetValueForPlatform(InPlatformName); SerializedSampleRate > 0) { UE_LOG(LogMetaSound, VeryVerbose, TEXT("Metasound [%s] SampleRate Override: %d"), *GetName(), SerializedSampleRate); OutQualitySettings.SampleRate = SerializedSampleRate; } // Success. UE_LOG(LogMetaSound, Verbose, TEXT("Metasound [%s] using SampleRate=%d, BlockRate=%2.3f (not-cooked)"), *GetName(), OutQualitySettings.SampleRate.GetValue(), OutQualitySettings.BlockRate.GetValue()) ; return true; #else //WITH_EDITORONLY_DATA // If we've been cooked, this should contain the quality settings. if (CookedQualitySettings) { if (const float SerializedBlockRate = CookedQualitySettings->BlockRate.GetValue(); SerializedBlockRate > 0.0f) { UE_LOG(LogMetaSound, VeryVerbose, TEXT("Metasound [%s] BlockRate: %3.3f"), *GetName(), SerializedBlockRate); OutQualitySettings.BlockRate = SerializedBlockRate; } if (const int32 SerializedSampleRate = CookedQualitySettings->SampleRate.GetValue(); SerializedSampleRate > 0) { UE_LOG(LogMetaSound, VeryVerbose, TEXT("Metasound [%s] SampleRate: %d"), *GetName(), SerializedSampleRate); OutQualitySettings.SampleRate = SerializedSampleRate; } UE_LOG(LogMetaSound, Verbose, TEXT("Metasound [%s] using SampleRate=%d, BlockRate=%2.3f (cooked)"), *GetName(), OutQualitySettings.SampleRate.GetValue(), OutQualitySettings.BlockRate.GetValue()) ; return true; } // Fail. return false; #endif //WITH_EDITORONLY_DATA } void UMetaSoundSource::SerializeCookedQualitySettings(const FName PlatformName, FArchive& Ar) { Metasound::SourcePrivate::FCookedQualitySettings Settings; if (Ar.IsSaving()) { GetQualitySettings(PlatformName, Settings); } // Use Struct Serializer. FMetaSoundQualitySettings::StaticStruct()->SerializeItem(Ar,&Settings,nullptr); if (Ar.IsLoading()) { CookedQualitySettings = MakePimpl(Settings); } } #if WITH_EDITOR void UMetaSoundSource::SetReferencedAssets(TSet&& InAssetRefs) { Metasound::Engine::FAssetHelper::SetReferencedAssets(*this, MoveTemp(InAssetRefs)); } #endif // WITH_EDITOR TArray UMetaSoundSource::GetReferencedAssets() { return Metasound::Engine::FAssetHelper::GetReferencedAssets(*this); } const TSet& UMetaSoundSource::GetAsyncReferencedAssetClassPaths() const { return ReferenceAssetClassCache; } void UMetaSoundSource::OnAsyncReferencedAssetsLoaded(const TArray& InAsyncReferences) { Metasound::Engine::FAssetHelper::OnAsyncReferencedAssetsLoaded(*this, InAsyncReferences); } #if WITH_EDITORONLY_DATA void UMetaSoundSource::MigrateEditorGraph(FMetaSoundFrontendDocumentBuilder& OutBuilder) { PRAGMA_DISABLE_DEPRECATION_WARNINGS if (Graph) { Graph->MigrateEditorDocumentData(OutBuilder); Graph = nullptr; } PRAGMA_ENABLE_DEPRECATION_WARNINGS } UEdGraph* UMetaSoundSource::GetGraph() const { return EditorGraph; } UEdGraph& UMetaSoundSource::GetGraphChecked() const { check(EditorGraph); return *EditorGraph; } FText UMetaSoundSource::GetDisplayName() const { FString TypeName = UMetaSoundSource::StaticClass()->GetName(); return FMetasoundAssetBase::GetDisplayName(MoveTemp(TypeName)); } #endif // WITH_EDITORONLY_DATA void UMetaSoundSource::PostLoad() { Super::PostLoad(); Metasound::Engine::FAssetHelper::PostLoad(*this); Duration = GetDuration(); bLooping = IsLooping(); PostLoadQualitySettings(); } void UMetaSoundSource::PostLoadQualitySettings() { #if WITH_EDITORONLY_DATA // Ensure that our Quality settings resolve. if (UMetaSoundSettings* Settings = GetMutableDefault()) { ResolveQualitySettings(Settings); // Register for any changes to the settings while we're open in the editor. Settings->OnSettingChanged().AddWeakLambda(this, [WeakSource = MakeWeakObjectPtr(this)](UObject* InObj, struct FPropertyChangedEvent& InEvent) { if ( WeakSource.IsValid() && InEvent.GetMemberPropertyName() == UMetaSoundSettings::GetQualitySettingPropertyName() ) { WeakSource->ResolveQualitySettings(CastChecked(InObj)); } }); // Register for changes from the CVars that control overrides. // We cache the OperatorSettings, so reset when these change. auto ResetOperatorSettings = [WeakSource = MakeWeakObjectPtr(this)](IConsoleVariable* Var) { if (WeakSource.IsValid()) { WeakSource->ResolveQualitySettings(GetMutableDefault()); // Override SampleRate with the Operator settings version which uses our Quality settings. WeakSource->SampleRate = WeakSource->GetOperatorSettings(WeakSource->CachedAudioDeviceSampleRate).GetSampleRate(); } }; Metasound::Frontend::GetBlockRateOverrideChangedDelegate().AddWeakLambda(this, ResetOperatorSettings); Metasound::Frontend::GetSampleRateOverrideChangedDelegate().AddWeakLambda(this, ResetOperatorSettings); } #endif //WITH_EDITORONLY_DATA } void UMetaSoundSource::ResolveQualitySettings(const UMetaSoundSettings* Settings) { #if WITH_EDITORONLY_DATA const FMetaSoundQualitySettings* Resolved = nullptr; // 1. Try and resolve by name. (most should resolve unless its been renamed, deleted). auto FindByName = [&Name = QualitySetting](const FMetaSoundQualitySettings& Q) -> bool { return Q.Name == Name; }; Resolved = Settings->GetQualitySettings().FindByPredicate(FindByName); // 2. If that failed, try by guid (if its been renamed in the settings, we can still find it). if (!Resolved && QualitySettingGuid.IsValid()) { auto FindByGuid = [&Guid = QualitySettingGuid](const FMetaSoundQualitySettings& Q) -> bool { return Q.UniqueId == Guid; }; Resolved = Settings->GetQualitySettings().FindByPredicate(FindByName); } // 3. If still failed to resolve, use defaults and warn. if (!Resolved) { // Disable the warning for now. //UE_LOG(LogMetaSound, Warning, TEXT("Failed to resolve Quality '%s', resetting to the default."), *QualitySetting.ToString()); // Reset to defaults. (and make sure they are sane) QualitySetting = GetDefault()->QualitySetting; QualitySettingGuid = GetDefault()->QualitySettingGuid; if (!Settings->GetQualitySettings().FindByPredicate(FindByName) && !Settings->GetQualitySettings().IsEmpty()) { // Default doesn't point to anything, use first one in the list. QualitySetting = Settings->GetQualitySettings()[0].Name; QualitySettingGuid = Settings->GetQualitySettings()[0].UniqueId; } } // Refresh the guid/name now we've resolved to correctly reflect. if (Resolved) { QualitySetting = Resolved->Name; QualitySettingGuid = Resolved->UniqueId; } #endif //WITH_EDITORONLY_DATA } void UMetaSoundSource::InitParameters(TArray& ParametersToInit, FName InFeatureName) { using namespace Metasound::SourcePrivate; METASOUND_LLM_SCOPE; METASOUND_TRACE_CPUPROFILER_EVENT_SCOPE(UMetaSoundSource::InitParameters); if (bIsBuilderActive) { // Do not create UObject proxies in the runtime input map because they proxies // stored there will not be used. The necessary proxies in the ParametersToInit // will be created and used instead. constexpr bool bCreateUObjectProxiesInRuntimeInputMap = false; InitParametersInternal(CreateRuntimeInputMap(bCreateUObjectProxiesInRuntimeInputMap), ParametersToInit, InFeatureName); } else { const bool bIsRuntimeInputDataValid = RuntimeInputData.bIsValid.load(); if (bIsRuntimeInputDataValid) { InitParametersInternal(RuntimeInputData.InputMap, ParametersToInit, InFeatureName); } else { // The runtime input data should have been cached, but is not so we use // a fallback method. If this is occurring, then callers need to ensure // that InitResources has been called before this method executes or else // suffer the consequences of incurring significant performance losses // each time a parameter is set on the MetaSound. UE_CLOG(HasNotBeenLoggedForThisObject(*this, __LINE__), LogMetaSound, Warning, TEXT("Initializing parameters on uninitialized UMetaSoundSource %s will result in slower performance. UMetaSoundSource::InitResources should finish executing on the game thread before attempting to call UMetaSoundSource::InitParameters(...)"), *GetOwningAssetName()); // Do not create UObject proxies in the runtime input map because they proxies // stored there will not be used. The necessary proxies in the ParametersToInit // will be created and used instead. constexpr bool bCreateUObjectProxiesInRuntimeInputMap = false; InitParametersInternal(CreateRuntimeInputMap(bCreateUObjectProxiesInRuntimeInputMap), ParametersToInit, InFeatureName); } } } void UMetaSoundSource::InitResources() { using namespace Metasound; using namespace Metasound::Engine; using namespace Metasound::Frontend; using namespace Metasound::SourcePrivate; METASOUND_LLM_SCOPE; METASOUND_TRACE_CPUPROFILER_EVENT_SCOPE(UMetaSoundSource::InitResources); ensureMsgf(Metasound::CanEverExecuteGraph(), TEXT("UMetaSoundSource::InitResources can only be called in applications where MetaSounds can execute.")); if (IsInGameThread()) { UpdateAndRegisterForExecution(GetInitRegistrationOptions()); } else { const bool bIsInGCSafeThread = IsInAudioThread() || IsInAsyncLoadingThread(); // Audio Thread is safe from GC, so we can safely construct the TWeakObjectPtr<> to this. if (!bIsInGCSafeThread) { UE_LOG(LogMetaSound, Warning, TEXT("Attempt to call UMetaSoundSource::InitResources() on %s from thread which may not provide garbage collection safety of the UMetaSoundSource"), *GetOwningAssetName()); } ExecuteOnGameThread( UE_SOURCE_LOCATION, [MetaSoundSourcePtr=TWeakObjectPtr(this)]() { if (UMetaSoundSource* Source = MetaSoundSourcePtr.Get()) { Source->InitResources(); } } ); } } void UMetaSoundSource::UpdateAndRegisterForExecution(Metasound::Frontend::FMetaSoundAssetRegistrationOptions InRegistrationOptions) { using namespace Metasound; using namespace Metasound::Frontend; check(IsInGameThread()); FMetasoundAssetBase::UpdateAndRegisterForExecution(InRegistrationOptions); const bool bIsRuntimeInputDataValid = RuntimeInputData.bIsValid.load(); // Runtime data does not need to and should not be created in builds that don't execute graphs if (!bIsRuntimeInputDataValid && Metasound::CanEverExecuteGraph()) { CacheRuntimeInputData(); } } bool UMetaSoundSource::IsPlayable() const { return true; } void UMetaSoundSource::GetAssetRegistryTags(FAssetRegistryTagsContext Context) const { Super::GetAssetRegistryTags(Context); Metasound::Engine::FAssetHelper::GetAssetRegistryTags(this, Context); } float UMetaSoundSource::GetDuration() const { // This is an unfortunate function required by logic in determining what sounds can be potentially // culled (in this case prematurally). MetaSound OneShots are stopped either by internally logic that // triggers OnFinished, or if an external system requests the sound to be stopped. Setting the duration // as a "close to" maximum length without being considered looping avoids the MetaSound from being // culled inappropriately. return IsOneShot() ? INDEFINITELY_LOOPING_DURATION - 1.0f : INDEFINITELY_LOOPING_DURATION; } Metasound::Frontend::FDocumentAccessPtr UMetaSoundSource::GetDocumentAccessPtr() { using namespace Metasound::Frontend; // Mutation of a document via the soft deprecated access ptr/controller system is not tracked by // the builder registry, so the document cache is invalidated here. if (IDocumentBuilderRegistry* BuilderRegistry = IDocumentBuilderRegistry::Get()) { BuilderRegistry->ReloadBuilder(RootMetasoundDocument.RootGraph.Metadata.GetClassName()); } // Return document using FAccessPoint to inform the TAccessPtr when the // object is no longer valid. return MakeAccessPtr(RootMetasoundDocument.AccessPoint, RootMetasoundDocument); } Metasound::Frontend::FConstDocumentAccessPtr UMetaSoundSource::GetDocumentConstAccessPtr() const { using namespace Metasound::Frontend; // Return document using FAccessPoint to inform the TAccessPtr when the // object is no longer valid. return MakeAccessPtr(RootMetasoundDocument.AccessPoint, RootMetasoundDocument); } bool UMetaSoundSource::ImplementsParameterInterface(Audio::FParameterInterfacePtr InInterface) const { const FMetasoundFrontendVersion Version { InInterface->GetName(), { InInterface->GetVersion().Major, InInterface->GetVersion().Minor } }; return GetConstDocument().Interfaces.Contains(Version); } ISoundGeneratorPtr UMetaSoundSource::CreateSoundGenerator(const FSoundGeneratorInitParams& InParams, TArray&& InDefaultParameters) { using namespace Metasound; using namespace Metasound::Frontend; using namespace Metasound::Engine; using namespace Metasound::SourcePrivate; METASOUND_LLM_SCOPE; METASOUND_TRACE_CPUPROFILER_EVENT_SCOPE(UMetaSoundSource::CreateSoundGenerator); FOperatorSettings InSettings = GetOperatorSettings(static_cast(InParams.SampleRate)); SampleRate = InSettings.GetSampleRate(); FMetasoundEnvironment Environment = CreateEnvironment(InParams); FParameterRouter& Router = UMetaSoundSource::GetParameterRouter(); TSharedPtr> DataChannel = Router.FindOrCreateDataChannelForReader(InParams.AudioDeviceID, InParams.InstanceID); if (InParams.StartTime > 0.0f) { if (IsInterfaceDeclared(SourceStartTimeInterface::GetVersion())) { InDefaultParameters.Add({ SourceStartTimeInterface::Inputs::StartTime, InParams.StartTime }); } else { const FString AssetName = GetName(); UE_LOG(LogMetaSound, Warning, TEXT("A non-zero StartTime (%.2f) was used in asset '%s' that doesn't implement the Start Time interface. StartTime will be ignored."), InParams.StartTime, *AssetName); } } FOperatorBuilderSettings BuilderSettings = FOperatorBuilderSettings::GetDefaultSettings(); // Graph analyzer currently only enabled for preview sounds (but can theoretically be supported for all sounds) BuilderSettings.bPopulateInternalDataReferences = InParams.bIsPreviewSound; constexpr bool bBuildSynchronous = false; const bool bIsDynamic = DynamicTransactor.IsValid(); TSharedPtr Generator; if (bIsDynamic) { // In order to ensure synchronization and avoid race conditions the current state // of the graph is copied and transform queue created here. This ensures that: // // 1. Modifications to the underlying FGraph in the FDynamicOperatorTransactor can continue // while the generator is being constructed on an async task. If this were not ensured, // a race condition would be introduced wherein the FGraph could be manipulated while the // graph is being read while building the generator. // // 2. The state of the FGraph and TransformQueue are synchronized so that any additional // changes applied to the FDynamicOperatorTransactor will be placed in the TransformQueue. // The dynamic operator & generator will then consume these transforms after it has finished // being built. BuilderSettings.bEnableOperatorRebind = true; TSharedRef GraphRenderCost = FGraphRenderCost::MakeGraphRenderCost(); FMetasoundDynamicGraphGeneratorInitParams InitParams { { InSettings, MoveTemp(BuilderSettings), MakeShared(DynamicTransactor->GetGraph()), // Make a copy of the graph. Environment, GetName(), GetOutputAudioChannelOrder(), MoveTemp(InDefaultParameters), bBuildSynchronous, DataChannel, GraphRenderCost, FName(GetClass()->GetName()), GetAssetPathChecked().GetPackageName() }, DynamicTransactor->CreateTransformQueue(InSettings, Environment, GraphRenderCost) // Create transaction queue }; TSharedPtr DynamicGenerator = MakeShared(InSettings); DynamicGenerator->Init(MoveTemp(InitParams)); Generator = MoveTemp(DynamicGenerator); } else { // By default, the sound generator for a metasound preset uses a graph specifically // associated with the UMetaSoundSource_Preset. The overridden defaults for that // preset are baked into the IGraph. Unfortunately, this makes the MetaSound // operator pool less efficient because it associates the operator with the IGraph. // The way the presets use the IGraph mean that there is less sharing of cached // operators. // // To improve the efficiency of the operator pool, we have presets use their // base IGraphs so that more MetaSounds utilize the same IGraph. This requires // us to retrieve that specific graph. We also supply the parameters that were overridden // in the preset to the FMetaSoundGenerator, because they are not backed into // the base IGraph. FAudioParameterCollector AudioParameterCollector(InDefaultParameters); TSharedPtr MetasoundGraph = FindFirstNoninflatableGraph(AudioParameterCollector, Environment); if (MetasoundGraph.IsValid()) { // Create generator. FMetasoundGeneratorInitParams InitParams { InSettings, MoveTemp(BuilderSettings), MetasoundGraph, Environment, GetName(), GetOutputAudioChannelOrder(), MoveTemp(InDefaultParameters), bBuildSynchronous, DataChannel, {}, // RelativeRenderCost FName(GetClass()->GetName()), GetAssetPathChecked().GetPackageName() }; Generator = MakeShared(MoveTemp(InitParams)); } } if (Generator.IsValid()) { FGeneratorInstanceInfo Info = FGeneratorInstanceInfo(InParams.AudioComponentId, InParams.InstanceID, Generator); TrackGenerator(MoveTemp(Info)); } return ISoundGeneratorPtr(Generator); } void UMetaSoundSource::OnEndGenerate(ISoundGeneratorPtr Generator) { using namespace Metasound; ForgetGenerator(Generator); } bool UMetaSoundSource::GetAllDefaultParameters(TArray& OutParameters) const { using namespace Metasound; using namespace Metasound::Frontend; using namespace Metasound::Engine; if (Metasound::CanEverExecuteGraph()) { // If we are in an executable context, we expect runtime input data to be // cached as default parameters may be requested many times. if(!RuntimeInputData.bIsValid.load()) { UE_CLOG(SourcePrivate::HasNotBeenLoggedForThisObject(*this, __LINE__), LogMetaSound, Warning, TEXT("Default parameters will be ommitted. Accessing invalid runtime data on MetaSound %s. Ensure that UMetaSoundSource::InitResources() is executed on the game thread before calling UMetaSoundSource::GetAllDefaultParameters(...)"), *GetOwningAssetName()); return false; } for(const TPair& Pair : RuntimeInputData.InputMap) { OutParameters.Add(Pair.Value.DefaultParameter); } return true; } else { // Non executable contexts do not have cached runtime data and need their // parameters created each time they are requested. constexpr bool bCreateUObjectProxies = false; TSortedVertexNameMap InputMap = CreateRuntimeInputMap(bCreateUObjectProxies); for(const TPair& Pair : InputMap) { OutParameters.Add(Pair.Value.DefaultParameter); } return true; } } void UMetaSoundSource::InitParametersInternal(const Metasound::TSortedVertexNameMap& InInputMap, TArray& ParametersToInit, FName InFeatureName) const { using namespace Metasound; using namespace Metasound::Frontend; METASOUND_LLM_SCOPE; METASOUND_TRACE_CPUPROFILER_EVENT_SCOPE(UMetaSoundSource::InitParametersInternal); checkf(IsInGameThread() || IsInAudioThread(), TEXT("Parameter initialization must happen on the GameThread or AudioThread to allow for safe creation of UObject proxies")); IDataTypeRegistry& DataTypeRegistry = IDataTypeRegistry::Get(); // Removes values that are not explicitly defined by the ParamType auto Sanitize = [](FAudioParameter& Parameter) { switch (Parameter.ParamType) { case EAudioParameterType::Trigger: { Parameter = FAudioParameter(Parameter.ParamName, EAudioParameterType::Trigger); } break; case EAudioParameterType::Boolean: { Parameter = FAudioParameter(Parameter.ParamName, Parameter.BoolParam); } break; case EAudioParameterType::BooleanArray: { TArray TempArray = Parameter.ArrayBoolParam; Parameter = FAudioParameter(Parameter.ParamName, MoveTemp(TempArray)); } break; case EAudioParameterType::Float: { Parameter = FAudioParameter(Parameter.ParamName, Parameter.FloatParam); } break; case EAudioParameterType::FloatArray: { TArray TempArray = Parameter.ArrayFloatParam; Parameter = FAudioParameter(Parameter.ParamName, MoveTemp(TempArray)); } break; case EAudioParameterType::Integer: { Parameter = FAudioParameter(Parameter.ParamName, Parameter.IntParam); } break; case EAudioParameterType::IntegerArray: { TArray TempArray = Parameter.ArrayIntParam; Parameter = FAudioParameter(Parameter.ParamName, MoveTemp(TempArray)); } break; case EAudioParameterType::Object: { Parameter = FAudioParameter(Parameter.ParamName, Parameter.ObjectParam); } break; case EAudioParameterType::ObjectArray: { TArray TempArray = Parameter.ArrayObjectParam; Parameter = FAudioParameter(Parameter.ParamName, MoveTemp(TempArray)); } break; case EAudioParameterType::String: { Parameter = FAudioParameter(Parameter.ParamName, Parameter.StringParam); } break; case EAudioParameterType::StringArray: { TArray TempArray = Parameter.ArrayStringParam; Parameter = FAudioParameter(Parameter.ParamName, MoveTemp(TempArray)); } break; case EAudioParameterType::None: default: break; } }; for (int32 i = ParametersToInit.Num() - 1; i >= 0; --i) { bool bIsParameterValid = false; FAudioParameter& Parameter = ParametersToInit[i]; if (const FRuntimeInput* Input = InInputMap.Find(Parameter.ParamName)) { if (IsParameterValidInternal(Parameter, Input->TypeName, DataTypeRegistry)) { bIsParameterValid = true; // note: this was originally called 2x, the 1st call would create the proxy // and clear out the ObjectParam. The 2nd call would clear out the proxy, // and fail to re-create it because we cleared out the ObjectParam. // if there is already a proxy, don't attempt to recreate them. if (Parameter.ObjectProxies.IsEmpty()) { Sanitize(Parameter); constexpr bool bClearUObjectPointers = true; // protect against leaking UObject ptrs to the audio thread SourcePrivate::CreateUObjectProxies(DataTypeRegistry, Input->TypeName, bClearUObjectPointers, Parameter); } } } if (!bIsParameterValid) { ParametersToInit.RemoveAtSwap(i, EAllowShrinking::No); #if !NO_LOGGING if (::Metasound::MetaSoundParameterEnableWarningOnIgnoredParameterCVar) { const FString AssetName = GetName(); UE_LOG(LogMetaSound, Warning, TEXT("Failed to set parameter '%s' in asset '%s': No name specified, no transmittable input found, or type mismatch."), *Parameter.ParamName.ToString(), *AssetName); } #endif // !NO_LOGGING } } } bool UMetaSoundSource::IsParameterValid(const FAudioParameter& InParameter) const { const TArray& Inputs = GetConstDocument().RootGraph.GetDefaultInterface().Inputs; const FMetasoundFrontendVertex* Vertex = Algo::FindByPredicate(Inputs, [&InParameter] (const FMetasoundFrontendClassInput& Input) { return Input.Name == InParameter.ParamName; }); if (Vertex) { return IsParameterValidInternal(InParameter, Vertex->TypeName, Metasound::Frontend::IDataTypeRegistry::Get()); } else { return false; } } bool UMetaSoundSource::IsParameterValidInternal(const FAudioParameter& InParameter, const FName& InTypeName, Metasound::Frontend::IDataTypeRegistry& InDataTypeRegistry) const { using namespace Metasound; using namespace Metasound::Frontend; if (InParameter.ParamName.IsNone()) { // Invalid parameter name return false; } if (!InParameter.TypeName.IsNone() && InParameter.TypeName != InTypeName) { // Mismatched parameter type and vertex data type return false; } // Special handling for UObject proxies if (InParameter.ParamType == EAudioParameterType::Object) { return InDataTypeRegistry.IsValidUObjectForDataType(InTypeName, InParameter.ObjectParam); } else if (InParameter.ParamType == EAudioParameterType::ObjectArray) { bool bIsValid = true; const FName ElementTypeName = CreateElementTypeNameFromArrayTypeName(InTypeName); for (const UObject* Object : InParameter.ArrayObjectParam) { bIsValid = InDataTypeRegistry.IsValidUObjectForDataType(ElementTypeName, Object); if (!bIsValid) { break; } } return bIsValid; } const IDataTypeRegistryEntry* RegistryEntry = InDataTypeRegistry.FindDataTypeRegistryEntry(InTypeName); if (!RegistryEntry) { // Unregistered MetaSound data type return false; } switch (InParameter.ParamType) { case EAudioParameterType::Trigger: case EAudioParameterType::Boolean: { return RegistryEntry->GetDataTypeInfo().bIsBoolParsable; } case EAudioParameterType::BooleanArray: { return RegistryEntry->GetDataTypeInfo().bIsBoolArrayParsable; } case EAudioParameterType::Float: { return RegistryEntry->GetDataTypeInfo().bIsFloatParsable; } case EAudioParameterType::FloatArray: { return RegistryEntry->GetDataTypeInfo().bIsFloatArrayParsable; } case EAudioParameterType::Integer: { return RegistryEntry->GetDataTypeInfo().bIsIntParsable; } case EAudioParameterType::IntegerArray: { return RegistryEntry->GetDataTypeInfo().bIsIntArrayParsable; } case EAudioParameterType::String: { return RegistryEntry->GetDataTypeInfo().bIsStringParsable; } case EAudioParameterType::StringArray: { return RegistryEntry->GetDataTypeInfo().bIsStringArrayParsable; } case EAudioParameterType::NoneArray: { return RegistryEntry->GetDataTypeInfo().bIsDefaultArrayParsable; } case EAudioParameterType::None: { return RegistryEntry->GetDataTypeInfo().bIsDefaultParsable; } default: { // All parameter types should be covered. static_assert(static_cast(EAudioParameterType::COUNT) == 13, "Possible unhandled EAudioParameterType"); checkNoEntry(); // Unhandled parameter type return false; } } } bool UMetaSoundSource::IsLooping() const { return !IsOneShot(); } bool UMetaSoundSource::IsOneShot() const { using namespace Metasound::Frontend; // If the metasound source implements the one-shot interface, then it's a one-shot metasound return IsInterfaceDeclared(SourceOneShotInterface::GetVersion()); } TSharedPtr UMetaSoundSource::CreateParameterTransmitter(Audio::FParameterTransmitterInitParams&& InParams) const { using namespace Metasound; using namespace Metasound::SourcePrivate; METASOUND_LLM_SCOPE; auto CreateParameterTransmitterInternal = [this](const TSortedVertexNameMap& InInputMap, Audio::FParameterTransmitterInitParams& InParams) { // Build list of parameters that can be set at runtime. TArray ValidParameters; for (const TPair& Pair : InInputMap) { if (Pair.Value.bIsTransmittable && (Pair.Value.AccessType == EMetasoundFrontendVertexAccessType::Reference)) { ValidParameters.Add(Pair.Value.Name); } } FParameterRouter& Router = UMetaSoundSource::GetParameterRouter(); TSharedPtr> DataChannel = Router.FindOrCreateDataChannelForWriter(InParams.AudioDeviceID, InParams.InstanceID); Metasound::FMetaSoundParameterTransmitter::FInitParams InitParams ( GetOperatorSettings(InParams.SampleRate), InParams.InstanceID, MoveTemp(InParams.DefaultParams), MoveTemp(ValidParameters), DataChannel ); InitParams.DebugMetaSoundName = this->GetFName(); return MakeShared(MoveTemp(InitParams)); }; const bool bIsRuntimeInputDataValid = RuntimeInputData.bIsValid.load(); const bool bCreateInputMapOnTheFly = bIsBuilderActive || !bIsRuntimeInputDataValid; if (bCreateInputMapOnTheFly) { if (!bIsBuilderActive) { // If we're not using a builder, that means the metasound cannot change and that the runtime input data should have been cached. UE_LOG(LogMetaSound, Warning, TEXT("Creating a Parameter Transmiiter on uninitialized UMetaSoundSource %s will result in slower performance. UMetaSoundSource::InitResources should finish executing on the game thread before attempting to call UMetaSoundSource::CreateParameterTransmitter(...)"), *GetOwningAssetName()); } // Do not create UObject proxies in the runtime input map because they proxies // stored there will not be used. The necessary proxies in the ParametersToInit // will be created and used instead. constexpr bool bCreateUObjectProxiesInRuntimeInputMap = false; return CreateParameterTransmitterInternal(CreateRuntimeInputMap(bCreateUObjectProxiesInRuntimeInputMap), InParams); } else { return CreateParameterTransmitterInternal(RuntimeInputData.InputMap, InParams); } } Metasound::FOperatorSettings UMetaSoundSource::GetOperatorSettings(Metasound::FSampleRate InDeviceSampleRate) const { using namespace Metasound; using namespace Metasound::SourcePrivate; // Default sensibly. FCookedQualitySettings Settings; Settings.BlockRate = DefaultBlockRateConstant; Settings.SampleRate = InDeviceSampleRate > 0 ? InDeviceSampleRate : DefaultSampleRateConstant; // Fetch our quality settings. // If we are cooked these are baked, if we are editor these are queried from the project settings and this assets overrides. const bool bFoundQualitySettings = GetQualitySettings(FPlatformProperties::IniPlatformName(), Settings); UE_CLOG(!bFoundQualitySettings && IsAsset(), LogMetaSound, Error, TEXT("Could not retrieve quality settings for asset %s"), *GetOwningAssetName()); // Query CVars. (Override with CVars if they are > 0) using namespace Metasound::Frontend; const float BlockRateCVar = GetBlockRateOverride(); const int32 SampleRateCvar = GetSampleRateOverride(); if (SampleRateCvar > 0) { Settings.SampleRate = SampleRateCvar; } if (BlockRateCVar > 0) { Settings.BlockRate = BlockRateCVar; } // Sanity clamps. const TRange BlockRange = GetBlockRateClampRange(); const TRange RateRange = GetSampleRateClampRange(); Settings.BlockRate = FMath::Clamp(Settings.BlockRate.GetValue(), BlockRange.GetLowerBoundValue(), BlockRange.GetUpperBoundValue()); Settings.SampleRate = FMath::Clamp(Settings.SampleRate.GetValue(), RateRange.GetLowerBoundValue(), RateRange.GetUpperBoundValue()); UE_LOG(LogMetaSound, Verbose, TEXT("Metasound [%s] GetOperatorSettings: SampleRate: %d, BlockRate: %3.3f"), *GetName(), Settings.SampleRate.GetValue(), Settings.BlockRate.GetValue()); return Metasound::FOperatorSettings( /* SampleRate */ Settings.SampleRate.GetValue(), /* BlockRate */ Settings.BlockRate.GetValue()); } Metasound::FMetasoundEnvironment UMetaSoundSource::CreateEnvironment() const { using namespace Metasound; using namespace Metasound::Frontend; FMetasoundEnvironment Environment; Environment.SetValue(SourceInterface::Environment::SoundUniqueID, GetUniqueID()); return Environment; } Metasound::FMetasoundEnvironment UMetaSoundSource::CreateEnvironment(const FSoundGeneratorInitParams& InParams) const { using namespace Metasound; using namespace Metasound::Engine; using namespace Metasound::Frontend; FMetasoundEnvironment Environment = CreateEnvironment(); Environment.SetValue(SourceInterface::Environment::IsPreview, InParams.bIsPreviewSound); Environment.SetValue(CoreInterface::Environment::InstanceID, InParams.InstanceID); Environment.SetValue(SourceInterface::Environment::TransmitterID, InParams.InstanceID); Environment.SetValue(SourceInterface::Environment::DeviceID, InParams.AudioDeviceID); Environment.SetValue(SourceInterface::Environment::AudioMixerNumOutputFrames, InParams.AudioMixerNumOutputFrames); Environment.SetValue(SourceInterface::Environment::AudioMixerSampleRate, InParams.SampleRate); #if WITH_METASOUND_DEBUG_ENVIRONMENT Environment.SetValue(SourceInterface::Environment::GraphName, GetFullName()); #endif // WITH_METASOUND_DEBUG_ENVIRONMENT return Environment; } Metasound::FMetasoundEnvironment UMetaSoundSource::CreateEnvironment(const Audio::FParameterTransmitterInitParams& InParams) const { using namespace Metasound; using namespace Metasound::Engine; using namespace Metasound::Frontend; FMetasoundEnvironment Environment = CreateEnvironment(); Environment.SetValue(CoreInterface::Environment::InstanceID, InParams.InstanceID); return Environment; } const TArray& UMetaSoundSource::GetOutputAudioChannelOrder() const { using namespace Metasound::Engine; using namespace Metasound::Frontend; if (const FOutputAudioFormatInfo* FormatInfo = GetOutputAudioFormatInfo().Find(OutputFormat)) { return FormatInfo->OutputVertexChannelOrder; } else { // Unhandled audio format. Need to update audio output format vertex key map. checkNoEntry(); static const TArray Empty; return Empty; } } void UMetaSoundSource::TrackGenerator(Metasound::FGeneratorInstanceInfo&& GeneratorInfo) { FScopeLock GeneratorMapLock(&GeneratorMapCriticalSection); Generators.Add(MoveTemp(GeneratorInfo)); PRAGMA_DISABLE_DEPRECATION_WARNINGS OnGeneratorInstanceCreated.Broadcast(Generators.Last().AudioComponentID, Generators.Last().Generator.Pin()); PRAGMA_ENABLE_DEPRECATION_WARNINGS OnGeneratorInstanceInfoCreated.Broadcast(Generators.Last()); } void UMetaSoundSource::ForgetGenerator(ISoundGeneratorPtr Generator) { using namespace Metasound; FMetasoundGenerator* AsMetasoundGenerator = static_cast(Generator.Get()); FScopeLock GeneratorMapLock(&GeneratorMapCriticalSection); const uint32 Index = Generators.IndexOfByPredicate([AsMetasoundGenerator](const FGeneratorInstanceInfo& GeneratorInfo) { return GeneratorInfo.Generator.HasSameObject(AsMetasoundGenerator); }); if (Index != INDEX_NONE) { PRAGMA_DISABLE_DEPRECATION_WARNINGS OnGeneratorInstanceDestroyed.Broadcast(Generators[Index].AudioComponentID, Generators[Index].Generator.Pin()); PRAGMA_ENABLE_DEPRECATION_WARNINGS OnGeneratorInstanceInfoDestroyed.Broadcast(Generators[Index]); Generators.RemoveAtSwap(Index); } } TWeakPtr UMetaSoundSource::GetGeneratorForAudioComponent(uint64 ComponentId) const { using namespace Metasound; check(ComponentId != INDEX_NONE) FScopeLock GeneratorMapLock(&GeneratorMapCriticalSection); const FGeneratorInstanceInfo* GeneratorInfo = Generators.FindByPredicate([ComponentId](const FGeneratorInstanceInfo& Info) { return Info.AudioComponentID == ComponentId; }); if (GeneratorInfo != nullptr) { return GeneratorInfo->Generator; } return TWeakPtr(nullptr); } TWeakPtr UMetaSoundSource::GetGeneratorForInstanceID(uint64 InstanceId) const { using namespace Metasound; FScopeLock GeneratorMapLock(&GeneratorMapCriticalSection); const FGeneratorInstanceInfo* GeneratorInfo = Generators.FindByPredicate([InstanceId](const FGeneratorInstanceInfo& Info) { return Info.InstanceID == InstanceId; }); if (GeneratorInfo != nullptr) { return GeneratorInfo->Generator; } return TWeakPtr(nullptr); } bool UMetaSoundSource::IsDynamic() const { return DynamicTransactor.IsValid(); } Metasound::SourcePrivate::FParameterRouter& UMetaSoundSource::GetParameterRouter() { using namespace Metasound::SourcePrivate; static FParameterRouter Router; return Router; } bool UMetaSoundSource::IsActivelyBuilding() const { return bIsBuilderActive; } void UMetaSoundSource::OnBeginActiveBuilder() { using namespace Metasound::Frontend; if (bIsBuilderActive) { UE_LOG(LogMetaSound, Error, TEXT("OnBeginActiveBuilder() call while prior builder is still active. This may indicate that multiple builders are attempting to modify the MetaSound %s concurrently."), *GetOwningAssetName()) } // If a builder is activating, make sure any in-flight registration // tasks have completed. Async registration tasks use the FMetasoundFrontendDocument // that lives on this object. We need to make sure that registration task // completes so that the FMetasoundFrontendDocument does not get modified // by a builder while it is also being read by async registration. const FGraphRegistryKey GraphKey = GetGraphRegistryKey(); if (GraphKey.IsValid()) { FMetasoundFrontendRegistryContainer::Get()->WaitForAsyncGraphRegistration(GraphKey); } bIsBuilderActive = true; // Currently we do not have information on whether inputs were added or removed // from the document. We invalidate the cached runtime inputs just in case. // MetaSounds which have an active builder should not be using cached runtime // input data until the builder is no longer active. InvalidateCachedRuntimeInputData(); } void UMetaSoundSource::OnFinishActiveBuilder() { bIsBuilderActive = false; } TSharedPtr UMetaSoundSource::SetDynamicGeneratorEnabled(bool bInIsEnabled) { using namespace Metasound; using namespace Metasound::DynamicGraph; if (bInIsEnabled) { if (!DynamicTransactor.IsValid()) { // If a FGraph exists for this UMetaSoundSource, then we need to initialize // the DynamicTransactor with the existing FGraph so it has the correct // initial state. // // Currently, any existing FGraph will be stored in the node registry, // hence we check if the graph is registered and retrieve the current // graph to see if any FGraph already exists. if (IsRegistered()) { TSharedPtr CurrentGraph = FMetasoundFrontendRegistryContainer::Get()->GetGraph(GetGraphRegistryKey()); if (CurrentGraph.IsValid()) { DynamicTransactor = MakeShared(*CurrentGraph); } else { UE_LOG(LogMetaSound, Warning, TEXT("Failed to get existing graph for dynamic metasound %s. Initializing to empty graph."), *GetOwningAssetName()); DynamicTransactor = MakeShared(); } } else { DynamicTransactor = MakeShared(); } } } else { DynamicTransactor.Reset(); } return DynamicTransactor; } TSharedPtr UMetaSoundSource::GetDynamicGeneratorTransactor() const { return DynamicTransactor; } UMetaSoundSource::FRuntimeInput UMetaSoundSource::CreateRuntimeInput(const Metasound::Frontend::IDataTypeRegistry& Registry, const FMetasoundFrontendClassInput& Input, bool bCreateUObjectProxies) { using namespace Metasound; using namespace Metasound::Frontend; bool bIsTransmittable = false; if (const IDataTypeRegistryEntry* RegistryEntry = Registry.FindDataTypeRegistryEntry(Input.TypeName)) { bIsTransmittable = RegistryEntry->GetDataTypeInfo().bIsTransmittable; } else { UE_LOG(LogMetaSound, Warning, TEXT("Failed to find data type '%s' in registry. Assuming data type is not transmittable"), *Input.TypeName.ToString()); } const FGuid PageID = Engine::FDocumentBuilderRegistry::GetChecked().ResolveTargetPageID(Input); const FMetasoundFrontendLiteral& DefaultLiteral = Input.FindConstDefaultChecked(PageID); FAudioParameter DefaultParameter = SourcePrivate::MakeAudioParameter(Registry, Input.Name, Input.TypeName, DefaultLiteral, bCreateUObjectProxies); return FRuntimeInput { Input.Name, Input.TypeName, Input.AccessType, DefaultParameter, bIsTransmittable }; } Metasound::TSortedVertexNameMap UMetaSoundSource::CreateRuntimeInputMap(bool bCreateUObjectProxies) const { using namespace Metasound; using namespace Metasound::Frontend; METASOUND_TRACE_CPUPROFILER_EVENT_SCOPE(UMetaSoundSource::CreateRuntimeInputMap); auto GetInputName = [](const FMetasoundFrontendClassInput& InInput) { return InInput.Name; }; const IDataTypeRegistry& Registry = IDataTypeRegistry::Get(); const FMetasoundFrontendDocument& Doc = GetConstDocument(); TArray Interfaces; FMetaSoundFrontendDocumentBuilder::FindDeclaredInterfaces(Doc, Interfaces); // Inputs which are controlled by an interface are private unless // their router name is `Audio::IParameterTransmitter::RouterName` TSet PrivateInputs; for (const IInterfaceRegistryEntry* InterfaceEntry : Interfaces) { if (InterfaceEntry) { if (InterfaceEntry->GetRouterName() != Audio::IParameterTransmitter::RouterName) { const FMetasoundFrontendInterface& Interface = InterfaceEntry->GetInterface(); Algo::Transform(Interface.Inputs, PrivateInputs, GetInputName); } } } // Cache all inputs which are not private inputs. TSortedVertexNameMap PublicInputs; for (const FMetasoundFrontendClassInput& Input : Doc.RootGraph.GetDefaultInterface().Inputs) { if (!PrivateInputs.Contains(Input.Name)) { PublicInputs.Add(Input.Name, CreateRuntimeInput(Registry, Input, bCreateUObjectProxies)); } } // Add the parameter pack input that ALL Metasounds have FMetasoundFrontendClassInput ParameterPackInput = UMetasoundParameterPack::GetClassInput(); const FMetasoundFrontendLiteral& DefaultLiteral = ParameterPackInput.FindConstDefaultChecked(Frontend::DefaultPageID); FAudioParameter ParameterPackDefaultParameter = SourcePrivate::MakeAudioParameter(Registry, ParameterPackInput.Name, ParameterPackInput.TypeName, DefaultLiteral, bCreateUObjectProxies) ; PublicInputs.Add(ParameterPackInput.Name, FRuntimeInput{ParameterPackInput.Name, ParameterPackInput.TypeName, ParameterPackInput.AccessType, ParameterPackDefaultParameter, true /* bIsTransmittable */}); return PublicInputs; } void UMetaSoundSource::CacheRuntimeInputData() { using namespace Metasound; constexpr bool bCreateUObjectProxies = true; RuntimeInputData.InputMap = CreateRuntimeInputMap(bCreateUObjectProxies); // Determine if preset graph inflation is possible // // Constructor inputs conflict with `Preset Graph Inflation` and `Operator Caching`. // This logic protects against attempting to use preset graph inflation when the preset // graph has overridden constructor pins. // // Operator caching of base preset graphs fail when there are constructor inputs because // constructor inputs set on the preset cannot be updated after the base operator is // cached. auto IsOverriddenConstructorInput = [&InputsInheritingDefault=RootMetasoundDocument.RootGraph.PresetOptions.InputsInheritingDefault](const TPair& Pair) { if (Pair.Value.AccessType == EMetasoundFrontendVertexAccessType::Value) { return !InputsInheritingDefault.Contains(Pair.Key); } return false; }; bIsPresetGraphInflationSupported = RootMetasoundDocument.RootGraph.PresetOptions.bIsPreset && !Algo::AnyOf(RuntimeInputData.InputMap, IsOverriddenConstructorInput); RuntimeInputData.bIsValid.store(true); } void UMetaSoundSource::InvalidateCachedRuntimeInputData() { bIsPresetGraphInflationSupported = false; RuntimeInputData.bIsValid.store(false); } const UMetaSoundSource& UMetaSoundSource::FindFirstNoninflatableSource(Metasound::FMetasoundEnvironment& InOutEnvironment, TFunctionRef OnTraversal) const { TArray GraphHierarchy; const UMetaSoundSource& FoundSource = FindFirstNoninflatableSourceInternal(GraphHierarchy, OnTraversal); if (GraphHierarchy.Num()) { // Preset graph inflation needs to emulate the graph hierarchy for nodes which depend upon accurate graph hierarchies. InOutEnvironment.SetValue>(Metasound::CoreInterface::Environment::GraphHierarchy, MoveTemp(GraphHierarchy)); } return FoundSource; } const UMetaSoundSource& UMetaSoundSource::FindFirstNoninflatableSourceInternal(TArray& OutHierarchy, TFunctionRef OnTraversal) const { using namespace Metasound; using namespace Metasound::Frontend; const bool bIsDynamic = DynamicTransactor.IsValid(); if (!bIsDynamic && ConsoleVariables::bEnableExperimentalRuntimePresetGraphInflation && bIsPresetGraphInflationSupported) { if (ReferencedAssetClassObjects.Num() == 1) { // Get first element from TSet<> TObjectPtr BaseGraph = *ReferencedAssetClassObjects.CreateConstIterator(); // Get the reference graph as a UMetaSoundSource TObjectPtr BaseMetaSoundSource = Cast(BaseGraph); if (ensure(BaseMetaSoundSource)) // SourcePresets assume they are referencing a UMetaSoundSource { // Preset graph inflation needs to emulate the graph hierarchy for nodes which depend upon accurate graph hierarchies. FGuid ClassID; ensureAlways(IMetaSoundAssetManager::GetChecked().TryGetAssetIDFromClassName(RootMetasoundDocument.RootGraph.Metadata.GetClassName(), ClassID)); OutHierarchy.Add(ClassID); OnTraversal(*this); // If the base metasound is also a preset that can be inflate, recurse into it. return BaseMetaSoundSource->FindFirstNoninflatableSourceInternal(OutHierarchy, OnTraversal); } } else { UE_LOG(LogMetaSound, Warning, TEXT("Attempt to reference parent of metasound preset failed due to unexpected number of reference asses (%d) from MetaSound Preset %s"), ReferencedAssetClassObjects.Num(), *GetOwningAssetName()); } } return *this; } TSharedPtr UMetaSoundSource::FindFirstNoninflatableGraph(UMetaSoundSource::FAudioParameterCollector& InOutParameterCollector, Metasound::FMetasoundEnvironment& InOutEnvironment) const { using namespace Metasound; auto OnGraphInflation = [&InOutParameterCollector](const UMetaSoundSource& InInflatedSource) { // Any preset overrides on this object need to be baked in to the parameters InOutParameterCollector.CollectPresetOverrides(InInflatedSource.RootMetasoundDocument.RootGraph.PresetOptions.InputsInheritingDefault, InInflatedSource.RuntimeInputData.InputMap); }; const UMetaSoundSource& NoninflatableSource = FindFirstNoninflatableSource(InOutEnvironment, OnGraphInflation); return FMetasoundFrontendRegistryContainer::Get()->GetGraph(NoninflatableSource.GetGraphRegistryKey()); } #undef LOCTEXT_NAMESPACE // MetaSound