// 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 TUniquePtr 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()); const Metasound::Frontend::FNodeHandle OutputNode = Builder.AddOutput(OutputName, Metasound::GetMetasoundDataTypeName()); 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()); 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 SongMaps = MakeShared(Tempo, TimeSigNum, TimeSigDenom); check(SongMaps); SongMaps->SetSongLengthTicks(std::numeric_limits::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 Generator = BuildPassthroughGraph( *this, MidiInputName, MidiOutputName, SampleRate, NumSamplesPerBlock, &OutputNodeGuid); UTEST_TRUE("Generator is valid", Generator.IsValid()); // Add the analyzer Metasound::Frontend::FAnalyzerAddress AnalyzerAddress; AnalyzerAddress.DataType = Metasound::GetMetasoundDataTypeName(); 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 MidiIn = Generator->GetInputWriteReference(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 OutputData) { CallbackSuccess = TestEqual("Data types match", OutputData->GetDataTypeName(), Metasound::GetMetasoundDataTypeName()) && 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*>(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(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(SampleCount) * 1000.0f / SampleRate; Clock->AdvanceToMs(BlockOffset, AdvanceToMs); BlockOffset += MidiGranularity; } } // Render a block { CallbackSuccess = false; NumEventsReceivedThisBlock = 0; TArray 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