Files
UnrealEngine/Engine/Plugins/Runtime/Harmonix/Source/HarmonixMetasoundTests/Private/Analysis/MidiClockVertexAnalyzerTests.cpp
2025-05-18 13:04:45 +08:00

165 lines
5.8 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "NodeTestGraphBuilder.h"
#include "HarmonixMetasound/Analysis/MidiClockVertexAnalyzer.h"
#include "HarmonixMetasound/DataTypes/MidiClock.h"
#include "Misc/AutomationTest.h"
#if WITH_DEV_AUTOMATION_TESTS
namespace HarmonixMetasoundTests::MidiClockVertexAnalyzer
{
template<typename DataType>
TUniquePtr<Metasound::FMetasoundGenerator> BuildPassthroughGraph(
FAutomationTestBase& Test,
const FName& InputName,
const FName& OutputName,
const Metasound::FSampleRate SampleRate,
const int32 NumSamplesPerBlock,
FGuid& OutputGuid)
{
Metasound::Test::FNodeTestGraphBuilder Builder;
const Metasound::Frontend::FNodeHandle InputNode = Builder.AddInput(InputName, Metasound::GetMetasoundDataTypeName<DataType>());
const Metasound::Frontend::FNodeHandle OutputNode = Builder.AddOutput(OutputName, Metasound::GetMetasoundDataTypeName<DataType>());
const Metasound::Frontend::FOutputHandle OutputToConnect = InputNode->GetOutputWithVertexName(InputName);
const Metasound::Frontend::FInputHandle InputToConnect = OutputNode->GetInputWithVertexName(OutputName);
if (!Test.TestTrue("Connected input to output", InputToConnect->Connect(*OutputToConnect)))
{
return nullptr;
}
OutputGuid = OutputNode->GetID();
// have to add an audio output for the generator to render
Builder.AddOutput("Audio", Metasound::GetMetasoundDataTypeName<Metasound::FAudioBuffer>());
return Builder.BuildGenerator(SampleRate, NumSamplesPerBlock);
}
void ResetAndStartClock(const HarmonixMetasound::FMidiClockWriteRef& ClockInput, float Tempo, float Speed, int32 TimeSigNumerator, int32 TimeSigDenominator)
{
const TSharedPtr<FSongMaps> SongMaps = MakeShared<FSongMaps>(Tempo, TimeSigNumerator, TimeSigDenominator);
check(SongMaps);
SongMaps->SetSongLengthTicks(std::numeric_limits<int32>::max());
ClockInput->AttachToSongMapEvaluator(SongMaps);
ClockInput->SeekTo(0,0,0);
ClockInput->SetSpeed(0, Speed);
ClockInput->SetTransportState(0, HarmonixMetasound::EMusicPlayerTransportState::Playing);
}
void AdvanceClock(
bool bNeedsPrepare,
const HarmonixMetasound::FMidiClockWriteRef& ClockInput,
const int32 NumSamples)
{
if (bNeedsPrepare)
{
ClockInput->PrepareBlock();
}
ClockInput->Advance(0, NumSamples);
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FMidiClockVertexAnalyzerTestBasic,
"Harmonix.Metasound.Analysis.MidiClockVertexAnalyzer.Basic",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FMidiClockVertexAnalyzerTestBasic::RunTest(const FString&)
{
const FName InputName = "MidiClockIn";
const FName OutputName = "MidiClockOut";
constexpr Metasound::FSampleRate SampleRate = 48000;
constexpr int32 NumSamplesPerBlock = 480;
FGuid OutputGuid;
const TUniquePtr<Metasound::FMetasoundGenerator> Generator =
BuildPassthroughGraph<HarmonixMetasound::FMidiClock>(
*this,
InputName,
OutputName,
SampleRate,
NumSamplesPerBlock,
OutputGuid);
UTEST_TRUE("Generator is valid", Generator.IsValid());
// Add an analyzer to get the timestamp
{
Metasound::Frontend::FAnalyzerAddress AnalyzerAddress;
AnalyzerAddress.DataType = Metasound::GetMetasoundDataTypeName<HarmonixMetasound::FMidiClock>();
AnalyzerAddress.InstanceID = 1234;
AnalyzerAddress.OutputName = OutputName;
AnalyzerAddress.AnalyzerName = HarmonixMetasound::Analysis::FMidiClockVertexAnalyzer::GetAnalyzerName();
AnalyzerAddress.AnalyzerInstanceID = FGuid::NewGuid();
AnalyzerAddress.AnalyzerMemberName = HarmonixMetasound::Analysis::FMidiClockVertexAnalyzer::FOutputs::Timestamp.Name;
AnalyzerAddress.NodeID = OutputGuid;
Generator->AddOutputVertexAnalyzer(AnalyzerAddress);
}
// Get the clock
const TOptional<HarmonixMetasound::FMidiClockWriteRef> ClockRef = Generator->GetInputWriteReference<HarmonixMetasound::FMidiClock>(InputName);
UTEST_TRUE("Got clock", ClockRef.IsSet());
// Reset the clock
constexpr float Tempo = 123;
constexpr float Speed = 1.2f;
const FTimeSignature TimeSignature{ 3, 4 };
ResetAndStartClock(*ClockRef, Tempo, Speed, TimeSignature.Numerator, TimeSignature.Denominator);
// Listen for changes
bool CallbackSuccess = false;
FMusicTimestamp ReceivedTimestamp;
Generator->OnOutputChanged.AddLambda([ExpectedOutputName = OutputName, &CallbackSuccess, &ReceivedTimestamp](
const FName AnalyzerName,
const FName OutputName,
const FName AnalyzerOutputName,
TSharedPtr<Metasound::IOutputStorage> OutputData)
{
const bool IsExpectedCallback =
OutputData->GetDataTypeName() == Metasound::GetMetasoundDataTypeName<FMusicTimestamp>()
&& AnalyzerName == HarmonixMetasound::Analysis::FMidiClockVertexAnalyzer::GetAnalyzerName()
&& OutputName == ExpectedOutputName
&& AnalyzerOutputName == HarmonixMetasound::Analysis::FMidiClockVertexAnalyzer::FOutputs::Timestamp.Name;
if (!IsExpectedCallback)
{
return;
}
CallbackSuccess = true;
ReceivedTimestamp = static_cast<Metasound::TOutputStorage<FMusicTimestamp>*>(OutputData.Get())->Get();
});
// Render some blocks and make sure we're advancing at the expected rate
constexpr int32 NumBlocks = 20;
for(int32 i = 0; i < NumBlocks; ++i)
{
// Reset
CallbackSuccess = false;
ReceivedTimestamp.Reset();
// Advance the clock
AdvanceClock(i != 0, *ClockRef, NumSamplesPerBlock);
const FMusicTimestamp ExpectedTimestamp = (*ClockRef)->GetMusicTimestampAtBlockEnd();
// Render a block
TArray<float> Buffer;
Buffer.SetNumUninitialized(NumSamplesPerBlock);
Generator->OnGenerateAudio(Buffer.GetData(), Buffer.Num());
// Check that we got correct data from the analyzer
UTEST_TRUE("Callback succeeded", CallbackSuccess);
UTEST_EQUAL("Timestamps match", ReceivedTimestamp, ExpectedTimestamp);
}
return true;
}
}
#endif