Last active
January 31, 2017 14:50
-
-
Save AlexanderBrevig/3704063a2a608090384d527bb91d4ddf to your computer and use it in GitHub Desktop.
LightMIDI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <iostream> // std::cout | |
#include <fstream> // std::ifstream | |
#include <chrono> | |
#include <thread> | |
#define MIDI_DEBUG(s) (s); | |
using namespace std; | |
//expect total size and current size to be available | |
class fileinfo { | |
public: | |
uint32_t currentPosition; | |
uint32_t fileSize; | |
}; | |
namespace MidiHelper { | |
typedef uint8_t(*getchar)(fileinfo*); | |
}; | |
class MidiEvent { | |
public: | |
static const uint8_t META_PREFIX = 0xFF; | |
static const uint8_t SYSEX_PREFIX = 0xF0; | |
static const uint8_t SYSEX_SUFFIX = 0xF7; | |
void reset() { | |
deltaTick = 0; | |
eventType = EventType::NONE_EVENT; | |
dataLength = 0; | |
for (int i = 0; i < MAX_DATA_LENGTH; i++) { data[i] = 0; } | |
isConsumed = false; | |
isDone = false; | |
hasDeltaTick = false; | |
messageLength = 0; | |
metaQualifier = 0; | |
} | |
uint8_t wantChar() { | |
return !isDone; | |
} | |
void parse(MidiHelper::getchar getChar, fileinfo *info) { | |
MIDI_DEBUG(cout << "PARSE MESSAGE" << endl); | |
uint8_t ch = 0; | |
deltaTick = getVarLen(getChar, info); | |
midiId = getChar(info); | |
if (midiId == SYSEX_PREFIX || midiId == SYSEX_SUFFIX) { | |
eventType = EventType::SYSEX_EVENT; | |
dataLength = getVarLen(getChar, info); | |
for (int i = 0; i < dataLength; i++) { | |
data[i] = getChar(info); | |
} | |
MIDI_DEBUG(cout << "\t[^sysex message]" << endl); | |
} | |
else if (midiId == META_PREFIX) { | |
eventType = EventType::META_EVENT; | |
ch = getChar(info); //purge meta qualifier | |
for (int i = 0; i < META_TYPES_LENGTH; i++) { | |
if (ch == META_TYPES[i]) { | |
metaQualifier = i; | |
} | |
} | |
data[0] = ch; | |
dataLength = getVarLen(getChar, info); | |
for (int i = 0; i < dataLength; i++) { | |
data[1 + i] = getChar(info); | |
} | |
dataLength++; //account for meta type | |
MIDI_DEBUG(cout << "\t[^meta message]" << endl); | |
} | |
else { | |
eventType = EventType::MIDI_EVENT; | |
data[0] = midiId; | |
if (isEventType(MidiEventType::PC)) | |
{ | |
dataLength = 2; | |
data[1] = getChar(info); | |
MIDI_DEBUG(cout << "\t[^midi program change message]" << endl); | |
} | |
else if (isEventType(MidiEventType::AFTERTOUCH_PRESSURE)) | |
{ | |
dataLength = 2; | |
data[1] = getChar(info); | |
MIDI_DEBUG(cout << "\t[^midi pressure aftertouch message]" << endl); | |
} | |
else if (isEventType(MidiEventType::CC)) | |
{ | |
dataLength = 3; | |
data[1] = getChar(info); | |
data[2] = getChar(info); | |
MIDI_DEBUG(cout << "\t[^midi control change message]" << endl); | |
} | |
else { | |
dataLength = 2; | |
data[1] = getChar(info); | |
if (isEventType(MidiEventType::NOTE_OFF) | |
|| isEventType(MidiEventType::NOTE_ON) | |
|| isEventType(MidiEventType::AFTERTOUCH)) { | |
dataLength = 3; | |
data[2] = getChar(info); | |
MIDI_DEBUG(cout << "\t[^midi message 3]" << endl); | |
} | |
else { | |
MIDI_DEBUG(cout << "\t[^midi message 2]" << endl); | |
} | |
} | |
} | |
isDone = true; | |
} | |
/// utility for clients | |
static uint32_t getVariableLength(uint8_t *arr, uint8_t idx) { | |
uint8_t ch = 0; | |
uint32_t len = 0; | |
while (true) { | |
ch = arr[idx++]; | |
if (ch & 0x80) { | |
len = (len << 7) + (ch & 0x7F); | |
} | |
else { | |
if (len > 0) { len = len << 7; } | |
len += ch; | |
break; | |
} | |
} | |
return len; | |
} | |
bool isEventType(uint8_t eventTypeSpecifier) { | |
return (midiId & eventTypeSpecifier) == eventTypeSpecifier; | |
} | |
enum EventType { | |
NONE_EVENT, | |
MIDI_EVENT, | |
META_EVENT, | |
SYSEX_EVENT | |
}; | |
enum MidiEventType { | |
NOTE_OFF = 0x80, | |
NOTE_ON = 0x90, | |
AFTERTOUCH = 0xA0, | |
CC = 0xB0, | |
PC = 0xC0, | |
AFTERTOUCH_PRESSURE = 0xD0, | |
PITCH_CHANGE = 0xE0 | |
}; | |
enum MetaType { | |
SEQUENCE = 0x00, | |
TEXT = 0x01, | |
COPYRIGHT = 0x02, | |
NAME = 0x03, | |
LYRIC = 0x05, | |
MARKER = 0x06, | |
CUE = 0x07, | |
CHANNEL_PREFIX = 0x20, | |
EOT = 0x2F, | |
SET_TEMPO = 0x51, | |
SMTPE = 0x54, | |
TIME_SIGNATURE = 0x58, | |
KEY_SIGNATURE = 0x59, | |
SEQ_SPEC_META_EVENT = 0x7F | |
}; | |
static const uint8_t META_TYPES_LENGTH = 15; | |
static const uint8_t META_TYPES[META_TYPES_LENGTH]; | |
static const uint8_t MAX_DATA_LENGTH = 128; | |
uint8_t midiId; | |
uint8_t metaQualifier; | |
uint8_t data[MAX_DATA_LENGTH]; //figure out a max data length | |
uint8_t dataLength; | |
uint32_t deltaTick; | |
EventType eventType; | |
uint8_t isConsumed; | |
uint8_t isDone; | |
uint8_t hasDeltaTick; | |
uint32_t messageLength; | |
uint32_t emitAtTick; | |
private: | |
uint32_t getVarLen(MidiHelper::getchar getChar, fileinfo *info) { | |
uint8_t ch = 0; | |
uint32_t len = 0; | |
while (true) { | |
ch = getChar(info); | |
if (ch & 0x80) { | |
len = (len << 7) + (ch & 0x7F); | |
} | |
else { | |
if (len > 0) { len = len << 7; } | |
len += ch; | |
break; | |
} | |
} | |
return len; | |
} | |
}; | |
const uint8_t MidiEvent::META_TYPES[META_TYPES_LENGTH] = { | |
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x20, 0x2F, 0x51, 0x54, 0x58, 0x59, 0x7F | |
}; | |
class MidiParser { | |
public: | |
MidiParser(fileinfo *finfo, MidiHelper::getchar getter) : fileInfo(finfo), getChar(getter) { | |
lastEventTick = 0; | |
totalTicks = 0; | |
firstParse = true; | |
BPM = 0; | |
microsecondsPerQuarterNote = 0; | |
ticksPerQuarterNote = 0; | |
microsecondsPerTick = 0; | |
timeSignatureNumerator = 0; | |
timeSignatureDenominator = 0; | |
currentEvent.reset(); | |
} | |
void parseMThd() { | |
uint8_t mthd[14]; | |
for (int i = 0; i < 14; i++) { | |
mthd[i] = getChar(fileInfo); | |
} | |
format = (uint8_t)mthd[8] << 8 | mthd[9]; | |
tracks = (uint8_t)mthd[10] << 8 | mthd[11]; | |
ticksPerQuarterNote = (uint32_t)mthd[12] << 8 | mthd[13]; //8 here? | |
} | |
void parseMTrk() { | |
uint8_t mtrd[8]; | |
for (int i = 0; i < 8; i++) { | |
mtrd[i] = getChar(fileInfo); | |
} | |
trackLength = (((uint8_t)mtrd[4] << 24) | |
| ((uint8_t)mtrd[5] << 16) | |
| ((uint8_t)mtrd[6] << 8) | |
| ((uint8_t)mtrd[7])); | |
} | |
MidiEvent *getNextEvent(uint32_t deltaTicks) { | |
totalTicks += deltaTicks; | |
if (firstParse) { | |
parseMThd(); | |
parseMTrk(); | |
MIDI_DEBUG(cout << "ticksPerQuarterNote: " << ticksPerQuarterNote << endl); | |
MIDI_DEBUG(cout << "tracklen: " << (int)trackLength << endl); | |
firstParse = false; | |
currentEvent.isConsumed = true; //force first parse | |
} | |
if (currentEvent.isConsumed) { | |
currentEvent.reset(); | |
/// stream in a frame | |
while (currentEvent.wantChar()) { | |
currentEvent.parse(getChar, fileInfo); | |
} | |
if (currentEvent.eventType == MidiEvent::EventType::META_EVENT && currentEvent.data[0] == 0x51) { | |
hasTempo = true; | |
microsecondsPerQuarterNote = (uint32_t)((uint32_t)currentEvent.data[1] << 16) | ((uint32_t)currentEvent.data[2] << 8) | ((uint32_t)currentEvent.data[3]); | |
calculateTime(); | |
} | |
if (currentEvent.eventType == MidiEvent::EventType::META_EVENT && currentEvent.data[0] == 0x58) { | |
hasTimeSign = true; | |
timeSignatureNumerator = currentEvent.data[1]; | |
timeSignatureDenominator = pow(2, currentEvent.data[2]); | |
calculateTime(); | |
} | |
currentEvent.emitAtTick = totalTicks + currentEvent.deltaTick; | |
//return ¤tEvent; | |
} | |
if (currentEvent.emitAtTick <= totalTicks) { | |
return ¤tEvent; | |
} | |
return 0; | |
} | |
uint8_t format; | |
uint8_t tracks; | |
uint32_t trackLength; | |
uint32_t totalTicks; | |
uint8_t timeSignatureNumerator; | |
uint8_t timeSignatureDenominator; | |
uint32_t microsecondsPerQuarterNote; | |
uint32_t ticksPerQuarterNote; | |
float BPM; | |
float microsecondsPerTick; | |
private: | |
void calculateTime() { | |
if (hasTimeSign && hasTempo) { | |
microsecondsPerTick = (float)microsecondsPerQuarterNote / ticksPerQuarterNote; | |
const float kOneMinuteInMicroseconds = 60000000; | |
BPM = (kOneMinuteInMicroseconds / (float)microsecondsPerQuarterNote) * ((float)timeSignatureDenominator / (float)timeSignatureNumerator); | |
MIDI_DEBUG(cout << "BPM: " << BPM << " muS/tick " << microsecondsPerTick << endl); | |
} | |
} | |
MidiEvent currentEvent; | |
fileinfo *fileInfo; | |
MidiHelper::getchar getChar; | |
uint32_t lastEventTick; | |
uint8_t frame[4]; | |
uint8_t firstParse; | |
bool hasTimeSign = false; | |
bool hasTempo = false; | |
}; | |
///// | |
char * buffer = 0; | |
fileinfo info; | |
uint8_t getCharCallback(fileinfo* info) { | |
if (info->currentPosition < info->fileSize) { | |
uint8_t ch = buffer[info->currentPosition++]; | |
cout << "\t" << std::hex << (int)ch << endl; | |
return ch; | |
} | |
else 0; //should never happen, don't use readlength 0 | |
} | |
void printEventType(MidiEvent *midiEvent) { | |
switch (midiEvent->eventType) { | |
case MidiEvent::EventType::META_EVENT: | |
cout << "META_EVENT"; | |
if (midiEvent->data[0] == MidiEvent::MetaType::MARKER) { | |
cout << ": "; | |
for (int i = 1; i < midiEvent->dataLength; i++) { | |
cout << (char)midiEvent->data[i]; | |
} | |
} | |
cout << endl; | |
break; | |
case MidiEvent::EventType::MIDI_EVENT: | |
cout << "MIDI_EVENT" << endl; | |
cout << '\a'; | |
break; | |
case MidiEvent::EventType::SYSEX_EVENT: | |
cout << "SYSEX_EVENT" << endl; | |
break; | |
} | |
} | |
int main() { | |
Beep(440, 330); | |
int length = 0; | |
std::ifstream is("testmidi0.mid", std::ifstream::binary); | |
if (is) { | |
is.seekg(0, is.end); | |
length = is.tellg(); | |
is.seekg(0, is.beg); | |
buffer = new char[length]; | |
for (int i = 0; i < length; i++) { | |
buffer[i] = 0; | |
} | |
is.read(buffer, length); | |
is.close(); | |
} | |
info.currentPosition = 0; | |
info.fileSize = length; | |
MidiParser parser = MidiParser(&info, getCharCallback); | |
MidiEvent *midiEvent; | |
/// start by purging out every 0 | |
int i = 0; | |
while (midiEvent = parser.getNextEvent(0)) { | |
if (midiEvent->deltaTick > 0) { break; } | |
cout << "EVENT: @" << (int)midiEvent->deltaTick << " type: "; | |
printEventType(midiEvent); | |
cout << endl << "\t"; | |
for (int i = 0; i < midiEvent->dataLength; i++) { | |
cout << std::hex << (int)midiEvent->data[i] << " "; | |
} | |
cout << endl; | |
midiEvent->isConsumed = true; | |
i++; | |
if (i > 10) { | |
break; | |
} | |
} | |
//let's simulate some ticks! | |
for (int i = 0; i < parser.ticksPerQuarterNote * 100; i++) { | |
if (midiEvent = parser.getNextEvent(1)) { | |
if (midiEvent->emitAtTick <= i) { | |
cout << "EVENT: @" << (int)midiEvent->deltaTick << " type: "; | |
printEventType(midiEvent); | |
cout << endl << "\t ["; | |
for (int i = 0; i < midiEvent->dataLength; i++) { | |
cout << std::hex << (int)midiEvent->data[i] << " "; | |
} | |
cout << "]" << endl; | |
midiEvent->isConsumed = true; | |
} | |
} | |
std::this_thread::sleep_for(std::chrono::microseconds((uint32_t)parser.microsecondsPerTick)); | |
} | |
delete[] buffer; | |
system("pause"); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment