Skip to content
This repository has been archived by the owner on Mar 16, 2021. It is now read-only.

Support MIDI type 1 files #258

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 6 additions & 7 deletions source/MrsWatson.c
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ static ReturnCode setupInputSource(SampleSource inputSource) {
}

static ReturnCode setupMidiSource(MidiSource midiSource,
MidiSequence *outSequence) {
MidiSequence *outSequence,
const unsigned short midiTrack) {
if (midiSource != NULL) {
if (!midiSource->openMidiSource(midiSource)) {
logError("MIDI source '%s' could not be opened",
Expand All @@ -178,11 +179,7 @@ static ReturnCode setupMidiSource(MidiSource midiSource,
}

// Read in all events from the MIDI source
// TODO: This will not work if we want to support streaming MIDI events (ie,
// from a pipe)
*outSequence = newMidiSequence();

if (!midiSource->readMidiEvents(midiSource, *outSequence)) {
if (!midiSource->readMidiEvents(midiSource, outSequence, midiTrack)) {
logWarn("Failed reading MIDI events from source '%s'",
midiSource->sourceName->data);
return RETURN_CODE_IO_ERROR;
Expand Down Expand Up @@ -783,7 +780,9 @@ int mrsWatsonMain(ErrorReporter errorReporter, int argc, char **argv) {
freeCharString(pluginSearchRoot);

if (midiSource != NULL) {
result = setupMidiSource(midiSource, &midiSequence);
const unsigned short midiTrack = (unsigned short)programOptionsGetNumber(
programOptions, OPTION_MIDI_TRACK);
result = setupMidiSource(midiSource, &midiSequence, midiTrack);

if (result != RETURN_CODE_SUCCESS) {
logError("MIDI source could not be opened, exiting");
Expand Down
7 changes: 7 additions & 0 deletions source/MrsWatsonOptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,13 @@ input source length. Mostly useful when using internal plugins as sources.",
HAS_SHORT_FORM, kProgramOptionTypeString,
kProgramOptionArgumentTypeRequired));

programOptionsAdd(
options, newProgramOptionWithName(OPTION_MIDI_TRACK, "midi-track",
"MIDI track to read events from, when "
"reading from type 1 MIDI files.",
NO_SHORT_FORM, kProgramOptionTypeNumber,
kProgramOptionArgumentTypeRequired));

programOptionsAdd(
options,
newProgramOptionWithName(
Expand Down
1 change: 1 addition & 0 deletions source/MrsWatsonOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ typedef enum {
OPTION_LOG_LEVEL,
OPTION_MAX_TIME,
OPTION_MIDI_SOURCE,
OPTION_MIDI_TRACK,
OPTION_OUTPUT_SOURCE,
OPTION_PARAMETER,
OPTION_PLUGIN,
Expand Down
2 changes: 1 addition & 1 deletion source/midi/MidiEvent.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ MidiEvent newMidiEvent(void) {

void freeMidiEvent(MidiEvent self) {
if (self != NULL) {
if (self->eventType == MIDI_TYPE_SYSEX ||
if (self->eventType == MIDI_TYPE_SYSTEM ||
self->eventType == MIDI_TYPE_META) {
free(self->extraData);
}
Expand Down
5 changes: 3 additions & 2 deletions source/midi/MidiEvent.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@

typedef enum {
MIDI_TYPE_INVALID,
MIDI_TYPE_REGULAR,
MIDI_TYPE_SYSEX,
MIDI_TYPE_VOICE,
MIDI_TYPE_SYSTEM,
MIDI_TYPE_META,
NUM_MIDI_TYPES
} MidiEventType;
Expand All @@ -45,6 +45,7 @@ typedef struct {
byte data1;
byte data2;
byte *extraData;
size_t extraDataSize;
} MidiEventMembers;
typedef MidiEventMembers *MidiEvent;

Expand Down
3 changes: 2 additions & 1 deletion source/midi/MidiSource.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ typedef enum {
} MidiSourceType;

typedef boolByte (*OpenMidiSourceFunc)(void *);
typedef boolByte (*ReadMidiEventsFunc)(void *, MidiSequence);
typedef boolByte (*ReadMidiEventsFunc)(void *, MidiSequence *,
const unsigned short);
typedef void (*FreeMidiSourceDataFunc)(void *);

typedef struct {
Expand Down
166 changes: 130 additions & 36 deletions source/midi/MidiSourceFile.c
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ static boolByte _readMidiFileTrack(FILE *midiFile, const int trackNumber,

currentByte = trackData;
endByte = trackData + numBytes;
byte runningStatus = 0x0;

while (currentByte < endByte) {
// Unpack variable length timestamp
Expand All @@ -182,39 +183,87 @@ static boolByte _readMidiFileTrack(FILE *midiFile, const int trackNumber,
freeMidiEvent(midiEvent);
midiEvent = newMidiEvent();

switch (*currentByte) {
case 0xff:
if (*currentByte == 0xff) {
midiEvent->eventType = MIDI_TYPE_META;
currentByte++;
midiEvent->status = *(currentByte++);
numBytes = *(currentByte++);
midiEvent->extraDataSize = *(currentByte++);
midiEvent->extraData = (byte *)malloc(numBytes);
runningStatus = 0x0;

for (i = 0; i < numBytes; i++) {
for (i = 0; i < midiEvent->extraDataSize; ++i) {
midiEvent->extraData[i] = *(currentByte++);
}

break;

case 0x7f:
logUnsupportedFeature("MIDI files containing sysex events");
free(trackData);
freeMidiEvent(midiEvent);
return false;

default:
midiEvent->eventType = MIDI_TYPE_REGULAR;
} else if (*currentByte >= 0x80 && *currentByte < 0xf0) {
midiEvent->eventType = MIDI_TYPE_VOICE;
midiEvent->status = *currentByte++;
runningStatus = midiEvent->status;
midiEvent->data1 = *currentByte++;

// All regular MIDI events have 3 bytes except for program change and
// channel aftertouch
if (!((midiEvent->status & 0xf0) == 0xc0 ||
(midiEvent->status & 0xf0) == 0xd0)) {
switch (midiEvent->status & 0xf0) {
// Program change and aftertouch have only one byte. All other voice
// events should read in the next data byte.
case 0xc0:
case 0xd0:
break;
default:
midiEvent->data2 = *currentByte++;
break;
}
} else if (*currentByte >= 0xf0) {
logWarn("Ignoring MIDI system event of type 0x%x", *currentByte);
midiEvent->eventType = MIDI_TYPE_SYSTEM;
midiEvent->status = *currentByte++;
runningStatus = 0x0;

switch (midiEvent->status) {
case 0xf0: // Sysex
while (*currentByte != 0xf7) {
++currentByte;
}
break;

// System messages with no data (unlikely to be in MIDI files)
case 0xf6: // Tune Request
case 0xf8: // MIDI Clock
case 0xf9: // MIDI Tick
case 0xfa: // MIDI Start
case 0xfb: // MIDI Continue
case 0xfc: // MIDI Stop
case 0xfe: // MIDI Sense
break;

// System messages with one data byte
case 0xf1: // MTC Quarter Frame
case 0xf3: // Song Select
++currentByte;
break;

// System messages with two data bytes
case 0xf2: // Song Position Pointer
++currentByte;
++currentByte;
break;
}
numBytes = *(++currentByte);
currentByte++;
break;
} else {
logDebug("Assuming running status of type 0x%02x", runningStatus);
midiEvent->eventType = MIDI_TYPE_VOICE;
midiEvent->status = runningStatus;
midiEvent->data1 = *currentByte++;

switch (midiEvent->status & 0xf0) {
// Program change and aftertouch have only one byte. All other voice
// events should read in the next data byte.
case 0xc0:
case 0xd0:
break;
default:
midiEvent->data2 = *currentByte++;
break;
}
}

switch (divisionType) {
Expand Down Expand Up @@ -242,7 +291,8 @@ static boolByte _readMidiFileTrack(FILE *midiFile, const int trackNumber,

midiEvent->timestamp = currentTimeInSampleFrames;

if (midiEvent->eventType == MIDI_TYPE_META) {
switch (midiEvent->eventType) {
case MIDI_TYPE_META:
switch (midiEvent->status) {
case MIDI_META_TYPE_TEXT:
case MIDI_META_TYPE_COPYRIGHT:
Expand All @@ -252,35 +302,51 @@ static boolByte _readMidiFileTrack(FILE *midiFile, const int trackNumber,
case MIDI_META_TYPE_MARKER:
case MIDI_META_TYPE_CUE_POINT:

// This event type could theoretically be supported, as long as the
// plugin supports it
// These event type could theoretically be supported, so long as the
// plugin supports it. But for now, we just throw them away until such a
// plugin comes along.
case MIDI_META_TYPE_PROGRAM_NAME:
case MIDI_META_TYPE_DEVICE_NAME:
case MIDI_META_TYPE_KEY_SIGNATURE:
case MIDI_META_TYPE_PROPRIETARY:
logDebug("Ignoring MIDI meta event of type 0x%x at %ld",
midiEvent->status, midiEvent->timestamp);
if (midiEvent->extraDataSize > 0) {
CharString metaData =
newCharStringWithCapacity(midiEvent->extraDataSize + 1);
charStringCopyCString(metaData, (char *)(midiEvent->extraData));
logDebug(
"Ignoring MIDI meta event of type 0x%02x at %ld (data: '%s')",
midiEvent->status, midiEvent->timestamp, metaData->data);
}
break;

case MIDI_META_TYPE_TEMPO:
case MIDI_META_TYPE_TIME_SIGNATURE:
case MIDI_META_TYPE_TRACK_END:
logDebug("Parsed MIDI meta event of type 0x%02x at %ld",
logDebug("Adding MIDI meta event of type 0x%02x at %ld",
midiEvent->status, midiEvent->timestamp);
appendMidiEventToSequence(midiSequence, midiEvent);
midiEvent = NULL;
break;

default:
logWarn("Ignoring MIDI meta event of type 0x%x at %ld",
logWarn("Ignoring unknown MIDI meta event of type 0x%02x at %ld",
midiEvent->status, midiEvent->timestamp);
break;
}
} else {
logDebug("MIDI event of type 0x%02x parsed at %ld", midiEvent->status,
break;
case MIDI_TYPE_VOICE:
logDebug("Adding MIDI voice event (0x%02x, 0x%02x, 0x%02x) at %ld",
midiEvent->status, midiEvent->data1, midiEvent->data2,
midiEvent->timestamp);
appendMidiEventToSequence(midiSequence, midiEvent);
midiEvent = NULL;
break;
case MIDI_TYPE_SYSTEM:
logInfo("Ignoring MIDI system event of type 0x%02x", midiEvent->status);
break;
default:
logInternalError("Unhandled MIDI event type %d", midiEvent->eventType);
break;
}
}

Expand All @@ -290,7 +356,8 @@ static boolByte _readMidiFileTrack(FILE *midiFile, const int trackNumber,
}

static boolByte _readMidiEventsFile(void *midiSourcePtr,
MidiSequence midiSequence) {
MidiSequence *midiSequence,
const unsigned short midiTrack) {
MidiSource midiSource = (MidiSource)midiSourcePtr;
MidiSourceFileData extraData = (MidiSourceFileData)(midiSource->extraData);
unsigned short formatType, numTracks, timeDivision = 0;
Expand All @@ -301,12 +368,30 @@ static boolByte _readMidiEventsFile(void *midiSourcePtr,
return false;
}

if (formatType != 0) {
logUnsupportedFeature("MIDI file types other than 0");
switch (formatType) {
case 0:
if (numTracks != 1) {
logError("Invalid MIDI file '%s' is of type 0, but has %d tracks",
midiSource->sourceName->data, numTracks);
return false;
} else if (midiTrack > 0) {
logError("MIDI file '%s' is of type 0, but requested to read track %d",
midiSource->sourceName->data, midiTrack);
return false;
}
break;
case 1:
if (midiTrack >= numTracks) {
logError("Cannot read track %d from MIDI file '%s', which has %d tracks",
midiTrack, midiSource->sourceName->data, numTracks);
return false;
}
break;
case 2:
logUnsupportedFeature("MIDI files of type 2");
return false;
} else if (formatType == 0 && numTracks != 1) {
logError("MIDI file '%s' is of type 0, but contains %d tracks",
midiSource->sourceName->data, numTracks);
default:
logError("MIDI file is of invalid type %d", formatType);
return false;
}

Expand All @@ -324,13 +409,22 @@ static boolByte _readMidiEventsFile(void *midiSourcePtr,
formatType, numTracks, timeDivision, extraData->divisionType);

for (track = 0; track < numTracks; track++) {
MidiSequence readSequence = newMidiSequence();
logDebug("Reading MIDI track %d", track);
if (!_readMidiFileTrack(extraData->fileHandle, track, timeDivision,
extraData->divisionType, midiSequence)) {
extraData->divisionType, readSequence)) {
return false;
} else {
if (track == midiTrack) {
*midiSequence = readSequence;
return true;
} else {
freeMidiSequence(readSequence);
}
}
}

return true;
return false;
}

static void _freeMidiEventsFile(void *midiSourceDataPtr) {
Expand Down
4 changes: 2 additions & 2 deletions source/plugin/PluginVst2x.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,7 @@ static void _processAudioVst2xPlugin(void *pluginPtr, SampleBuffer inputs,
static void _fillVstMidiEvent(const MidiEvent midiEvent,
VstMidiEvent *vstMidiEvent) {
switch (midiEvent->eventType) {
case MIDI_TYPE_REGULAR:
case MIDI_TYPE_VOICE:
vstMidiEvent->type = kVstMidiType;
vstMidiEvent->byteSize = sizeof(VstMidiEvent);
vstMidiEvent->deltaFrames = (VstInt32)midiEvent->deltaFrames;
Expand All @@ -770,7 +770,7 @@ static void _fillVstMidiEvent(const MidiEvent midiEvent,
vstMidiEvent->reserved2 = 0;
break;

case MIDI_TYPE_SYSEX:
case MIDI_TYPE_SYSTEM:
logUnsupportedFeature("VST2.x plugin sysex messages");
break;

Expand Down
15 changes: 15 additions & 0 deletions test/IntegrationTests.c
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,19 @@ static int _testProcessWithVstxsynthPlugin(const char *testName,
return result;
}

static int _testProcessMidiType1File(const char *testName,
const CharString applicationPath,
const CharString resourcesPath) {
CharString midiFile =
getTestResourcePath(resourcesPath, "midi", "children.mid");
int result = runIntegrationTest(
testName, buildTestArgumentString(
"--plugin vstxsynth --midi-file \"%s\" --midi-track 4", midiFile->data),
RETURN_CODE_SUCCESS, kTestOutputPcm, applicationPath, resourcesPath);
freeCharString(midiFile);
return result;
}

static int _testProcessEffectChain(const char *testName,
const CharString applicationPath,
const CharString resourcesPath) {
Expand Down Expand Up @@ -968,6 +981,8 @@ TestSuite addIntegrationTests(File mrsWatsonExePath, File resourcesPath) {
_testProcessWithAgainPlugin);
addTestWithPaths(testSuite, "Process MIDI with vstxsynth plugin",
_testProcessWithVstxsynthPlugin);
addTestWithPaths(testSuite, "Process MIDI type 1 file",
_testProcessMidiType1File);
addTestWithPaths(testSuite, "Process effect chain", _testProcessEffectChain);
addTestWithPaths(testSuite, "Load FXP preset in VST",
_testLoadFxpPresetInVst);
Expand Down