335 lines
17 KiB
C++
335 lines
17 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
#pragma once
|
|
|
|
#include "AudioDevice.h"
|
|
#include "Components/AudioComponent.h"
|
|
#include "IAudioParameterInterfaceRegistry.h"
|
|
#include "Interfaces/IPluginManager.h"
|
|
#include "Interfaces/MetasoundInputFormatInterfaces.h"
|
|
#include "Interfaces/MetasoundOutputFormatInterfaces.h"
|
|
#include "Interfaces/MetasoundFrontendSourceInterface.h"
|
|
#include "Metasound.h"
|
|
#include "MetasoundBuilderSubsystem.h"
|
|
#include "MetasoundDataReference.h"
|
|
#include "MetasoundFrontendController.h"
|
|
#include "MetasoundFrontendSearchEngine.h"
|
|
#include "MetasoundTrigger.h"
|
|
#include "Misc/AutomationTest.h"
|
|
#include "Misc/Paths.h"
|
|
#include "NodeTemplates/MetasoundFrontendNodeTemplateInput.h"
|
|
#include "Tests/AutomationCommon.h"
|
|
|
|
#if WITH_DEV_AUTOMATION_TESTS
|
|
|
|
namespace EngineTestMetaSoundPatchBuilderPrivate
|
|
{
|
|
UMetaSoundPatchBuilder& CreatePatchBuilderChecked(FAutomationTestBase& Test, FName BuilderName, const TArray<FName>& InterfaceNamesToAdd)
|
|
{
|
|
EMetaSoundBuilderResult Result = EMetaSoundBuilderResult::Failed;
|
|
UMetaSoundPatchBuilder* Builder = UMetaSoundBuilderSubsystem::GetChecked().CreatePatchBuilder(BuilderName, Result);
|
|
checkf(Builder, TEXT("Failed to create MetaSoundPatchBuilder '%s', required for all further testing."), *BuilderName.ToString());
|
|
Test.AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded, TEXT("Builder created but CreatePatchBuilder did not result in 'Succeeded' state"));
|
|
|
|
for (const FName& InterfaceName : InterfaceNamesToAdd)
|
|
{
|
|
Builder->AddInterface(InterfaceName, Result);
|
|
Test.AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded, FString::Printf(TEXT("Failed to add initial interface '%s' to MetaSound patch"), *InterfaceName.ToString()));
|
|
}
|
|
|
|
return *Builder;
|
|
}
|
|
|
|
UMetaSoundPatch* CreatePatchFromBuilder(FAutomationTestBase& Test, const FString& PatchName, const TArray<FName>& InterfaceNamesToAdd)
|
|
{
|
|
UMetaSoundPatchBuilder& Builder = CreatePatchBuilderChecked(Test, FName(PatchName + TEXT(" Builder")), InterfaceNamesToAdd);
|
|
|
|
UMetaSoundPatch* InputPatch = CastChecked<UMetaSoundPatch>(Builder.BuildNewMetaSound(FName(PatchName)).GetObject());
|
|
Test.AddErrorIfFalse(InputPatch != nullptr, FString::Printf(TEXT("Failed to build MetaSound patch '%s'"), *PatchName));
|
|
return InputPatch;
|
|
}
|
|
} // namespace EngineTestMetaSoundPatchBuilderPrivate
|
|
|
|
|
|
DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FMetaSoundSourceBuilderDisconnectInputLatentCommand, FAutomationTestBase&, Test, UMetaSoundBuilderBase*, Builder, FMetaSoundBuilderNodeInputHandle, InputToDisconnect);
|
|
|
|
bool FMetaSoundSourceBuilderDisconnectInputLatentCommand::Update()
|
|
{
|
|
EMetaSoundBuilderResult Result = EMetaSoundBuilderResult::Failed;
|
|
if (Builder)
|
|
{
|
|
Builder->DisconnectNodeInput(InputToDisconnect, Result);
|
|
Test.AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded, TEXT("Failed to disconnect MetaSound node's input"));
|
|
}
|
|
|
|
return Result == EMetaSoundBuilderResult::Succeeded;
|
|
}
|
|
|
|
DEFINE_LATENT_AUTOMATION_COMMAND_FOUR_PARAMETER(FMetaSoundSourceBuilderSetLiteralLatentCommand, FAutomationTestBase&, Test, UMetaSoundBuilderBase*, Builder, FMetaSoundBuilderNodeInputHandle, NodeInput, FMetasoundFrontendLiteral, NewValue);
|
|
|
|
bool FMetaSoundSourceBuilderSetLiteralLatentCommand::Update()
|
|
{
|
|
EMetaSoundBuilderResult Result = EMetaSoundBuilderResult::Failed;
|
|
if (Builder)
|
|
{
|
|
Builder->SetNodeInputDefault(NodeInput, NewValue, Result);
|
|
Test.AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded, TEXT("Failed to disconnect MetaSound node's input"));
|
|
}
|
|
|
|
return Result == EMetaSoundBuilderResult::Succeeded;
|
|
}
|
|
|
|
DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FMetaSoundSourceBuilderRemoveNodeDefaultLiteralLatentCommand, FAutomationTestBase&, Test, UMetaSoundBuilderBase*, Builder, FMetaSoundBuilderNodeInputHandle, NodeInput);
|
|
|
|
bool FMetaSoundSourceBuilderRemoveNodeDefaultLiteralLatentCommand::Update()
|
|
{
|
|
EMetaSoundBuilderResult Result = EMetaSoundBuilderResult::Failed;
|
|
if (Builder)
|
|
{
|
|
Builder->RemoveNodeInputDefault(NodeInput, Result);
|
|
Test.AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded, TEXT("Failed to disconnect MetaSound node's input"));
|
|
}
|
|
|
|
return Result == EMetaSoundBuilderResult::Succeeded;
|
|
}
|
|
|
|
DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FMetaSoundSourceBuilderCreateAndConnectTriGeneratorNodeLatentCommand, FAutomationTestBase&, Test, UMetaSoundSourceBuilder*, Builder, FMetaSoundBuilderNodeInputHandle, AudioOutNodeInput);
|
|
|
|
bool FMetaSoundSourceBuilderCreateAndConnectTriGeneratorNodeLatentCommand::Update()
|
|
{
|
|
EMetaSoundBuilderResult Result = EMetaSoundBuilderResult::Failed;
|
|
if (Builder)
|
|
{
|
|
// Tri Oscillator Node
|
|
FMetaSoundNodeHandle TriNode = Builder->AddNodeByClassName({ "UE", "Triangle", "Audio" }, Result, 1);
|
|
Test.AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded && TriNode.IsSet(), TEXT("Failed to create node by class name 'UE:Triangle:Audio"));
|
|
|
|
FMetaSoundBuilderNodeOutputHandle TriNodeAudioOutput = Builder->FindNodeOutputByName(TriNode, "Audio", Result);
|
|
Test.AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded, TEXT("Failed to find Triangle Oscillator node output 'Audio'"));
|
|
|
|
Builder->ConnectNodes(TriNodeAudioOutput, AudioOutNodeInput, Result);
|
|
Test.AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded, TEXT("Failed to connect 'Audio' Triangle Oscillator output to MetaSound graph's 'Mono Output'"));
|
|
}
|
|
|
|
return Result == EMetaSoundBuilderResult::Succeeded;
|
|
}
|
|
|
|
DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FBuilderRemoveFromRootLatentCommand, UMetaSoundBuilderBase*, Builder);
|
|
|
|
bool FBuilderRemoveFromRootLatentCommand::Update()
|
|
{
|
|
if (Builder)
|
|
{
|
|
Builder->RemoveFromRoot();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Creates a collection of MetaSound patches from builders and exercises bindings all input and output interfaces by connecting and disconnecting using various builder API calls.
|
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAudioMetaSoundBuilderInterfaceBindingConnectAndDisconnect, "Audio.Metasound.Builder.InterfaceBindingConnectAndDisconnect", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
|
bool FAudioMetaSoundBuilderInterfaceBindingConnectAndDisconnect::RunTest(const FString& Parameters)
|
|
{
|
|
using namespace EngineTestMetaSoundPatchBuilderPrivate;
|
|
using namespace Metasound::Engine;
|
|
|
|
const TArray<FName> InputInterfaces =
|
|
{
|
|
InputFormatMonoInterface::GetVersion().Name,
|
|
InputFormatStereoInterface::GetVersion().Name,
|
|
InputFormatQuadInterface::GetVersion().Name,
|
|
InputFormatFiveDotOneInterface::GetVersion().Name,
|
|
InputFormatSevenDotOneInterface::GetVersion().Name
|
|
};
|
|
const TArray<FName> OutputInterfaces =
|
|
{
|
|
OutputFormatMonoInterface::GetVersion().Name,
|
|
OutputFormatStereoInterface::GetVersion().Name,
|
|
OutputFormatQuadInterface::GetVersion().Name,
|
|
OutputFormatFiveDotOneInterface::GetVersion().Name,
|
|
OutputFormatSevenDotOneInterface::GetVersion().Name
|
|
};
|
|
|
|
auto MakePatchName = [](FName InterfaceName) { return FString::Printf(TEXT("%s Patch"), *InterfaceName.ToString()); };
|
|
EMetaSoundBuilderResult Result = EMetaSoundBuilderResult::Failed;
|
|
for (const FName& InputInterface : InputInterfaces)
|
|
{
|
|
if (UMetaSoundPatch* InputBuilder = CreatePatchFromBuilder(*this, MakePatchName(InputInterface), { InputInterface }))
|
|
{
|
|
for (const FName& OutputInterface : OutputInterfaces)
|
|
{
|
|
if (UMetaSoundPatch* OutputBuilder = CreatePatchFromBuilder(*this, MakePatchName(OutputInterface), { OutputInterface }))
|
|
{
|
|
const FString BindBuilderName = FString::Printf(TEXT("%s to %s Binding"), *OutputInterface.ToString(), *InputInterface.ToString());
|
|
UMetaSoundPatchBuilder& BindingBuilder = CreatePatchBuilderChecked(*this, *BindBuilderName, { InputInterface, OutputInterface });
|
|
FMetaSoundNodeHandle InputInterfaceNode = BindingBuilder.AddNode(TScriptInterface<IMetaSoundDocumentInterface>(InputBuilder), Result);
|
|
FMetaSoundNodeHandle OutputInterfaceNode = BindingBuilder.AddNode(TScriptInterface<IMetaSoundDocumentInterface>(OutputBuilder), Result);
|
|
BindingBuilder.ConnectNodesByInterfaceBindings(OutputInterfaceNode, InputInterfaceNode, Result);
|
|
AddErrorIfFalse(
|
|
Result == EMetaSoundBuilderResult::Succeeded,
|
|
FString::Printf(TEXT("Failed to connect nodes via binding with interface '%s' to '%s'"), *OutputInterface.ToString(), *InputInterface.ToString())
|
|
);
|
|
|
|
BindingBuilder.DisconnectNodesByInterfaceBindings(OutputInterfaceNode, InputInterfaceNode, Result);
|
|
AddErrorIfFalse(
|
|
Result == EMetaSoundBuilderResult::Succeeded,
|
|
FString::Printf(TEXT("Failed to disconnect nodes via binding with interface '%s' to '%s'"), *OutputInterface.ToString(), *InputInterface.ToString())
|
|
);
|
|
|
|
BindingBuilder.ConnectNodeInputsToMatchingGraphInterfaceInputs(InputInterfaceNode, Result);
|
|
AddErrorIfFalse(
|
|
Result == EMetaSoundBuilderResult::Succeeded,
|
|
FString::Printf(TEXT("Failed to connect input node with matching graph interface inputs: interface '%s'"), *InputInterface.ToString())
|
|
);
|
|
|
|
BindingBuilder.ConnectNodeOutputsToMatchingGraphInterfaceOutputs(OutputInterfaceNode, Result);
|
|
AddErrorIfFalse(
|
|
Result == EMetaSoundBuilderResult::Succeeded,
|
|
FString::Printf(TEXT("Failed to connect output node with matching graph interface outputs: interface '%s'"), *OutputInterface.ToString())
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// This test creates a MetaSound patch, then adds and connects sin oscillator, attempts to retrieve a default input set, then clears it, and finally retrieves the class default
|
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAudioMetaSoundNodeClassQueryFunctions, "Audio.Metasound.Builder.NodeClassQueryFunctions", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
|
bool FAudioMetaSoundNodeClassQueryFunctions::RunTest(const FString& Parameters)
|
|
{
|
|
using namespace EngineTestMetaSoundPatchBuilderPrivate;
|
|
using namespace Metasound;
|
|
using namespace Metasound::Frontend;
|
|
|
|
constexpr float NodeDefaultFreq = 100.0f;
|
|
|
|
EMetaSoundBuilderResult Result = EMetaSoundBuilderResult::Failed;
|
|
UMetaSoundPatchBuilder& Builder = CreatePatchBuilderChecked(*this, "DefaultLiteralAssignment_Test", { });
|
|
|
|
// Sine Oscillator Node
|
|
const FMetaSoundNodeHandle OscNode = Builder.AddNodeByClassName({ "UE", "Sine", "Audio" }, Result, 1);
|
|
AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded && OscNode.IsSet(), TEXT("Failed to create new MetaSound node by class name"));
|
|
|
|
// Make connections:
|
|
const FMetaSoundBuilderNodeInputHandle OscNodeFrequencyInput = Builder.FindNodeInputByName(OscNode, "Frequency", Result);
|
|
AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded && OscNodeFrequencyInput.IsSet(), TEXT("Failed to find Sine Oscillator node input 'Frequency'"));
|
|
|
|
FMetasoundFrontendLiteral Literal;
|
|
Literal.Set(100.0f);
|
|
|
|
Builder.SetNodeInputDefault(OscNodeFrequencyInput, Literal, Result);
|
|
AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded, TEXT("Failed to 'SetNodeInputDefault'"));
|
|
|
|
Literal = Builder.GetNodeInputDefault(OscNodeFrequencyInput, Result);
|
|
AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded, TEXT("Failed to 'GetNodeInputDefault'"));
|
|
|
|
float Freq = 0.0f;
|
|
bool bLiteralIsFloat = Literal.TryGet(Freq);
|
|
AddErrorIfFalse(bLiteralIsFloat, TEXT("Failed to retrieve node literal as 'float'"));
|
|
AddErrorIfFalse(FMath::IsNearlyEqual(NodeDefaultFreq, Freq), TEXT("'Freq' node default not set to provided default"));
|
|
|
|
Builder.RemoveNodeInputDefault(OscNodeFrequencyInput, Result);
|
|
AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded, TEXT("Failed to 'RemoveNodeInputDefault'"));
|
|
|
|
Literal = Builder.GetNodeInputClassDefault(OscNodeFrequencyInput, Result);
|
|
AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded, TEXT("Failed to 'GetNodeInputClassDefault' on c++ Sin Osc node"));
|
|
|
|
bLiteralIsFloat = Literal.TryGet(Freq);
|
|
AddErrorIfFalse(bLiteralIsFloat, TEXT("Failed to retrieve class input literal as 'float'"));
|
|
AddErrorIfFalse(FMath::IsNearlyEqual(440.0f, Freq), TEXT("'Freq' node default not set to expected sin freq default of 440.0f Hz"));
|
|
|
|
// Output Default Test
|
|
{
|
|
Literal.Set(123.0f);
|
|
const FMetaSoundBuilderNodeInputHandle InputTest = Builder.AddGraphOutputNode("Output", GetMetasoundDataTypeName<float>(), Literal, Result);
|
|
AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded && InputTest.IsSet(), TEXT("Failed to create new MetaSound graph input"));
|
|
|
|
bLiteralIsFloat = Literal.TryGet(Freq);
|
|
AddErrorIfFalse(bLiteralIsFloat, TEXT("Failed to retrieve class input literal as 'float'"));
|
|
AddErrorIfFalse(FMath::IsNearlyEqual(123.0f, Freq), TEXT("'Freq' node default not set to expected sin freq default of 440.0f Hz"));
|
|
|
|
const bool bIsConstructorPin = Builder.GetNodeInputIsConstructorPin(InputTest);
|
|
AddErrorIfFalse(!bIsConstructorPin, TEXT("Pin is not constructor pin but GetNodeInputIsConstructorPin is returning true"));
|
|
}
|
|
|
|
// Output Ctor tests
|
|
{
|
|
const FMetaSoundBuilderNodeInputHandle CtorTest = Builder.AddGraphOutputNode("CtorOutputTest", GetMetasoundDataTypeName<float>(), Literal, Result, true);
|
|
AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded && CtorTest.IsSet(), TEXT("Failed to create new MetaSound graph input"));
|
|
|
|
const bool bIsConstructorPin = Builder.GetNodeInputIsConstructorPin(CtorTest);
|
|
AddErrorIfFalse(bIsConstructorPin, TEXT("Output handle is constructor pin but GetNodeInputIsConstructorPin is returning false"));
|
|
}
|
|
|
|
// Input Ctor tests
|
|
{
|
|
const FMetaSoundBuilderNodeOutputHandle CtorTest = Builder.AddGraphInputNode("CtorOutput", GetMetasoundDataTypeName<float>(), Literal, Result, true);
|
|
AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded && CtorTest.IsSet(), TEXT("Failed to create new MetaSound graph input"));
|
|
|
|
const bool bIsConstructorPin = Builder.GetNodeOutputIsConstructorPin(CtorTest);
|
|
AddErrorIfFalse(bIsConstructorPin, TEXT("Input handle is constructor pin but GetNodeInputIsConstructorPin is returning false"));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// This test creates a MetaSound patch, adds a frequency input, connects a sin oscillator, and if the editor is loaded, and then attempts to inject template input nodes.
|
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAudioMetaSoundInjectTemplateNodes, "Audio.Metasound.Builder.InjectInputTemplateNodes", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
|
bool FAudioMetaSoundInjectTemplateNodes::RunTest(const FString& Parameters)
|
|
{
|
|
using namespace EngineTestMetaSoundPatchBuilderPrivate;
|
|
using namespace Metasound;
|
|
using namespace Metasound::Frontend;
|
|
|
|
constexpr float NodeDefaultFreq = 100.0f;
|
|
|
|
EMetaSoundBuilderResult Result = EMetaSoundBuilderResult::Failed;
|
|
UMetaSoundPatchBuilder& Builder = CreatePatchBuilderChecked(*this, "TemplateInputInjection_Test", { });
|
|
|
|
// Sine Oscillator Node
|
|
const FMetaSoundNodeHandle OscNode = Builder.AddNodeByClassName({ "UE", "Sine", "Audio" }, Result, 1);
|
|
AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded && OscNode.IsSet(), TEXT("Failed to create new MetaSound node by class name"));
|
|
|
|
auto MakeAndConnectSinInputToGraphInput = [&](FName InputName, FName TypeName, const FVector2D* Location, const FMetasoundFrontendLiteral& InLiteral, bool bIsCtorPin)
|
|
{
|
|
const FMetaSoundBuilderNodeOutputHandle InputNodeOutputHandle = Builder.AddGraphInputNode(InputName, TypeName, InLiteral, Result, bIsCtorPin);
|
|
AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded && InputNodeOutputHandle.IsSet(), TEXT("Failed to create new MetaSound graph input"));
|
|
|
|
const FMetaSoundBuilderNodeInputHandle OscNodeInput = Builder.FindNodeInputByName(OscNode, InputName, Result);
|
|
AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded && OscNodeInput.IsSet(), TEXT("Failed to find Sine Oscillator node input"));
|
|
|
|
Builder.ConnectNodes(InputNodeOutputHandle, OscNodeInput, Result);
|
|
AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded, TEXT("Failed to connect input node to node input"));
|
|
|
|
#if WITH_EDITOR
|
|
if (Location)
|
|
{
|
|
Builder.SetNodeLocation(FMetaSoundNodeHandle{ InputNodeOutputHandle.NodeID }, *Location, Result);
|
|
AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded, TEXT("Failed to set input node location"));
|
|
}
|
|
#endif // WITH_EDITOR
|
|
};
|
|
FMetasoundFrontendLiteral Literal;
|
|
|
|
Literal.Set(true);
|
|
MakeAndConnectSinInputToGraphInput("Enabled", GetMetasoundDataTypeName<bool>(), nullptr, Literal, true);
|
|
|
|
Literal.Set(100.0f);
|
|
FVector2D FreqLoc(100, 100);
|
|
MakeAndConnectSinInputToGraphInput("Frequency", GetMetasoundDataTypeName<float>(), nullptr, Literal, false);
|
|
|
|
Literal.Set(FMetasoundFrontendLiteral::FDefault { });
|
|
FVector2D SyncLoc(300, 300);
|
|
MakeAndConnectSinInputToGraphInput("Sync", GetMetasoundDataTypeName<FTrigger>(), nullptr, Literal, false);
|
|
|
|
#if WITH_EDITOR
|
|
constexpr bool bForceNodeCreation = false;
|
|
Builder.InjectInputTemplateNodes(bForceNodeCreation, Result);
|
|
AddErrorIfFalse(Result == EMetaSoundBuilderResult::Succeeded, TEXT("Failed to inject input template node"));
|
|
#endif // WITH_EDITOR
|
|
|
|
return true;
|
|
}
|
|
#endif // WITH_DEV_AUTOMATION_TESTS
|