-
-
Save nielsmh/e0b8372c86d9a22c84675f75af0a8d1a to your computer and use it in GitHub Desktop.
TTD DOS music decoder v0.1a
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
struct MpsMachine { | |
struct Channel { | |
byte cur_program; // program selected, used for velocity scaling (lookup into programvelocities array) | |
byte running_status; // last midi status code seen | |
uint16 delay; // frames until next command | |
ptrdiff_t playbackpos; // next byte to play this channel from | |
ptrdiff_t trackstart; // start position of master track | |
ptrdiff_t segmentret; // next return position after playing a segment | |
Channel() : cur_program(0xFF), running_status(0), delay(0), playbackpos(0), trackstart(0), segmentret(0) { } | |
}; | |
Channel channels[16]; | |
std::vector<ptrdiff_t> segments; // pointers into songdata to repeatable data segments | |
ptrdiff_t playbackpos; // used as extra parameter between function calls, current playback pointer for current channel | |
uint16 song_tempo; // threshold for actually playing a frame | |
int16 tempo_ticks; // ticker that increments when playing a frame, decrements before playing a frame | |
bool shouldplayflag; // not-end-of-song | |
static const byte programvelocities[128]; | |
const byte *songdata; // raw data array | |
size_t songdatalen; // length of song data | |
MidiFile ⌖ // recipient of data | |
static void AddMidiData(MidiFile::DataBlock &block, byte b1) { | |
*block.data.Append() = b1; | |
} | |
static void AddMidiData(MidiFile::DataBlock &block, byte b1, byte b2) { | |
*block.data.Append() = b1; | |
*block.data.Append() = b2; | |
} | |
static void AddMidiData(MidiFile::DataBlock &block, byte b1, byte b2, byte b3) { | |
*block.data.Append() = b1; | |
*block.data.Append() = b2; | |
*block.data.Append() = b3; | |
} | |
MpsMachine(const byte *data, size_t length, MidiFile &target) | |
: songdata(data), songdatalen(length), target(target) | |
{ | |
ptrdiff_t songptr1 = 0; | |
int loopmax; | |
int loopidx; | |
// mmusic.com 0D4Bh | |
// enumerate all callable segments | |
this->song_tempo = this->songdata[songptr1++]; | |
loopmax = this->songdata[songptr1++]; | |
for (loopidx = 0; loopidx < loopmax; loopidx++) { | |
this->segments.push_back(songptr1 + 4); | |
songptr1 += *(const int16 *)(this->songdata + songptr1); | |
} | |
// mmusic.com 0DA4 | |
// enumerate the master tracks for each channel | |
loopmax = this->songdata[songptr1++]; | |
for (loopidx = 0; loopidx < loopmax; loopidx++) { | |
byte ch = this->songdata[songptr1++]; | |
this->channels[ch].trackstart = songptr1 + 4; | |
songptr1 += *(const int16 *)(this->songdata + songptr1); | |
} | |
} | |
/** | |
* Find the next delay value in data stream. | |
* This is black magic. | |
*/ | |
uint16 FindDelay() { | |
byte b = 0; | |
uint16 res = 0; | |
do { | |
b = this->songdata[this->playbackpos++]; | |
res = (res << 7) + (b & 0x7F); | |
} while (b & 0x80); | |
return res; | |
} | |
/** | |
* Prepare for playback from the beginning | |
*/ | |
void RestartSong() { | |
for (int ch = 0; ch < 16; ch++) { | |
Channel &chandata = this->channels[ch]; | |
if (chandata.trackstart != 0) { | |
// mmusic.com 0E23 | |
this->playbackpos = chandata.trackstart; | |
chandata.delay = this->FindDelay(); | |
chandata.playbackpos = this->playbackpos; | |
} else { | |
// mmusic.com 0E6B | |
chandata.playbackpos = 0; | |
chandata.delay = 0; | |
} | |
} | |
} | |
/** | |
* Play one frame of data from one channel | |
*/ | |
uint16 PlayChannelFrame(MidiFile::DataBlock &outblock, int channel) { | |
uint16 newdelay = 0; | |
byte b1, b2; | |
Channel &chandata = this->channels[channel]; | |
do { | |
// mmusic.com 09DB | |
b1 = this->songdata[this->playbackpos++]; | |
if (b1 == 0xFE) { | |
// opcode FE | |
// jump to track segment identified by following byte | |
b1 = this->songdata[this->playbackpos++]; | |
// save position before jump | |
chandata.segmentret = this->playbackpos; | |
// perform jump and find new delay | |
this->playbackpos = this->segments[b1]; | |
newdelay = this->FindDelay(); | |
if (newdelay == 0) { | |
continue; | |
} | |
return newdelay; | |
} | |
// mmusic.com 03AC | |
if (b1 == 0xFD) { | |
// opcode FD | |
// return from segment to main track | |
this->playbackpos = chandata.segmentret; | |
chandata.segmentret = 0; | |
newdelay = this->FindDelay(); | |
if (newdelay == 0) { | |
continue; | |
} | |
return newdelay; | |
} | |
// mmusic.com 0A77 | |
if (b1 == 0xFF) { | |
// opcode FF | |
// end of song | |
this->shouldplayflag = false; | |
return 0; | |
} | |
// mmusic.com 0A87 | |
if (b1 >= 0x80) { | |
// regular MIDI status byte, save and read first parameter byte | |
chandata.running_status = b1; | |
b1 = this->songdata[this->playbackpos++]; | |
} | |
// mmusic.com 0AA8 | |
switch (chandata.running_status & 0xF0) { | |
case MIDIST_NOTEOFF: | |
case MIDIST_NOTEON: | |
b2 = this->songdata[this->playbackpos++]; | |
if (b2 != 0) { | |
// mmusic.com 0AEE | |
int16 velocity; | |
if (channel != 9) { | |
velocity = b2 * programvelocities[chandata.cur_program]; | |
} else { | |
velocity = (int16)b2 * 0x50; | |
} | |
b2 = (velocity / 128) & 0x00FF; | |
AddMidiData(outblock, MIDIST_NOTEON + channel, b1, b2); | |
} else { | |
// mmusic.com 0B3E | |
AddMidiData(outblock, MIDIST_NOTEON + channel, b1, 0); | |
} | |
break; | |
case MIDIST_CONTROLLER: | |
b2 = this->songdata[this->playbackpos++]; | |
if (b1 == 0x7E) { | |
break; | |
} | |
// mmusic.com 0BBB | |
if (b1 == 0) { | |
// mmusic.com 0BF4 | |
if (b2 != 0) { | |
this->song_tempo = ((int)b2) * 0x30 / 0x3C; | |
} | |
break; | |
} else if (b1 == 0x5B) { | |
// mmusic.com 0C0F | |
// enable reverb | |
b2 = 0x1E; | |
} | |
// mmusic.com 0C19 | |
if (b1 != 0) { | |
AddMidiData(outblock, MIDIST_CONTROLLER + channel, b1, b2); | |
} | |
break; | |
case MIDIST_PROGCHG: | |
if (b1 == 0x7E) { | |
// general midi program 7Eh = "applause"! | |
// should normally cause the playback to loop, but for decoding just end the song | |
this->shouldplayflag = false; | |
break; | |
} | |
// program 63 and 82 get sent as program 62, but provide three different base-velocity lookup values | |
chandata.cur_program = b1; | |
if (b1 == 0x57 || b1 == 0x3F) { | |
b1 = 0x3E; | |
} | |
AddMidiData(outblock, MIDIST_PROGCHG + channel, b1); | |
break; | |
case MIDIST_PITCHBEND: | |
b2 = this->songdata[this->playbackpos++]; | |
AddMidiData(outblock, MIDIST_PITCHBEND + channel, b1, b2); | |
break; | |
default: | |
break; | |
} | |
// mmusic.com 0C65 | |
newdelay = this->FindDelay(); | |
if (newdelay != 0) { | |
return newdelay; | |
} | |
} while (newdelay == 0); | |
NOT_REACHED(); | |
} | |
/** | |
* Play one frame of data into a block. | |
*/ | |
bool PlayFrame(MidiFile::DataBlock &block) { | |
// mmusic.com 0941 | |
this->tempo_ticks -= this->song_tempo; | |
if (this->tempo_ticks > 0) { | |
// not past tempo threshold yet, nothing new this frame | |
return true; | |
} | |
this->tempo_ticks += 0x94; | |
// mmusic.com 0962 | |
for (int ch = 0; ch < 16; ch++) { | |
Channel &chandata = this->channels[ch]; | |
if (chandata.playbackpos != 0) { | |
if (chandata.delay == 0) { | |
this->playbackpos = chandata.playbackpos; | |
chandata.delay = this->PlayChannelFrame(block, ch); | |
chandata.playbackpos = this->playbackpos; | |
} | |
chandata.delay--; | |
} | |
} | |
return this->shouldplayflag; | |
} | |
/** | |
* Perform playback of whole song. | |
*/ | |
bool PlayInto() { | |
this->target.tickdiv = 1; | |
this->target.tempos.push_back(MidiFile::TempoChange(0, 6500)); // 6.5 ms per frame seems mostly correct | |
// mmusic.com 0ECA | |
this->RestartSong(); | |
this->song_tempo = (int32)this->song_tempo * 0x18 / 0x3C; | |
this->tempo_ticks = this->song_tempo; | |
// mmusic.com 0EE2 | |
// insert a program change for drums (ch 10) to program 0 | |
this->target.blocks.push_back(MidiFile::DataBlock()); | |
AddMidiData(this->target.blocks.back(), MIDIST_PROGCHG+10, 0x00); | |
// end-of-song flag | |
this->shouldplayflag = true; | |
for (uint32 tick = 0; tick < 100000; tick+=1) { | |
// max 100k ticks = max 600 seconds at 6 ms/tick, to prevent endless loops | |
this->target.blocks.push_back(MidiFile::DataBlock()); | |
auto &block = this->target.blocks.back(); | |
block.ticktime = tick; | |
if (!this->PlayFrame(block)) | |
break; | |
} | |
return true; | |
} | |
}; | |
/* base note velocities for various GM programs */ | |
const byte MpsMachine::programvelocities[128] = { | |
100,100,100,100,100, 90,100,100,100,100,100, 90,100,100,100,100, | |
100,100, 85,100,100,100,100,100,100,100,100,100, 90, 90,110, 80, | |
100,100,100, 90, 70,100,100,100,100,100,100,100,100,100,100,100, | |
100,100, 90,100,100,100,100,100,100,120,100,100,100,120,100,127, | |
100,100, 90,100,100,100,100,100,100, 95,100,100,100,100,100,100, | |
100,100,100,100,100,100,100,115,100,100,100,100,100,100,100,100, | |
100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100, | |
100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100, | |
}; | |
/** | |
* Create MIDI data from song data for the original Microprose music drivers. | |
* @param data pointer to block of data | |
* @param length size of data in bytes | |
* @return true if the data could be loaded | |
*/ | |
bool MidiFile::LoadMpsData(const byte *data, size_t length) { | |
MpsMachine machine(data, length, *this); | |
return machine.PlayInto() && FixupMidiData(*this); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment