Skip to content

Instantly share code, notes, and snippets.

@nielsmh
Created March 14, 2018 20:49
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 nielsmh/e0b8372c86d9a22c84675f75af0a8d1a to your computer and use it in GitHub Desktop.
Save nielsmh/e0b8372c86d9a22c84675f75af0a8d1a to your computer and use it in GitHub Desktop.
TTD DOS music decoder v0.1a
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 &target; // 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