Skip to content

Instantly share code, notes, and snippets.

@devinacker
Last active June 23, 2023 06:57
Show Gist options
  • Save devinacker/bdc58cfdba6a1ee80449 to your computer and use it in GitHub Desktop.
Save devinacker/bdc58cfdba6a1ee80449 to your computer and use it in GitHub Desktop.
.mmd to .mid converter
/*
MMD to MIDI (SMF) converter
(c) 2015 by Devin Acker <d@revenant1.net>
No sysex, no tempo slides, but it loops Touhou music!
Licensed under WTFPL:
http://www.wtfpl.net/txt/copying/
*/
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <cstdlib>
#include <vector>
#include <map>
#include <stack>
#ifdef DEBUG_OUT
#define debug(...) printf(__VA_ARGS__)
#else
#define debug(...)
#endif
int convert(FILE*, FILE*);
int main(int argc, char **argv) {
printf("MMD to SMF converter by Devin Acker (c) 2015\n");
if (argc < 2) {
printf("usage: %s infile\n", argv[0]);
exit(-1);
}
char *inpath = argv[1];
FILE *infile = fopen(inpath, "rb");
if (!infile) {
fprintf(stderr, "unable to open %s\n", inpath);
exit(-1);
}
char *ext = strrchr(inpath, '.');
if (ext) {
*ext = '\0';
}
char *outpath;
if (argc < 3) {
outpath = (char*)calloc(5 + strlen(inpath), 1);
sprintf(outpath, "%s.mid", inpath);
} else {
outpath = strdup(argv[2]);
}
FILE *outfile = fopen(outpath, "wb");
if (!outfile) {
fprintf(stderr, "unable to open %s\n", outpath);
exit(-1);
}
free(outpath);
int result = convert(infile, outfile);
fclose(infile);
fclose(outfile);
return result;
}
void processLength(std::vector<uint8_t>& trackData, int delay) {
if (delay >= 1<<21)
trackData.push_back((delay >> 21) | 0x80);
if (delay >= 1<<14)
trackData.push_back((delay >> 14) | 0x80);
if (delay >= 1<<7)
trackData.push_back((delay >> 7) | 0x80);
trackData.push_back(delay & 0x7f);
}
void processMarker(std::vector<uint8_t>& trackData, int delay, const char *text) {
int len = strlen(text);
processLength(trackData, delay);
trackData.push_back(0xFF);
trackData.push_back(6);
processLength(trackData, len);
for (int i = 0; i < len; i++)
trackData.push_back(text[i]);
}
// Check all currently playing notes on the track, update their lengths, write key-off
int processTicks(std::multimap<uint8_t, uint8_t>& keys, int ticks, std::vector<uint8_t>& trackData, uint8_t channel) {
int lastDelay = 0;
auto begin = keys.begin();
auto end = keys.end();
std::multimap<uint8_t, uint8_t> newKeys;
for (auto i = begin; i != end; i++) {
int length = i->first;
uint8_t note = i->second;
// time to note off?
if (length <= ticks) {
int delay = length - lastDelay;
processLength(trackData, delay);
trackData.push_back(0x80 | channel);
trackData.push_back(note);
trackData.push_back(0);
lastDelay = length;
}
// save for next time with updated note length
else {
newKeys.insert(std::pair<uint8_t, uint8_t>(length - ticks, note));
}
}
keys = newKeys;
return ticks - lastDelay;
}
void writeTrack(FILE *outfile, std::vector<uint8_t>& trackData) {
if (!outfile) return;
uint8_t buf[4];
fwrite("MTrk", 1, 4, outfile);
uint32_t size = trackData.size();
buf[0] = size >> 24;
buf[1] = size >> 16;
buf[2] = size >> 8;
buf[3] = size >> 0;
fwrite(buf, 1, 4, outfile);
fwrite(&trackData.front(), 1, size, outfile);
}
int convert(FILE *infile, FILE *outfile) {
uint8_t bpm, transpose;
uint16_t offset;
uint8_t key, channel;
int totalTime = 0;
// do a first pass over the file to measure the song length
if (outfile) {
totalTime = convert(infile, NULL);
debug(" song length = %d\n", totalTime);
}
// current track data
std::vector<uint8_t> masterData;
std::vector<uint8_t> trackData[16];
// maps note lengths to keys
std::multimap<uint8_t, uint8_t> noteTime;
// stores loop start points
std::stack<long> loopPoints;
// and number of loop times
std::stack<int> loopCounts;
fseek(infile, 0, SEEK_SET);
fread(&bpm, 1, 1, infile);
fread(&transpose, 1, 1, infile);
if (transpose > 36 || transpose < -36)
transpose = 0;
// write MIDI header (always has 17 tracks and 48 ticks/beat)
if (outfile) fwrite("MThd\0\0\0\6\0\1\0\x11\0\x30", 1, 14, outfile);
uint64_t tempo = 60000000 / bpm;
// prepare tempo event
processLength(masterData, 0);
masterData.push_back(0xFF);
masterData.push_back(0x51);
masterData.push_back(0x3);
masterData.push_back(tempo >> 16);
masterData.push_back(tempo >> 8);
masterData.push_back(tempo >> 0);
for (int i = 0; i < 16; i++) {
fseek(infile, 2 + 4*i, SEEK_SET);
fread(&offset, 1, 2, infile);
fread(&key, 1, 1, infile);
fread(&channel, 1, 1, infile);
if(key > 127) {
key = 0;
} else {
if(key > 64)
key -= 128;
key += transpose;
}
// debug("track %d: channel %d, key %d\n", i, channel, key);
fseek(infile, offset, SEEK_SET);
uint8_t buf[4] = {0};
uint8_t cmd;
int delay = 0;
int absTime = 0;
int loopdepth = 0;
noteTime.clear();
while (!loopPoints.empty()) loopPoints.pop();
// write empty track name
processLength(trackData[i], 0);
trackData[i].push_back(0xFF);
trackData[i].push_back(3);
trackData[i].push_back(1);
trackData[i].push_back(0);
// skip loop if channel number is invalid
while (channel < 16) {
fread(&cmd, 1, 1, infile);
// TODO: support sysex here?
if ((cmd & 0xF0) == 0x80) {
if (cmd & 0x08) fread(buf, 1, 1, infile);
if (cmd & 0x04) fread(buf+1, 1, 1, infile);
if (cmd & 0x02) fread(buf+2, 1, 1, infile);
if (cmd & 0x01) fread(buf+3, 1, 1, infile);
} else {
buf[0] = cmd;
fread(buf+1, 1, 1, infile);
fread(buf+2, 1, 1, infile);
fread(buf+3, 1, 1, infile);
}
for (int j = 0; outfile && j < loopdepth; j++) {
// debug("\t");
}
// end
// (if writing to a file, also end if the indefinite looping has exceeded the
// calculated length of the song)
if ((outfile && (absTime + delay >= totalTime)) || buf[0] == 0xFE) {
absTime += delay;
delay = processTicks(noteTime, delay, trackData[i], channel);
// if (outfile) debug("\n");
break;
}
// MIDI note
else if (buf[0] < 0x80) {
absTime += delay;
delay = processTicks(noteTime, delay, trackData[i], channel);
uint8_t note = (buf[0] + key) & 0x7f;
uint8_t length = buf[2];
uint8_t velocity = buf[3];
// if (outfile) debug(" delay %d, note 0x%02x, length %d, velocity %d\n", delay, (uint8_t)(buf[0] + transpose), buf[2], buf[3]);
bool gate = true;
// erase existing time for this note if it was already playing
for (auto it = noteTime.begin(); it != noteTime.end(); it++) {
if (it->second == note) {
noteTime.erase(it);
// note was already playing; extend it instead of playing again
gate = false;
break;
}
}
// add note-on command (or aftertouch command as a hack to maintain timing)
processLength(trackData[i], delay);
trackData[i].push_back((gate ? 0x90 : 0xa0) | channel);
trackData[i].push_back(note);
trackData[i].push_back(velocity);
delay = buf[1];
noteTime.insert(std::pair<uint8_t, uint8_t>(length, note));
}
// tempo change
else if (buf[0] == 0xE7) {
absTime += delay;
delay = processTicks(noteTime, delay, trackData[i], channel);
// if (outfile) debug("tempo %02x %02x\n", buf[2], buf[3]);
if (buf[3] && outfile)
printf(" warning: tempo ramp at %d ticks not supported\n", absTime + delay);
// stole this part from timidity++
// byte 2 is tempo setting, byte 3 is tempo ramping amount (not supported here)
tempo = ((unsigned)64 * 60000000) / (bpm * buf[2]);
processLength(trackData[i], delay);
trackData[i].push_back(0xFF);
trackData[i].push_back(0x51);
trackData[i].push_back(0x3);
trackData[i].push_back(tempo >> 16);
trackData[i].push_back(tempo >> 8);
trackData[i].push_back(tempo >> 0);
delay = buf[1];
}
// other MIDI message
else if ((buf[0] & 0xF8) == 0xE8) {
absTime += delay;
delay = processTicks(noteTime, delay, trackData[i], channel);
uint8_t msg = buf[0] & 0xF;
// if (outfile) debug(" delay %d, midi message %X, %d, %d\n", delay, msg, buf[2], buf[3]);
processLength(trackData[i], delay);
trackData[i].push_back((msg << 4) | channel);
trackData[i].push_back(buf[2]);
// no second byte for program change or aftertouch
if (msg != 0xC && msg != 0xD)
trackData[i].push_back(buf[3]);
delay = buf[1];
}
// loop start
else if (buf[0] == 0xF9) {
// if (outfile) debug(" loop start\n");
if (!loopdepth && (absTime + delay > 0)) {
// debug(" track %d loop start at absolute time %d\n", i, absTime + delay);
absTime += delay;
delay = processTicks(noteTime, delay, trackData[i], channel);
// insert a cue point here
processMarker(trackData[i], delay, "loopStart");
delay = 0;
}
loopdepth++;
loopPoints.push(ftell(infile));
loopCounts.push(0);
}
// loop end
else if (buf[0] == 0xF8) {
if (buf[1]) {
// if (outfile) debug(" \n", buf[1]);
if (++(loopCounts.top()) >= buf[1]) {
loopPoints.pop();
loopCounts.pop();
loopdepth--;
} else {
// repeat loop
fseek(infile, loopPoints.top(), SEEK_SET);
// reset buffer
buf[0] = 0xF9;
buf[1] = 0;
buf[2] = 0;
buf[3] = 0;
}
} else if (outfile) {
// if (outfile) debug(" repeat indefinitely\n");
// repeat loop
fseek(infile, loopPoints.top(), SEEK_SET);
// reset buffer
buf[0] = 0xF9;
buf[1] = 0;
buf[2] = 0;
buf[3] = 0;
}
// if (!loopdepth)
// debug(" track %d loop end at absolute time %d\n", i, absTime + delay);
}
else {
fprintf(stderr, "Unrecognized command: %02x %02x %02x %02x\n", buf[0], buf[1], buf[2], buf[3]);
// return -1;
}
}
// add end-of-track event
processLength(trackData[i], delay);
trackData[i].push_back(0xFF);
trackData[i].push_back(0x2F);
trackData[i].push_back(0);
// debug(" track %d ended at absolute time %d\n", i, absTime);
totalTime = std::max(totalTime, absTime);
}
// if no output file (i.e. we were just doing timing) then return the time
// otherwise return OK status
if (!outfile)
return totalTime;
// write song title
std::vector<char> title;
char ch;
fseek(infile, 0x50, SEEK_SET);
do {
fread(&ch, 1, 1, infile);
title.push_back(ch);
} while (ch);
processLength(masterData, 0);
masterData.push_back(0xFF);
masterData.push_back(3);
processLength(masterData, title.size());
for (int i = 0; i < title.size(); i++)
masterData.push_back(title[i]);
// add end-of-track event to master track
processLength(masterData, 0);
masterData.push_back(0xFF);
masterData.push_back(0x2F);
masterData.push_back(0);
// write all tracks now
writeTrack(outfile, masterData);
for (int i = 0; i < 16; i++)
writeTrack(outfile, trackData[i]);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment