// Copyright Epic Games, Inc. All Rights Reserved. #include "Logging/LogMacros.h" #include "Misc/AutomationTest.h" #include "HarmonixMidi/MidiFile.h" #if WITH_DEV_AUTOMATION_TESTS DEFINE_LOG_CATEGORY_STATIC(LogSongMapsTest, Log, All); namespace HarmonixMidiTests::SongMaps { static constexpr bool GLogQualtizationDetails = false; void LogQuantizationDetails(const ISongMapEvaluator& Map, int32 OriginalTick, int32 QuantizedTick, EMidiClockSubdivisionQuantization ResultDivision) { int32 BarIndex = 0; int32 BeatInBar = 0; int32 TickIndexInBeat = 0; Map.TickToBarBeatTickIncludingCountIn(QuantizedTick, BarIndex, BeatInBar, TickIndexInBeat); UE_LOG(LogSongMapsTest, Log, TEXT("Tick -> %d: Quantized %s to %d, Division = %s -> %d | %d | %d"), OriginalTick, (QuantizedTick < OriginalTick ? TEXT("^") : TEXT("v")), QuantizedTick, *StaticEnum()->GetDisplayNameTextByIndex((int32)ResultDivision).ToString(), BarIndex, BeatInBar, TickIndexInBeat); } TSharedPtr BuildMidiWithOneTimeSigature(int32 Numerator, int32 Denominator) { TSharedPtr MidiData = MakeShared(); check(MidiData); // make 97bpm, 4/4 tempo map... MidiData->SongMaps.EmptyAllMaps(); MidiData->Tracks.Empty(); MidiData->Tracks.Add(FMidiTrack(TEXT("conductor"))); MidiData->Tracks[0].AddEvent(FMidiEvent(0, FMidiMsg(Numerator, Denominator))); MidiData->SongMaps.AddTimeSignatureAtBarIncludingCountIn(0, Numerator, Denominator); const int32 MidiTempo = Harmonix::Midi::Constants::BPMToMidiTempo(97.0f); MidiData->Tracks[0].AddEvent(FMidiEvent(0, FMidiMsg(MidiTempo))); MidiData->SongMaps.AddTempoInfoPoint(MidiTempo, 0); MidiData->Tracks[0].Sort(); MidiData->ConformToLength(std::numeric_limits::max()); return MidiData; } IMPLEMENT_SIMPLE_AUTOMATION_TEST( FTestSongMapsFourFour, "Harmonix.Midi.SongMaps.QuantizeToAny.4/4", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) bool FTestSongMapsFourFour::RunTest(const FString&) { const TSharedPtr MidiData = BuildMidiWithOneTimeSigature(4,4); const FSongMaps& SongMaps = MidiData->SongMaps; for (int32 i = 0; i < SongMaps.GetTicksPerQuarterNote() * 7; i+=10) { EMidiClockSubdivisionQuantization ResultDivision = EMidiClockSubdivisionQuantization::Bar; int32 QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Nearest, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), QuantizedTick % SongMaps.SubdivisionToMidiTicks(ResultDivision, QuantizedTick) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Down, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), QuantizedTick % SongMaps.SubdivisionToMidiTicks(ResultDivision, QuantizedTick) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Up, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), QuantizedTick % SongMaps.SubdivisionToMidiTicks(ResultDivision, QuantizedTick) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } } return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST( FTestSongMapsFiveEight, "Harmonix.Midi.SongMaps.QuantizeToAny.5/8", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) bool FTestSongMapsFiveEight::RunTest(const FString&) { const TSharedPtr MidiData = BuildMidiWithOneTimeSigature(5, 8); const FSongMaps& SongMaps = MidiData->SongMaps; for (int32 i = 0; i < SongMaps.GetTicksPerQuarterNote() * 7; i += 10) { EMidiClockSubdivisionQuantization ResultDivision = EMidiClockSubdivisionQuantization::Bar; int32 QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Nearest, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), QuantizedTick % SongMaps.SubdivisionToMidiTicks(ResultDivision, QuantizedTick) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Down, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), QuantizedTick % SongMaps.SubdivisionToMidiTicks(ResultDivision, QuantizedTick) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Up, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), QuantizedTick % SongMaps.SubdivisionToMidiTicks(ResultDivision, QuantizedTick) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } } return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST( FTestSongMapsThreeTwo, "Harmonix.Midi.SongMaps.QuantizeToAny.3/2", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) bool FTestSongMapsThreeTwo::RunTest(const FString&) { const TSharedPtr MidiData = BuildMidiWithOneTimeSigature(3, 2); const FSongMaps& SongMaps = MidiData->SongMaps; for (int32 i = 0; i < SongMaps.GetTicksPerQuarterNote() * 7; i += 10) { EMidiClockSubdivisionQuantization ResultDivision = EMidiClockSubdivisionQuantization::Bar; int32 QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Nearest, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), QuantizedTick % SongMaps.SubdivisionToMidiTicks(ResultDivision, QuantizedTick) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Down, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), QuantizedTick % SongMaps.SubdivisionToMidiTicks(ResultDivision, QuantizedTick) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Up, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), QuantizedTick % SongMaps.SubdivisionToMidiTicks(ResultDivision, QuantizedTick) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } } return true; }; IMPLEMENT_SIMPLE_AUTOMATION_TEST( FTestSongMapsMixedTimeSig, "Harmonix.Midi.SongMaps.QuantizeToAny.MixedTimeSignatures", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) bool FTestSongMapsMixedTimeSig::RunTest(const FString&) { TSharedPtr MidiData = BuildMidiWithOneTimeSigature(4, 4); int32 TickAfterFirstBar = MidiData->TicksPerQuarterNote * 4; MidiData->Tracks[0].AddEvent(FMidiEvent(TickAfterFirstBar, FMidiMsg(5, 8))); MidiData->SongMaps.AddTimeSignatureAtBarIncludingCountIn(1, 5, 8); int32 TickAfterSecondBar = MidiData->TicksPerQuarterNote / 2 * 5 + TickAfterFirstBar; MidiData->Tracks[0].AddEvent(FMidiEvent(TickAfterSecondBar, FMidiMsg(3, 2))); MidiData->SongMaps.AddTimeSignatureAtBarIncludingCountIn(2, 3, 2); MidiData->Tracks[0].Sort(); const FSongMaps& SongMaps = MidiData->SongMaps; // Check First Bar... for (int32 i = 0; i < TickAfterFirstBar; i += 10) { EMidiClockSubdivisionQuantization ResultDivision = EMidiClockSubdivisionQuantization::Bar; int32 QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Nearest, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), QuantizedTick % SongMaps.SubdivisionToMidiTicks(ResultDivision, i) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Down, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), QuantizedTick % SongMaps.SubdivisionToMidiTicks(ResultDivision, i) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Up, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), QuantizedTick % SongMaps.SubdivisionToMidiTicks(ResultDivision, i) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } } // Check Second Bar... for (int32 i = TickAfterFirstBar; i < TickAfterSecondBar; i += 10) { EMidiClockSubdivisionQuantization ResultDivision = EMidiClockSubdivisionQuantization::Bar; int32 QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Nearest, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), (QuantizedTick - TickAfterFirstBar) % SongMaps.SubdivisionToMidiTicks(ResultDivision, i) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Down, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), (QuantizedTick - TickAfterFirstBar) % SongMaps.SubdivisionToMidiTicks(ResultDivision, i) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Up, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), (QuantizedTick - TickAfterFirstBar) % SongMaps.SubdivisionToMidiTicks(ResultDivision, i) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } } // Check Third Bar... for (int32 i = TickAfterSecondBar; i < (TickAfterSecondBar + MidiData->TicksPerQuarterNote * 2 * 3); i += 10) { EMidiClockSubdivisionQuantization ResultDivision = EMidiClockSubdivisionQuantization::Bar; int32 QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Nearest, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), (QuantizedTick-TickAfterSecondBar) % SongMaps.SubdivisionToMidiTicks(ResultDivision, i) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Down, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), (QuantizedTick - TickAfterSecondBar) % SongMaps.SubdivisionToMidiTicks(ResultDivision, i) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } QuantizedTick = SongMaps.QuantizeTickToAnyNearestSubdivision(i, EMidiFileQuantizeDirection::Up, ResultDivision); UTEST_TRUE(TEXT("Got good quantization."), (QuantizedTick - TickAfterSecondBar) % SongMaps.SubdivisionToMidiTicks(ResultDivision, i) == 0); if (GLogQualtizationDetails) { LogQuantizationDetails(SongMaps, i, QuantizedTick, ResultDivision); } } return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST( FTestSongMapsLengthIsSubdivision, "Harmonix.Midi.SongMaps.LengthIsPerfectSubdivision", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) bool FTestSongMapsLengthIsSubdivision::RunTest(const FString&) { TSharedPtr MidiData = BuildMidiWithOneTimeSigature(4, 4); int32 TickOnDownbeatOfBar2 = MidiData->TicksPerQuarterNote * 4; MidiData->Tracks.Add(FMidiTrack("note-data")); { FMidiTrack& NoteTrack = MidiData->Tracks[1]; NoteTrack.AddEvent(FMidiEvent(0, FMidiMsg::CreateNoteOn(0, 60, 127))); NoteTrack.AddEvent(FMidiEvent(TickOnDownbeatOfBar2, FMidiMsg::CreateNoteOff(0, 60))); NoteTrack.Sort(); MidiData->ScanTracksForSongLengthChange(); } UTEST_FALSE("MIDI file is NOT a perfect subdivision", MidiData->LengthIsAPerfectSubdivision()); EMidiClockSubdivisionQuantization QuantizedDivision = EMidiClockSubdivisionQuantization::None; int32 TargetFixTick = MidiData->SongMaps.QuantizeTickToAnyNearestSubdivision(MidiData->SongMaps.GetSongLengthData().LengthTicks, EMidiFileQuantizeDirection::Nearest, QuantizedDivision); UTEST_TRUE("Quantized Length Is Correct", TargetFixTick == TickOnDownbeatOfBar2); UTEST_TRUE("Quantized Subdivision Is Correct", QuantizedDivision == EMidiClockSubdivisionQuantization::Bar); MidiData->Tracks[1] = FMidiTrack("note-data"); { FMidiTrack& NoteTrack = MidiData->Tracks[1]; NoteTrack.AddEvent(FMidiEvent(0, FMidiMsg::CreateNoteOn(0, 60, 127))); NoteTrack.AddEvent(FMidiEvent(TickOnDownbeatOfBar2 - 1, FMidiMsg::CreateNoteOff(0, 60))); NoteTrack.Sort(); MidiData->ScanTracksForSongLengthChange(); } UTEST_TRUE("MIDI file IS a perfect subdivision", MidiData->LengthIsAPerfectSubdivision()); MidiData->Tracks[1] = FMidiTrack("note-data"); { FMidiTrack& NoteTrack = MidiData->Tracks[1]; NoteTrack.AddEvent(FMidiEvent(0, FMidiMsg::CreateNoteOn(0, 60, 127))); NoteTrack.AddEvent(FMidiEvent(TickOnDownbeatOfBar2 - MidiData->SongMaps.SubdivisionToMidiTicks(EMidiClockSubdivisionQuantization::ThirtySecondNote, 0), FMidiMsg::CreateNoteOff(0, 60))); NoteTrack.Sort(); MidiData->ScanTracksForSongLengthChange(); } UTEST_FALSE("MIDI file is NOT a perfect subdivision", MidiData->LengthIsAPerfectSubdivision()); MidiData->Tracks[1] = FMidiTrack("note-data"); { FMidiTrack& NoteTrack = MidiData->Tracks[1]; NoteTrack.AddEvent(FMidiEvent(0, FMidiMsg::CreateNoteOn(0, 60, 127))); NoteTrack.AddEvent(FMidiEvent(TickOnDownbeatOfBar2 - 1 - MidiData->SongMaps.SubdivisionToMidiTicks(EMidiClockSubdivisionQuantization::ThirtySecondNote, 0), FMidiMsg::CreateNoteOff(0, 60))); NoteTrack.Sort(); MidiData->ScanTracksForSongLengthChange(); } UTEST_TRUE("MIDI file IS a perfect subdivision", MidiData->LengthIsAPerfectSubdivision()); MidiData = BuildMidiWithOneTimeSigature(7, 8); TickOnDownbeatOfBar2 = MidiData->TicksPerQuarterNote / 2 * 7; MidiData->Tracks.Add(FMidiTrack("note-data")); { FMidiTrack& NoteTrack = MidiData->Tracks[1]; NoteTrack.AddEvent(FMidiEvent(0, FMidiMsg::CreateNoteOn(0, 60, 127))); NoteTrack.AddEvent(FMidiEvent(TickOnDownbeatOfBar2, FMidiMsg::CreateNoteOff(0, 60))); NoteTrack.Sort(); MidiData->ScanTracksForSongLengthChange(); } UTEST_FALSE("MIDI file is NOT a perfect subdivision", MidiData->LengthIsAPerfectSubdivision()); QuantizedDivision = EMidiClockSubdivisionQuantization::None; TargetFixTick = MidiData->SongMaps.QuantizeTickToAnyNearestSubdivision(MidiData->SongMaps.GetSongLengthData().LengthTicks, EMidiFileQuantizeDirection::Nearest, QuantizedDivision); UTEST_TRUE("Quantized Length Is Correct", TargetFixTick == TickOnDownbeatOfBar2); UTEST_TRUE("Quantized Subdivision Is Correct", QuantizedDivision == EMidiClockSubdivisionQuantization::Bar); MidiData->Tracks[1] = FMidiTrack("note-data"); { FMidiTrack& NoteTrack = MidiData->Tracks[1]; NoteTrack.AddEvent(FMidiEvent(0, FMidiMsg::CreateNoteOn(0, 60, 127))); NoteTrack.AddEvent(FMidiEvent(TickOnDownbeatOfBar2 - 1, FMidiMsg::CreateNoteOff(0, 60))); NoteTrack.Sort(); MidiData->ScanTracksForSongLengthChange(); } UTEST_TRUE("MIDI file IS a perfect subdivision", MidiData->LengthIsAPerfectSubdivision()); return true; } } #endif