Last active
June 23, 2023 06:57
-
-
Save devinacker/bdc58cfdba6a1ee80449 to your computer and use it in GitHub Desktop.
.mmd to .mid converter
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
/* | |
MMD to MIDI (SMF) converter | |
(c) 2015 by Devin Acker <[email protected]> | |
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