Files
UnrealEngine/Engine/Plugins/Runtime/Metasound/Source/MetasoundEngine/Private/MetasoundDocumentBuilderRegistry.cpp
2025-05-18 13:04:45 +08:00

608 lines
21 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MetasoundDocumentBuilderRegistry.h"
#include "HAL/PlatformProperties.h"
#include "MetasoundAssetManager.h"
#include "MetasoundGlobals.h"
#include "MetasoundSettings.h"
#include "MetasoundTrace.h"
#include "MetasoundUObjectRegistry.h"
namespace Metasound::Engine
{
FDocumentBuilderRegistry::~FDocumentBuilderRegistry()
{
TMultiMap<FMetasoundFrontendClassName, TWeakObjectPtr<UMetaSoundBuilderBase>> BuildersToFinish;
FScopeLock Lock(&BuildersCriticalSection);
{
BuildersToFinish = MoveTemp(Builders);
Builders.Reset();
}
UE_CLOG(!BuildersToFinish.IsEmpty(), LogMetaSound, Display, TEXT("BuilderRegistry is shutting down with the following %i active builder entries. Forcefully shutting down:"), BuildersToFinish.Num());
int32 NumStale = 0;
for (const TPair<FMetasoundFrontendClassName, TWeakObjectPtr<UMetaSoundBuilderBase>>& Pair : BuildersToFinish)
{
const bool bIsValid = Pair.Value.IsValid();
if (bIsValid)
{
UE_CLOG(bIsValid, LogMetaSound, Display, TEXT("- %s"), *Pair.Value->GetFullName());
constexpr bool bForceUnregister = true;
FinishBuildingInternal(*Pair.Value.Get(), bForceUnregister);
}
else
{
++NumStale;
}
}
UE_CLOG(NumStale > 0, LogMetaSound, Display, TEXT("BuilderRegistry is shutting down with %i stale entries"), NumStale);
}
void FDocumentBuilderRegistry::AddBuilderInternal(const FMetasoundFrontendClassName& InClassName, UMetaSoundBuilderBase* NewBuilder) const
{
FScopeLock Lock(&BuildersCriticalSection);
// #if !NO_LOGGING
// bool bLogDuplicateEntries = CanPostEventLog(ELogEvent::DuplicateEntries, ELogVerbosity::Error);
// if (bLogDuplicateEntries)
// {
// bLogDuplicateEntries = Builders.Contains(InClassName);
// }
// #endif // !NO_LOGGING
Builders.Add(InClassName, NewBuilder);
// #if !NO_LOGGING
// if (bLogDuplicateEntries)
// {
// TArray<TWeakObjectPtr<UMetaSoundBuilderBase>> Entries;
// Builders.MultiFind(InClassName, Entries);
//
// // Don't print stale entries as during cook and some editor asset actions,
// // these may be removed after a new valid builder is created. If stale
// // entries leak, they will show up on registry logging upon destruction.
// Entries.RemoveAllSwap([](const TWeakObjectPtr<UMetaSoundBuilderBase>& Builder) { return !Builder.IsValid(); });
//
// if (!Entries.IsEmpty())
// {
// UE_LOG(LogMetaSound, Error, TEXT("More than one asset registered with class name '%s'. "
// "Look-up may return builder that is not associated with desired object! \n"
// "This can happen if asset was moved using revision control and original location was revived. \n"
// "Remove all but one of the following assets and relink a duplicate or copied replacement asset:"),
// *InClassName.ToString());
// for (const TWeakObjectPtr<UMetaSoundBuilderBase>& BuilderPtr : Entries)
// {
// UE_LOG(LogMetaSound, Error, TEXT("- %s"), *BuilderPtr->GetConstBuilder().CastDocumentObjectChecked<UObject>().GetPathName());
// }
// }
// }
// #endif // !NO_LOGGING
}
bool FDocumentBuilderRegistry::CanPostEventLog(ELogEvent Event, ELogVerbosity::Type Verbosity) const
{
#if NO_LOGGING
return false;
#else // !NO_LOGGING
if (const ELogVerbosity::Type* SetVerbosity = EventLogVerbosity.Find(Event))
{
return *SetVerbosity >= Verbosity;
}
return true;
#endif // !NO_LOGGING
}
#if WITH_EDITORONLY_DATA
bool FDocumentBuilderRegistry::CookPages(FName PlatformName, FMetaSoundFrontendDocumentBuilder& Builder) const
{
METASOUND_LLM_SCOPE;
METASOUND_TRACE_CPUPROFILER_EVENT_SCOPE(FDocumentBuilderRegistry::CookPages);
bool bModified = false;
const UMetaSoundSettings* Settings = GetDefault<UMetaSoundSettings>();
check(Settings);
const TArray<FGuid> PlatformTargetPageIDs = Settings->GetCookedTargetPageIDs(PlatformName);
checkf(!PlatformTargetPageIDs.IsEmpty(), TEXT("Must have at least one targeted page ID to cook MetaSound."));
const FString DebugName = Builder.GetDebugName();
auto StripPageEntries = [&](
TArray<FGuid>& PageIdsToResolve,
TSet<FGuid>& ResolveTargetScratch,
TFunctionRef<bool(const FGuid&)> RemovePageItem,
const FName& ItemName,
const FString& ItemType)
{
ResolveTargetScratch.Reset();
for (const FGuid& TargetPage : PlatformTargetPageIDs)
{
const FGuid PageID = ResolveTargetPageIDInternal(*Settings, PageIdsToResolve, TargetPage, PlatformName);
ResolveTargetScratch.Add(PageID);
}
auto IsResolvedTarget = [&ResolveTargetScratch](const FGuid& PageID) { return ResolveTargetScratch.Contains(PageID); };
checkf(!ResolveTargetScratch.IsEmpty(), TEXT("Failed to resolve any valid target IDs, which will leave serialized page array in invalid state."));
PageIdsToResolve.RemoveAllSwap(IsResolvedTarget, EAllowShrinking::No);
for (const FGuid& PageID : PageIdsToResolve)
{
const bool bRemovedPageItem = RemovePageItem(PageID);
if (bRemovedPageItem)
{
UE_LOG(LogMetaSound, Display, TEXT("%s: Removed %s %s w/PageID '%s'"),
*DebugName,
ItemName.IsNone() ? TEXT("paged") : *ItemName.ToString(),
*ItemType,
*PageID.ToString());
}
bModified |= bRemovedPageItem;
}
};
const FMetasoundFrontendDocument& Document = Builder.GetConstDocumentChecked();
TArray<FGuid> ResolvePageIDs;
TSet<FGuid> ResolvedTargetScratchIDs;
{ // Strip graphs
auto AddPageID = [&ResolvePageIDs](const FMetasoundFrontendGraph& Graph) { ResolvePageIDs.Add(Graph.PageID); };
Document.RootGraph.IterateGraphPages(AddPageID);
auto RemoveGraphPage = [&Builder](const FGuid& InPageID)
{
return Builder.RemoveGraphPage(InPageID);
};
const int32 NumInitGraphs = Document.RootGraph.GetConstGraphPages().Num();
StripPageEntries(ResolvePageIDs, ResolvedTargetScratchIDs, RemoveGraphPage, FName(), TEXT("graph"));
const int32 NumRemainingGraphs = Document.RootGraph.GetConstGraphPages().Num();
checkf(NumRemainingGraphs > 0,
TEXT("Document in MetaSound asset '%s' had all default values "
"cooked away leaving it in an invalid state. "
"Graph must always have at least one implementation."),
*Builder.GetDebugName());
if (NumInitGraphs > NumRemainingGraphs)
{
UE_LOG(LogMetaSound, Display, TEXT("Cook removed %i graph page(s) from '%s'"), NumInitGraphs - NumRemainingGraphs, *Builder.GetDebugName());
}
}
{ // Strip default input values
for (const FMetasoundFrontendClassInput& GraphInput : Document.RootGraph.GetDefaultInterface().Inputs)
{
ResolvePageIDs.Reset();
auto AddPageID = [&ResolvePageIDs](const FGuid& PageID, const FMetasoundFrontendLiteral&) { ResolvePageIDs.Add(PageID); };
GraphInput.IterateDefaults(AddPageID);
auto RemoveDefault = [&Builder, &GraphInput](const FGuid& InPageID)
{
const bool bClearInheritsDefault = false;
return Builder.RemoveGraphInputDefault(GraphInput.Name, InPageID, bClearInheritsDefault);
};
const int32 NumInitDefaults = GraphInput.GetDefaults().Num();
StripPageEntries(ResolvePageIDs, ResolvedTargetScratchIDs, RemoveDefault, GraphInput.Name, TEXT("input default"));
const int32 NumRemainingDefaults = GraphInput.GetDefaults().Num();
checkf(NumRemainingDefaults > 0,
TEXT("Input '%s' had all default values stripped leaving it in an invalid state. "
"Input must always have at least one default value"),
*GraphInput.Name.ToString());
if (NumInitDefaults > NumRemainingDefaults)
{
UE_LOG(LogMetaSound, Display, TEXT("Cook removed %i default input page value(s) from input '%s'"), NumInitDefaults - NumRemainingDefaults, *GraphInput.Name.ToString());
}
}
}
return bModified;
}
FMetaSoundFrontendDocumentBuilder& FDocumentBuilderRegistry::FindOrBeginBuilding(TScriptInterface<IMetaSoundDocumentInterface> MetaSound)
{
UObject* Object = MetaSound.GetObject();
check(Object);
return FindOrBeginBuilding(*Object).GetBuilder();
}
#endif // WITH_EDITORONLY_DATA
FMetaSoundFrontendDocumentBuilder* FDocumentBuilderRegistry::FindBuilder(TScriptInterface<IMetaSoundDocumentInterface> MetaSound) const
{
if (UMetaSoundBuilderBase* Builder = FindBuilderObject(MetaSound))
{
return &Builder->GetBuilder();
}
return nullptr;
}
FMetaSoundFrontendDocumentBuilder* FDocumentBuilderRegistry::FindBuilder(const FMetasoundFrontendClassName& InClassName, const FTopLevelAssetPath& AssetPath) const
{
if (UMetaSoundBuilderBase* Builder = FindBuilderObject(InClassName, AssetPath))
{
return &Builder->GetBuilder();
}
return nullptr;
}
UMetaSoundBuilderBase* FDocumentBuilderRegistry::FindBuilderObject(TScriptInterface<const IMetaSoundDocumentInterface> MetaSound) const
{
UMetaSoundBuilderBase* FoundEntry = nullptr;
if (const UObject* MetaSoundObject = MetaSound.GetObject())
{
const FMetasoundFrontendDocument& Document = MetaSound->GetConstDocument();
const FMetasoundFrontendClassName& ClassName = Document.RootGraph.Metadata.GetClassName();
TArray<TWeakObjectPtr<UMetaSoundBuilderBase>> Entries;
{
FScopeLock Lock(&BuildersCriticalSection);
Builders.MultiFind(ClassName, Entries);
}
for (const TWeakObjectPtr<UMetaSoundBuilderBase>& BuilderPtr : Entries)
{
if (UMetaSoundBuilderBase* Builder = BuilderPtr.Get())
{
// Can be invalid if look-up is called during asset removal/destruction or the entry was
// prematurely "finished". Only return invalid entry if builder asset path cannot be
// matched as this is likely the destroyed entry associated with the provided AssetPath.
const FMetaSoundFrontendDocumentBuilder& DocBuilder = Builder->GetConstBuilder();
if (DocBuilder.IsValid())
{
UObject& TestMetaSound = BuilderPtr->GetConstBuilder().CastDocumentObjectChecked<UObject>();
if (&TestMetaSound == MetaSoundObject)
{
FoundEntry = Builder;
break;
}
}
else
{
FoundEntry = Builder;
}
}
}
}
return FoundEntry;
}
UMetaSoundBuilderBase* FDocumentBuilderRegistry::FindBuilderObject(const FMetasoundFrontendClassName& InClassName, const FTopLevelAssetPath& AssetPath) const
{
TArray<TWeakObjectPtr<UMetaSoundBuilderBase>> Entries;
{
FScopeLock Lock(&BuildersCriticalSection);
Builders.MultiFind(InClassName, Entries);
}
UMetaSoundBuilderBase* FoundEntry = nullptr;
for (const TWeakObjectPtr<UMetaSoundBuilderBase>& BuilderPtr : Entries)
{
if (UMetaSoundBuilderBase* Builder = BuilderPtr.Get())
{
const FMetaSoundFrontendDocumentBuilder& DocBuilder = Builder->GetConstBuilder();
// Can be invalid if look-up is called during asset removal/destruction or the entry was
// prematurely "finished". Only return invalid entry if builder asset path cannot be
// matched as this is likely the destroyed entry associated with the provided AssetPath.
if (DocBuilder.IsValid())
{
const UObject& DocObject = DocBuilder.CastDocumentObjectChecked<UObject>();
FTopLevelAssetPath ObjectPath;
if (ObjectPath.TrySetPath(&DocObject))
{
if (AssetPath.IsNull() || AssetPath == ObjectPath)
{
FoundEntry = Builder;
break;
}
}
else
{
FoundEntry = Builder;
}
}
else
{
FoundEntry = Builder;
}
}
}
return FoundEntry;
}
TArray<UMetaSoundBuilderBase*> FDocumentBuilderRegistry::FindBuilderObjects(const FMetasoundFrontendClassName& InClassName) const
{
TArray<UMetaSoundBuilderBase*> FoundBuilders;
TArray<TWeakObjectPtr<UMetaSoundBuilderBase>> Entries;
{
FScopeLock Lock(&BuildersCriticalSection);
Builders.MultiFind(InClassName, Entries);
}
if (!Entries.IsEmpty())
{
Algo::TransformIf(Entries, FoundBuilders,
[](const TWeakObjectPtr<UMetaSoundBuilderBase>& BuilderPtr) { return BuilderPtr.IsValid(); },
[](const TWeakObjectPtr<UMetaSoundBuilderBase>& BuilderPtr) { return BuilderPtr.Get(); }
);
}
return FoundBuilders;
}
FMetaSoundFrontendDocumentBuilder* FDocumentBuilderRegistry::FindOutermostBuilder(const UObject& InSubObject) const
{
using namespace Metasound::Frontend;
TScriptInterface<IMetaSoundDocumentInterface> DocumentInterface = InSubObject.GetOutermostObject();
check(DocumentInterface.GetObject());
return FindBuilder(DocumentInterface);
}
bool FDocumentBuilderRegistry::FinishBuilding(const FMetasoundFrontendClassName& InClassName, bool bForceUnregisterNodeClass) const
{
using namespace Metasound;
using namespace Metasound::Engine;
TArray<UMetaSoundBuilderBase*> FoundBuilders = FindBuilderObjects(InClassName);
for (UMetaSoundBuilderBase* Builder : FoundBuilders)
{
FinishBuildingInternal(*Builder, bForceUnregisterNodeClass);
}
FScopeLock Lock(&BuildersCriticalSection);
return Builders.Remove(InClassName) > 0;
}
bool FDocumentBuilderRegistry::FinishBuilding(const FMetasoundFrontendClassName& InClassName, const FTopLevelAssetPath& AssetPath, bool bForceUnregisterNodeClass) const
{
using namespace Metasound;
using namespace Metasound::Engine;
TWeakObjectPtr<UMetaSoundBuilderBase> BuilderPtr;
if (UMetaSoundBuilderBase* Builder = FindBuilderObject(InClassName, AssetPath))
{
FinishBuildingInternal(*Builder, bForceUnregisterNodeClass);
BuilderPtr = TWeakObjectPtr<UMetaSoundBuilderBase>(Builder);
}
FScopeLock Lock(&BuildersCriticalSection);
return Builders.RemoveSingle(InClassName, BuilderPtr) > 0;
}
void FDocumentBuilderRegistry::FinishBuildingInternal(UMetaSoundBuilderBase& Builder, bool bForceUnregisterNodeClass) const
{
using namespace Metasound;
using namespace Metasound::Frontend;
// If the builder has applied transactions to its document object that are not mirrored in the frontend registry,
// unregister version in registry. This will ensure that future requests for the builder's associated asset will
// register a fresh version from the object as the transaction history is intrinsically lost once this builder
// is destroyed. It is also possible that the DocBuilder's underlying object can be invalid if object was force
// deleted, so validity check is necessary.
FMetaSoundFrontendDocumentBuilder& DocBuilder = Builder.GetBuilder();
if (DocBuilder.IsValid())
{
if (Metasound::CanEverExecuteGraph())
{
const int32 TransactionCount = DocBuilder.GetTransactionCount();
const int32 LastTransactionRegistered = Builder.GetLastTransactionRegistered();
if (bForceUnregisterNodeClass || LastTransactionRegistered != TransactionCount)
{
UObject& MetaSound = DocBuilder.CastDocumentObjectChecked<UObject>();
if (FMetasoundAssetBase* MetaSoundAsset = IMetasoundUObjectRegistry::Get().GetObjectAsAssetBase(&MetaSound))
{
MetaSoundAsset->UnregisterGraphWithFrontend();
}
}
}
DocBuilder.FinishBuilding();
}
}
#if WITH_EDITOR
FOnResolveEditorPage& FDocumentBuilderRegistry::GetOnResolveAuditionPageDelegate()
{
return OnResolveAuditionPage;
}
#endif // WITH_EDITOR
FOnResolvePage& FDocumentBuilderRegistry::GetOnResolveProjectPageOverrideDelegate()
{
return OnResolveProjectPage;
}
bool FDocumentBuilderRegistry::ReloadBuilder(const FMetasoundFrontendClassName& InClassName) const
{
bool bReloaded = false;
TArray<UMetaSoundBuilderBase*> ClassBuilders = FindBuilderObjects(InClassName);
for (UMetaSoundBuilderBase* Builder : ClassBuilders)
{
Builder->Reload();
bReloaded = true;
}
return bReloaded;
}
FGuid FDocumentBuilderRegistry::ResolveTargetPageID(const FMetasoundFrontendGraphClass& InGraphClass) const
{
METASOUND_LLM_SCOPE;
METASOUND_TRACE_CPUPROFILER_EVENT_SCOPE(FDocumentBuilderRegistry::ResolveTargetPageID_GraphClass);
#if !WITH_EDITORONLY_DATA
// No resolution required if only one item. This is the typical scenario and saves from tying up a lock
// and calling resolution delegate(s). For larger graphs, this can add up, and most implementation
// only has a single page value. With editor-only data, go ahead and take the perf hit in favor of
// resolution reporting (for example if page data is invalid and needs to be fixed up).
const TArray<FMetasoundFrontendGraph>& GraphPages = InGraphClass.GetConstGraphPages();
if (GraphPages.Num() == 1)
{
return GraphPages.Last().PageID;
}
#endif // !WITH_EDITORONLY_DATA
FScopeLock Lock(&TargetPageResolveScratchCritSec);
TargetPageResolveScratch.Reset();
InGraphClass.IterateGraphPages([this](const FMetasoundFrontendGraph& PageGraph)
{
TargetPageResolveScratch.Add(PageGraph.PageID);
});
return ResolveTargetPageIDInternal(TargetPageResolveScratch);
}
FGuid FDocumentBuilderRegistry::ResolveTargetPageID(const FMetasoundFrontendClassInput& InClassInput) const
{
METASOUND_LLM_SCOPE;
METASOUND_TRACE_CPUPROFILER_EVENT_SCOPE(FDocumentBuilderRegistry::ResolveTargetPageID_ClassInput);
#if !WITH_EDITORONLY_DATA
// No resolution required if only one item. This is the typical scenario and saves from tying up a lock
// and calling resolution delegate(s). For larger graphs, this can add up, and most implementation
// only has a single page value. With editor-only data, go ahead and take the perf hit in favor of
// resolution reporting (for example if page data is invalid and needs to be fixed up).
const TArray<FMetasoundFrontendClassInputDefault>& ClassDefaults = InClassInput.GetDefaults();
if (ClassDefaults.Num() == 1)
{
return ClassDefaults.Last().PageID;
}
#endif // !WITH_EDITORONLY_DATA
FScopeLock Lock(&TargetPageResolveScratchCritSec);
TargetPageResolveScratch.Reset();
InClassInput.IterateDefaults([this](const FGuid& PageID, const FMetasoundFrontendLiteral&)
{
TargetPageResolveScratch.Add(PageID);
});
return ResolveTargetPageIDInternal(TargetPageResolveScratch);
}
FGuid FDocumentBuilderRegistry::ResolveTargetPageID(const TArray<FMetasoundFrontendClassInputDefault>& InClassDefaults) const
{
METASOUND_LLM_SCOPE;
METASOUND_TRACE_CPUPROFILER_EVENT_SCOPE(FDocumentBuilderRegistry::ResolveTargetPageID_ClassDefaults);
#if !WITH_EDITORONLY_DATA
// No resolution required if only one item. This is the typical scenario and saves from tying up a lock
// and calling resolution delegate(s). For larger graphs, this can add up, and most implementation
// only has a single page value. With editor-only data, go ahead and take the perf hit in favor of
// resolution reporting (for example if page data is invalid and needs to be fixed up).
if (InClassDefaults.Num() == 1)
{
return InClassDefaults.Last().PageID;
}
#endif // !WITH_EDITORONLY_DATA
FScopeLock Lock(&TargetPageResolveScratchCritSec);
TargetPageResolveScratch.Reset();
Algo::Transform(InClassDefaults, TargetPageResolveScratch, [](const FMetasoundFrontendClassInputDefault& ClassDefault) { return ClassDefault.PageID; });
return ResolveTargetPageIDInternal(TargetPageResolveScratch);
}
FGuid FDocumentBuilderRegistry::ResolveTargetPageIDInternal(const TArray<FGuid>& InPageIDsToResolve) const
{
FName PlatformName = FPlatformProperties::IniPlatformName();
#if WITH_EDITOR
if (OnResolveAuditionPage.IsBound())
{
FPageResolutionEditorResults PreviewInfo = OnResolveAuditionPage.Execute(InPageIDsToResolve);
if (PreviewInfo.PageID.IsSet())
{
return PreviewInfo.PageID.GetValue();
}
PlatformName = PreviewInfo.PlatformName;
}
#endif // WITH_EDITOR
if (OnResolveProjectPage.IsBound())
{
const FGuid ResolvedPageID = OnResolveProjectPage.Execute(InPageIDsToResolve);
check (InPageIDsToResolve.Contains(ResolvedPageID));
return ResolvedPageID;
}
if (const UMetaSoundSettings* Settings = GetDefault<UMetaSoundSettings>())
{
const FGuid& TargetPageID = Settings->GetTargetPageSettings().UniqueId;
return ResolveTargetPageIDInternal(*Settings, InPageIDsToResolve, TargetPageID, PlatformName);
}
return Frontend::DefaultPageID;
}
FGuid FDocumentBuilderRegistry::ResolveTargetPageIDInternal(const UMetaSoundSettings& Settings, const TArray<FGuid>& InPageIDsToResolve, const FGuid& TargetPageID, FName PlatformName) const
{
bool bResolved = false;
FGuid ResolvedPageID = Frontend::DefaultPageID;
constexpr bool bReverse = true;
bool bFoundTarget = false;
Settings.IteratePageSettings([&](const FMetaSoundPageSettings& PageSettings)
{
bFoundTarget |= PageSettings.UniqueId == TargetPageID;
if (bFoundTarget && !bResolved)
{
const bool bAssetImplementsPage = InPageIDsToResolve.Contains(PageSettings.UniqueId);
if (bAssetImplementsPage)
{
#if WITH_EDITOR
const bool bIsCooked = !PageSettings.GetExcludeFromCook(PlatformName);
if (bIsCooked)
{
bResolved = true;
ResolvedPageID = PageSettings.UniqueId;
}
#else // !WITH_EDITOR
bResolved = true;
ResolvedPageID = PageSettings.UniqueId;
#endif // !WITH_EDITOR
}
}
}, bReverse);
if (!bResolved)
{
const FGuid& AnyPageID = InPageIDsToResolve.Last();
#if !NO_LOGGING
auto GetDisplayPageString = [&Settings](const FGuid& InPageID)
{
if (const FMetaSoundPageSettings* DisplayPage = Settings.FindPageSettings(InPageID))
{
return DisplayPage->Name.ToString();
}
return InPageID.ToString();
};
UE_LOG(LogMetaSound, Error,
TEXT("Failed to resolve PageID for Target '%s': Setting to arbitrary Page '%s' (Target likely overridden by page not set as 'CanTarget/Targetable' for the current platform)"),
*GetDisplayPageString(TargetPageID),
*GetDisplayPageString(AnyPageID));
#endif // !NO_LOGGING
ResolvedPageID = AnyPageID;
}
return ResolvedPageID;
}
void FDocumentBuilderRegistry::SetEventLogVerbosity(ELogEvent Event, ELogVerbosity::Type Verbosity)
{
EventLogVerbosity.FindOrAdd(Event) = Verbosity;
}
} // namespace Metasound::Engine