Skip to content

Instantly share code, notes, and snippets.

@AlexanderBrevig
Last active January 31, 2017 14:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AlexanderBrevig/3704063a2a608090384d527bb91d4ddf to your computer and use it in GitHub Desktop.
Save AlexanderBrevig/3704063a2a608090384d527bb91d4ddf to your computer and use it in GitHub Desktop.
LightMIDI
#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 &currentEvent;
}
if (currentEvent.emitAtTick <= totalTicks) {
return &currentEvent;
}
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