295 lines
14 KiB
C++
295 lines
14 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "MidiTestUtility.h"
|
|
#include "Misc/AutomationTest.h"
|
|
#include "Internationalization/Regex.h"
|
|
|
|
#if WITH_DEV_AUTOMATION_TESTS
|
|
|
|
namespace HarmonixMidiTests::ConformMidiFileLength
|
|
{
|
|
using namespace Harmonix::Testing::Utility::MidiTestUtility;
|
|
|
|
/**
|
|
* Add some events to the input midi file for testing the ConformMidiFileLength() function
|
|
*
|
|
************************
|
|
* Midi File Structure: *
|
|
************************
|
|
*
|
|
*For each Midi track / channel :
|
|
* 1 Note On / Note Off pair will be added at the 1st beat of every bar, note duration is 1 beat(TicksPerQuarter)
|
|
*For every EVEN bar number :
|
|
* 1 CC event and 1 Text event will be added at the same position as the Note On events
|
|
*For every ODD bar number :
|
|
* 1 Pitch Bend event and 1 Poly Pres event will be added at the same position as the Note On events
|
|
*If input file length(bars) is a fractional number :
|
|
* 1 Note On / Note Off pair and 1 Text event will be added to the tick position of that fractional bar length
|
|
* (Note On and Note Off events are 1 tick away so they don't end up on the same tick)
|
|
*/
|
|
void AddEventsToTestMidiFile(UMidiFile* InMidiFile, float InFileLengthBars, int32 InNumChannels, int32 InNumTracksExcludingConductorTrack, int32& RemovableNotes, int32& RemoveableControlChanges, int32& RemovablePitchAndPoly)
|
|
{
|
|
constexpr int32 DefaultNoteNumber = 60;//C4
|
|
constexpr int32 DefaultNoteVelocity = 90;
|
|
constexpr uint8 DefaultControllerID = 69; //Hold Pedal
|
|
constexpr uint8 DefaultControlValue = 120;
|
|
constexpr uint8 DefaultPitchBendValueLSB = 64;
|
|
constexpr uint8 DefaultPitchBendValueMSB = 120;
|
|
constexpr uint8 DefaultPolyPressNoteNumber = 62;
|
|
constexpr uint8 DefaultPolyPresValue = 127;
|
|
|
|
// These will be useful below...
|
|
const FSongMaps* SongMaps = InMidiFile->GetSongMaps();
|
|
|
|
RemovableNotes = 0;
|
|
RemoveableControlChanges = 0;
|
|
RemovablePitchAndPoly = 0;
|
|
|
|
// Add events to track 1 - InNumTracks, Track 0 is the conductor track
|
|
for (int32 TrackIndex = 1; TrackIndex < (InNumTracksExcludingConductorTrack + 1); ++TrackIndex)
|
|
{
|
|
FMidiTrack* CurrentTrack = InMidiFile->GetTrack(TrackIndex);
|
|
for (int32 Channel = 0; Channel < InNumChannels; ++Channel)
|
|
{
|
|
for (int32 Bar = 0; Bar < FMath::FloorToInt32(InFileLengthBars); ++Bar)
|
|
{
|
|
int32 DestinationTick = SongMaps->BarBeatTickIncludingCountInToTick(Bar, 1, 0);
|
|
int32 Duration = SongMaps->SubdivisionToMidiTicks(EMidiClockSubdivisionQuantization::Beat, DestinationTick) - 1;
|
|
AddNoteOnNoteOffPairToFile(InMidiFile, DefaultNoteNumber, DefaultNoteVelocity, TrackIndex, Channel, DestinationTick, Duration);
|
|
|
|
// Currently, Midi CC events & Midi Text events are added to bars with EVEN bar numbers,
|
|
// Midi Poly Press & Pitch Bend event are added to bars with ODD bar numbers
|
|
if (Bar % 2 == 0)
|
|
{
|
|
// Add Control Events to each channels/tracks
|
|
AddCCEventToFile(InMidiFile, DefaultControllerID, DefaultNoteNumber, TrackIndex, Channel, DestinationTick);
|
|
// Add Text events to tracks
|
|
if (Channel == 0)
|
|
{
|
|
AddTextEventToFile(InMidiFile, TEXT("TextInMidiFile"), TrackIndex, DestinationTick);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//Add pitch bend events to channels/tracks
|
|
AddPitchEventToFile(InMidiFile, DefaultPitchBendValueLSB, DefaultPitchBendValueMSB, TrackIndex, Channel, DestinationTick);
|
|
//Add Poly Pres events to channels/tracks
|
|
AddPolyPresEventToFile(InMidiFile, DefaultPolyPressNoteNumber, DefaultPolyPresValue, TrackIndex, Channel, DestinationTick);
|
|
}
|
|
}
|
|
// If bar length is a fractional number, add additional midi events after the last integer bar
|
|
if (InFileLengthBars != (int32)InFileLengthBars)
|
|
{
|
|
int32 DestinationTick = SongMaps->FractionalBarIncludingCountInToTick(InFileLengthBars);
|
|
int32 Duration = SongMaps->SubdivisionToMidiTicks(EMidiClockSubdivisionQuantization::Beat, DestinationTick) - 1;
|
|
int32 NoteDestinationTick = DestinationTick - (Duration + 1);
|
|
DestinationTick--;
|
|
|
|
// This will end up getting removed
|
|
AddNoteOnNoteOffPairToFile(InMidiFile, DefaultNoteNumber, DefaultNoteVelocity, TrackIndex, Channel, NoteDestinationTick, Duration);
|
|
RemovableNotes++;
|
|
|
|
// Add 2 CC Events (same controller ID) to test the conform function where it should remove events with the same type on the last tick
|
|
// 1 of these should be removed by the conform...
|
|
AddCCEventToFile(InMidiFile, DefaultControllerID, DefaultControlValue, TrackIndex, Channel, DestinationTick);
|
|
RemoveableControlChanges++;
|
|
AddCCEventToFile(InMidiFile, DefaultControllerID, DefaultControlValue + 1, TrackIndex, Channel, DestinationTick);
|
|
|
|
// Add 3 Pitch Bend Events to test the conform function where it should remove events with the same type on the last tick
|
|
// 2 of these should be removed by the conform...
|
|
AddPitchEventToFile(InMidiFile, DefaultPitchBendValueLSB, DefaultPitchBendValueMSB, TrackIndex, Channel, DestinationTick);
|
|
AddPitchEventToFile(InMidiFile, DefaultPitchBendValueLSB + 1, DefaultPitchBendValueMSB + 1, TrackIndex, Channel, DestinationTick);
|
|
RemovablePitchAndPoly += 2;
|
|
AddPitchEventToFile(InMidiFile, DefaultPitchBendValueLSB - 1, DefaultPitchBendValueMSB - 1, TrackIndex, Channel, DestinationTick);
|
|
|
|
// Add 4 Poly pressure Events to test the conform function where it should remove events with the same type on the last tick
|
|
// 3 of these should be removed by the conform...
|
|
AddPolyPresEventToFile(InMidiFile, DefaultPolyPressNoteNumber, DefaultPolyPresValue, TrackIndex, Channel, DestinationTick);
|
|
AddPolyPresEventToFile(InMidiFile, DefaultPolyPressNoteNumber, DefaultPolyPresValue - 1, TrackIndex, Channel, DestinationTick);
|
|
AddPolyPresEventToFile(InMidiFile, DefaultPolyPressNoteNumber, DefaultPolyPresValue - 2, TrackIndex, Channel, DestinationTick);
|
|
RemovablePitchAndPoly += 3;
|
|
AddPolyPresEventToFile(InMidiFile, DefaultPolyPressNoteNumber, DefaultPolyPresValue + 1, TrackIndex, Channel, DestinationTick);
|
|
}
|
|
}
|
|
InMidiFile->GetTrack(TrackIndex)->Sort();
|
|
}
|
|
|
|
//update file information in SongLengthData
|
|
InMidiFile->ScanTracksForSongLengthChange();
|
|
}
|
|
|
|
/**
|
|
* Check if a Midi file has excessive midi events on the last tick after its length is conformed by ROUNDING DOWN
|
|
* ConformMidiFileLength(EMidiFileLengthConformOption::RoundDown) first move all the events exceeding the last integer bar to the last tick of
|
|
* the last integer bar, and then remove Note On/Note Off pairs, CC events with the same controller ID, Poly Pres events with the same note number
|
|
* and chan pres/pitch bend events that are on the same (last) tick
|
|
* this function validates these results
|
|
*/
|
|
bool ContainsExcessiveEventsAtLastEventTick(UMidiFile* ConformedMidiFile)
|
|
{
|
|
int32 ConformedLastEventTick = ConformedMidiFile->GetLastEventTick();
|
|
|
|
for (FMidiTrack& Track : ConformedMidiFile->GetTracks())
|
|
{
|
|
const FMidiEventList& Events = Track.GetEvents();
|
|
|
|
for (int EventIndex = Events.Num() - 1; EventIndex >= 0 && Events[EventIndex].GetTick() == ConformedLastEventTick; --EventIndex)
|
|
{
|
|
using namespace Harmonix::Midi::Constants;
|
|
|
|
const FMidiEvent& Event = Events[EventIndex];
|
|
const FMidiMsg& Msg = Event.GetMsg();
|
|
|
|
if (Msg.IsStd())
|
|
{
|
|
uint8 MsgType = Msg.GetStdStatusType();
|
|
|
|
//filter Note On/Note off pairs on the last tick
|
|
if (MsgType == GNoteOff)
|
|
{
|
|
for (int32 i = EventIndex - 1; i > 0 && Events[i].GetTick() == ConformedLastEventTick; --i)
|
|
{
|
|
//check equality of note on/note off events' midi note number (data1)
|
|
if (Events[i].GetMsg().IsNoteOn() && Events[i].GetMsg().GetStdData1() == Msg.GetStdData1())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
//filter chan press/pitch bend/program change events
|
|
else if (MsgType == GChanPres || MsgType == GPitch || MsgType == GProgram)
|
|
{
|
|
//check if there exist multiple events with same status on the last tick
|
|
for (int32 i = EventIndex - 1; i >= 0 && Events[i].GetTick() == ConformedLastEventTick; --i)
|
|
{
|
|
if (Events[i].GetMsg().IsStd() &&
|
|
Events[i].GetMsg().Status == Msg.Status)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
//filter Control Change events and Poly Pres events
|
|
else if (MsgType == GControl || MsgType == GPolyPres)
|
|
{
|
|
for (int32 i = EventIndex - 1; i >= 0 && Events[i].GetTick() == ConformedLastEventTick; --i)
|
|
{
|
|
//check control change events for identical controller ID (data1) on the same tick and remove the later one
|
|
//check poly pres events for the same note number (data1) on the same tick and remove the later one
|
|
if (Events[i].GetMsg().IsStd() &&
|
|
Events[i].GetMsg().GetStdStatus() == Msg.GetStdStatus() &&
|
|
Events[i].GetMsg().Data1 == Msg.Data1)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
|
FTestConformMidiFileLength,
|
|
"Harmonix.Midi.ConformMidiFileLength",
|
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
|
bool FTestConformMidiFileLength::RunTest(const FString&)
|
|
{
|
|
//input test values for creating a midi file
|
|
constexpr float FileLengthBars = 5.5f;
|
|
constexpr int32 NumChannels = 2;
|
|
constexpr int32 NumTracksIncludingConductor = 4;
|
|
constexpr int32 NumTracksExcludingConductor = NumTracksIncludingConductor - 1;
|
|
constexpr int32 TimeSigNum5 = 5;
|
|
constexpr int32 TimeSigDenum8 = 8;
|
|
constexpr int32 TimeSigNum4 = 4;
|
|
constexpr int32 TimeSigDenum4 = 4;
|
|
constexpr int32 Tempo = 120;
|
|
|
|
|
|
// We should get a warning and no quantization if we ask to round down because that would result in a zero length file.
|
|
AddExpectedMessage(TEXT("QuantizeLengthToNearestPerfectSubdivision: Asked to Quantize file length DOWN, but that would result in a zero length midi file. NOT ALLOWED! Skipping quantization!"), ELogVerbosity::Warning);
|
|
constexpr float FileLength0Bar = 0.0;
|
|
UMidiFile* MidiFile = CreateAndInitializaMidiFile(FileLength0Bar, NumTracksIncludingConductor, TimeSigNum4, TimeSigDenum4, Tempo, true);
|
|
FSongLengthData PreQuantizeLengthData = MidiFile->GetSongMaps()->GetSongLengthData();
|
|
MidiFile->QuantizeLengthToSubdivision(EMidiFileQuantizeDirection::Down, EMidiClockSubdivisionQuantization::Bar);
|
|
FSongLengthData PostQuantizeLengthData = MidiFile->GetSongMaps()->GetSongLengthData();
|
|
|
|
int32 ExpectedLengthTickPostQuantize = 1;
|
|
int32 ExpectedLastTickPostQuantize = 0;
|
|
|
|
UTEST_TRUE("Last ticks are correct pre/post quantization.",
|
|
PreQuantizeLengthData.LastTick == 0 &&
|
|
PostQuantizeLengthData.LastTick == ExpectedLastTickPostQuantize);
|
|
|
|
UTEST_TRUE("Length ticks are correct pre/post quantization.",
|
|
PreQuantizeLengthData.LengthTicks == 1 &&
|
|
PostQuantizeLengthData.LengthTicks == ExpectedLengthTickPostQuantize);
|
|
|
|
// "Nearest" should always be up since down would result in a zero length midi file...
|
|
PreQuantizeLengthData = MidiFile->GetSongMaps()->GetSongLengthData();
|
|
MidiFile->QuantizeLengthToSubdivision(EMidiFileQuantizeDirection::Nearest, EMidiClockSubdivisionQuantization::Bar);
|
|
PostQuantizeLengthData = MidiFile->GetSongMaps()->GetSongLengthData();
|
|
|
|
ExpectedLengthTickPostQuantize = (MidiFile->GetSongMaps()->GetTicksPerQuarterNote() * 4); // <-- because we know this test has a time signature of 4/4
|
|
ExpectedLastTickPostQuantize = ExpectedLengthTickPostQuantize - 1;
|
|
|
|
UTEST_TRUE("Last ticks are correct pre/post quantization.",
|
|
PreQuantizeLengthData.LastTick == 0 &&
|
|
PostQuantizeLengthData.LastTick == ExpectedLastTickPostQuantize);
|
|
|
|
UTEST_TRUE("Length ticks are correct pre/post quantization.",
|
|
PreQuantizeLengthData.LengthTicks == 1 &&
|
|
PostQuantizeLengthData.LengthTicks == ExpectedLengthTickPostQuantize);
|
|
|
|
// "Up" should definitely work...
|
|
MidiFile = CreateAndInitializaMidiFile(FileLength0Bar, NumTracksIncludingConductor, TimeSigNum4, TimeSigDenum4, Tempo, true);
|
|
PreQuantizeLengthData = MidiFile->GetSongMaps()->GetSongLengthData();
|
|
MidiFile->QuantizeLengthToSubdivision(EMidiFileQuantizeDirection::Up, EMidiClockSubdivisionQuantization::Bar);
|
|
PostQuantizeLengthData = MidiFile->GetSongMaps()->GetSongLengthData();
|
|
|
|
ExpectedLengthTickPostQuantize = (MidiFile->GetSongMaps()->GetTicksPerQuarterNote() * 4); // <-- because we know this test has a time signature of 4/4
|
|
ExpectedLastTickPostQuantize = ExpectedLengthTickPostQuantize - 1;
|
|
|
|
UTEST_TRUE("Last ticks are correct pre/post quantization.",
|
|
PreQuantizeLengthData.LastTick == 0 &&
|
|
PostQuantizeLengthData.LastTick == ExpectedLastTickPostQuantize);
|
|
|
|
UTEST_TRUE("Length ticks are correct pre/post quantization.",
|
|
PreQuantizeLengthData.LengthTicks == 1 &&
|
|
PostQuantizeLengthData.LengthTicks == ExpectedLengthTickPostQuantize);
|
|
|
|
// Let's try a longer length in 5/8...
|
|
constexpr float TestFileLengthNearest = 5.25;
|
|
MidiFile = CreateAndInitializaMidiFile(TestFileLengthNearest, NumTracksIncludingConductor, TimeSigNum5, TimeSigDenum8, Tempo, true);
|
|
//Add some events to the midi file for testing
|
|
int32 RemoveableNotes = 0;
|
|
int32 RemoveablePicthAndPoly = 0;
|
|
int32 RemoveableControl = 0;
|
|
AddEventsToTestMidiFile(MidiFile, TestFileLengthNearest, NumChannels, NumTracksExcludingConductor, RemoveableNotes, RemoveableControl, RemoveablePicthAndPoly);
|
|
PreQuantizeLengthData = MidiFile->GetSongMaps()->GetSongLengthData();
|
|
MidiFile->QuantizeLengthToSubdivision(EMidiFileQuantizeDirection::Down, EMidiClockSubdivisionQuantization::Bar);
|
|
PostQuantizeLengthData = MidiFile->GetSongMaps()->GetSongLengthData();
|
|
|
|
ExpectedLengthTickPostQuantize = ((MidiFile->GetSongMaps()->GetTicksPerQuarterNote() / 2) * 5) * 5; // <-- because we know this test has a time signature of 4/4
|
|
ExpectedLastTickPostQuantize = ExpectedLengthTickPostQuantize - 1;
|
|
|
|
UTEST_TRUE("Last ticks are correct pre/post quantization.",
|
|
PreQuantizeLengthData.LastTick == ((((MidiFile->GetSongMaps()->GetTicksPerQuarterNote() / 2) * 5) * TestFileLengthNearest) - 1) &&
|
|
PostQuantizeLengthData.LastTick == ExpectedLastTickPostQuantize);
|
|
|
|
UTEST_TRUE("Length ticks are corrent pre/post quantization.",
|
|
PreQuantizeLengthData.LengthTicks == (((MidiFile->GetSongMaps()->GetTicksPerQuarterNote() / 2) * 5) * TestFileLengthNearest) &&
|
|
PostQuantizeLengthData.LengthTicks == ExpectedLengthTickPostQuantize);
|
|
|
|
UTEST_FALSE("No excessive events on conformed last tick.", ContainsExcessiveEventsAtLastEventTick(MidiFile));
|
|
|
|
return true;
|
|
}
|
|
|
|
}
|
|
#endif |