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

217 lines
8.0 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "NodeTestGraphBuilder.h"
#include "HarmonixMetasound/Analysis/MidiStreamVertexAnalyzer.h"
#include "HarmonixMetasound/DataTypes/MidiStream.h"
#include "Misc/AutomationTest.h"
#if WITH_DEV_AUTOMATION_TESTS
namespace HarmonixMetasoundTests::MidiStreamVertexAnalyzer
{
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;
}
if (nullptr != OutputGuid)
{
*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 InitClock(
HarmonixMetasound::FMidiClockWriteRef& Clock,
const float Tempo,
const int32 TimeSigNum,
const int32 TimeSigDenom)
{
using namespace Metasound;
// Make the tempo and time sig maps
const TSharedPtr<FSongMaps> SongMaps = MakeShared<FSongMaps>(Tempo, TimeSigNum, TimeSigDenom);
check(SongMaps);
SongMaps->SetSongLengthTicks(std::numeric_limits<int32>::max());
// Attach the maps
Clock->AttachToSongMapEvaluator(SongMaps);
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FMidiStreamVertexAnalyzerTestBasic,
"Harmonix.Metasound.Analysis.MidiStreamVertexAnalyzer.Basic",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FMidiStreamVertexAnalyzerTestBasic::RunTest(const FString&)
{
const FName MidiInputName = "MyInput";
const FName MidiOutputName = "MyOutput";
constexpr Metasound::FSampleRate SampleRate = 48000;
constexpr int32 NumSamplesPerBlock = 128;
FGuid OutputNodeGuid;
const TUniquePtr<Metasound::FMetasoundGenerator> Generator = BuildPassthroughGraph<HarmonixMetasound::FMidiStream>(
*this,
MidiInputName,
MidiOutputName,
SampleRate,
NumSamplesPerBlock,
&OutputNodeGuid);
UTEST_TRUE("Generator is valid", Generator.IsValid());
// Add the analyzer
Metasound::Frontend::FAnalyzerAddress AnalyzerAddress;
AnalyzerAddress.DataType = Metasound::GetMetasoundDataTypeName<HarmonixMetasound::FMidiStream>();
AnalyzerAddress.InstanceID = 1234;
AnalyzerAddress.OutputName = MidiOutputName;
AnalyzerAddress.AnalyzerName = HarmonixMetasound::Analysis::FMidiStreamVertexAnalyzer::GetAnalyzerName();
AnalyzerAddress.AnalyzerInstanceID = FGuid::NewGuid();
AnalyzerAddress.AnalyzerMemberName = HarmonixMetasound::Analysis::FMidiStreamVertexAnalyzer::FOutputs::GetValue().Name;
AnalyzerAddress.NodeID = OutputNodeGuid;
Generator->AddOutputVertexAnalyzer(AnalyzerAddress);
// Get the MIDI in and attach a clock
const TOptional<HarmonixMetasound::FMidiStreamWriteRef> MidiIn =
Generator->GetInputWriteReference<HarmonixMetasound::FMidiStream>(MidiInputName);
UTEST_TRUE("MIDI input is valid", MidiIn.IsSet());
constexpr float Tempo = 87;
HarmonixMetasound::FMidiClockWriteRef Clock = HarmonixMetasound::FMidiClockWriteRef::CreateNew(Generator->OperatorSettings);
(*MidiIn)->SetClock(*Clock);
bool CallbackSuccess = false;
constexpr int32 EventIntervalTicks = Harmonix::Midi::Constants::GTicksPerQuarterNoteInt;
int32 NextExpectedNoteNumber = 60;
int32 NextExpectedVelocity = 20;
int32 NextExpectedEventTicks = 0;
int32 NumEventsReceivedThisBlock = 0;
// Subscribe for analyzer updates
Generator->OnOutputChanged.AddLambda(
[this, &AnalyzerAddress, &Clock, &CallbackSuccess, EventIntervalTicks, &NextExpectedNoteNumber, &NextExpectedVelocity, &NextExpectedEventTicks, &NumEventsReceivedThisBlock]
(const FName AnalyzerName, const FName OutputName, const FName AnalyzerOutputName, TSharedPtr<Metasound::IOutputStorage> OutputData)
{
CallbackSuccess = TestEqual("Data types match", OutputData->GetDataTypeName(), Metasound::GetMetasoundDataTypeName<FMidiEventInfo>())
&& TestEqual("Analyzer names match", AnalyzerName, AnalyzerAddress.AnalyzerName)
&& TestEqual("Output names match", OutputName, AnalyzerAddress.OutputName)
&& TestEqual("Analyzer output names match", AnalyzerOutputName, AnalyzerAddress.AnalyzerMemberName);
if (!CallbackSuccess)
{
return;
}
const FMidiEventInfo& EventInfo = static_cast<Metasound::TOutputStorage<FMidiEventInfo>*>(OutputData.Get())->Get();
// Check the note data
CallbackSuccess = TestTrue("Is note on", EventInfo.IsNoteOn())
&& TestEqual("Correct note number", EventInfo.GetNoteNumber(), NextExpectedNoteNumber)
&& TestEqual("Correct velocity", EventInfo.GetVelocity(), NextExpectedVelocity);
// Check the timestamp
const int32 Tick = Clock->GetSongMapEvaluator().MusicTimestampToTick(EventInfo.Timestamp);
CallbackSuccess = TestEqual("Timestamp matches", Tick, NextExpectedEventTicks);
++NextExpectedNoteNumber;
++NextExpectedVelocity;
NextExpectedEventTicks += EventIntervalTicks;
++NumEventsReceivedThisBlock;
});
constexpr int32 NumEventsToSend = 20;
const int32 NumBlocksToTest = FMath::CeilToInt((static_cast<float>(NumEventsToSend) * SampleRate / (Tempo / 60)) / NumSamplesPerBlock);
int32 SampleRemainder = 0;
int32 SampleCount = 0;
float TicksPerBlock = Harmonix::Midi::Constants::GTicksPerQuarterNote * NumSamplesPerBlock * (Tempo / 60) / SampleRate;
float CurrentBlockTick = 0;
int32 NextEventTick = 0;
int32 NextNoteNumber = NextExpectedNoteNumber;
int32 NextVelocity = NextExpectedVelocity;
// Render and expect to get all the events
for (int32 i = 0; i < NumBlocksToTest; ++i)
{
// Clear the MIDI input
(*MidiIn)->PrepareBlock();
// Add events if appropriate
int32 NumEventsExpected = 0;
CurrentBlockTick += TicksPerBlock;
while (NextEventTick < CurrentBlockTick)
{
FMidiMsg Msg = FMidiMsg::CreateNoteOn(0, NextNoteNumber++, NextVelocity++);
HarmonixMetasound::FMidiStreamEvent Event(0u, Msg);
Event.CurrentMidiTick = NextEventTick;
(*MidiIn)->AddMidiEvent(Event);
NextEventTick += EventIntervalTicks;
++NumEventsExpected;
}
// Advance the clock
{
if (i == 0)
{
Clock->SetTransportState(0, HarmonixMetasound::EMusicPlayerTransportState::Playing);
}
else
{
Clock->PrepareBlock();
}
int32 BlockOffset = SampleRemainder;
SampleRemainder += NumSamplesPerBlock;
constexpr int32 MidiGranularity = 128;
while (SampleRemainder >= MidiGranularity)
{
SampleCount += MidiGranularity;
SampleRemainder -= MidiGranularity;
const float AdvanceToMs = static_cast<float>(SampleCount) * 1000.0f / SampleRate;
Clock->AdvanceToMs(BlockOffset, AdvanceToMs);
BlockOffset += MidiGranularity;
}
}
// Render a block
{
CallbackSuccess = false;
NumEventsReceivedThisBlock = 0;
TArray<float> Buffer;
Buffer.Reserve(NumSamplesPerBlock);
UTEST_EQUAL("Generated the right number of samples.",
Generator->OnGenerateAudio(Buffer.GetData(), NumSamplesPerBlock),
NumSamplesPerBlock);
}
// If we were expecting an event, check to see that we got it
if (NumEventsExpected > 0)
{
UTEST_TRUE(FString::Printf(TEXT("Callback success on iteration %i"), i), CallbackSuccess);
UTEST_EQUAL(FString::Printf(TEXT("Right number of events in iteration %i"), i), NumEventsReceivedThisBlock, NumEventsExpected);
}
}
return true;
}
}
#endif