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

328 lines
16 KiB
C++

// 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<EQuartzCommandQuantization>()->GetDisplayNameTextByIndex((int32)ResultDivision).ToString(), BarIndex, BeatInBar, TickIndexInBeat);
}
TSharedPtr<FMidiFileData> BuildMidiWithOneTimeSigature(int32 Numerator, int32 Denominator)
{
TSharedPtr<FMidiFileData> MidiData = MakeShared<FMidiFileData>();
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<int32>::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<FMidiFileData> 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<FMidiFileData> 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<FMidiFileData> 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<FMidiFileData> 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<FMidiFileData> 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