Permalink
Cannot retrieve contributors at this time
399 lines (375 sloc)
12.4 KB
| /** | |
| * @file dro2txt.cpp | |
| * @brief Program to convert DOSBox .dro files into ASCII text files. | |
| * | |
| * This program will create text files describing the events in a .dro capture, | |
| * in a format suitable for comparing with the 'diff' command. The idea is to | |
| * capture an original song being played by a game in DOSBox, as well as | |
| * generating a .dro by converting the same song with libgamemusic. The two | |
| * .dro files can then be converted into text files and compared using diff, | |
| * which will indicate whether the libgamemusic converter is faithfully | |
| * reproducing the same output as the game. | |
| * | |
| * Note that the text is not produced at the register level, because many | |
| * registers can be written in an arbitrary order while producing the same | |
| * output. Thus the text output should only reflect what is heard, meaning two | |
| * very different .dro files can still compare as identical if they both sound | |
| * exactly the same, despite being very different at the byte level. | |
| * | |
| * Copyright (C) 2010-2015 Adam Nielsen <malvineous@shikadi.net> | |
| * | |
| * This program is free software: you can redistribute it and/or modify | |
| * it under the terms of the GNU General Public License as published by | |
| * the Free Software Foundation, either version 3 of the License, or | |
| * (at your option) any later version. | |
| * | |
| * This program is distributed in the hope that it will be useful, | |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| * GNU General Public License for more details. | |
| * | |
| * You should have received a copy of the GNU General Public License | |
| * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
| */ | |
| #include <iostream> | |
| #include <iomanip> | |
| #include <camoto/iostream_helpers.hpp> | |
| #include <camoto/stream_file.hpp> | |
| #include <camoto/gamemusic.hpp> | |
| using namespace camoto; | |
| namespace gm = camoto::gamemusic; | |
| inline const char *percName(int c) | |
| { | |
| switch (c) { | |
| case 0: return "HH"; | |
| case 1: return "CY"; | |
| case 2: return "TT"; | |
| case 3: return "SD"; | |
| case 4: return "BD"; | |
| } | |
| return "??"; | |
| } | |
| void printOp(uint8_t *oplState, int offset) | |
| { | |
| std::cout << std::hex | |
| << 0x20 + offset << '=' << std::setw(2) << (int)oplState[0x20 + offset] << ',' | |
| << 0x40 + offset << '=' << std::setw(2) << (int)oplState[0x40 + offset] << ',' | |
| << 0x60 + offset << '=' << std::setw(2) << (int)oplState[0x60 + offset] << ',' | |
| << 0x80 + offset << '=' << std::setw(2) << (int)oplState[0x80 + offset] << ',' | |
| << 0xE0 + offset << '=' << std::setw(2) << (int)oplState[0xE0 + offset]; | |
| return; | |
| } | |
| #define BIT_TOGGLED(b, v) ((o[b] ^ n[b]) & (v)) | |
| #define BIT_STATE(b, v) (n[b] & (v)) | |
| bool diffGlobalState(uint8_t *o, uint8_t *n, int chip) | |
| { | |
| bool sync = false; | |
| if (BIT_TOGGLED(0x01, 0x20)) { | |
| // WSEnable toggled | |
| std::cout << "Extended wavesel mode " << (BIT_STATE(0x01, 0x20) ? "enabled" : "disabled") << "\n"; | |
| sync = true; | |
| } | |
| if (chip == 1) { | |
| if (BIT_TOGGLED(0x04, 0x01)) { | |
| std::cout << "4-OP 0-3 " << (BIT_STATE(0x04, 0x01) ? "enabled" : "disabled") << "\n"; | |
| sync = true; | |
| } | |
| if (BIT_TOGGLED(0x04, 0x02)) { | |
| std::cout << "4-OP 1-4 " << (BIT_STATE(0x04, 0x02) ? "enabled" : "disabled") << "\n"; | |
| sync = true; | |
| } | |
| if (BIT_TOGGLED(0x04, 0x04)) { | |
| std::cout << "4-OP 2-5 " << (BIT_STATE(0x04, 0x04) ? "enabled" : "disabled") << "\n"; | |
| sync = true; | |
| } | |
| if (BIT_TOGGLED(0x04, 0x08)) { | |
| std::cout << "4-OP 9-C " << (BIT_STATE(0x04, 0x08) ? "enabled" : "disabled") << "\n"; | |
| sync = true; | |
| } | |
| if (BIT_TOGGLED(0x04, 0x10)) { | |
| std::cout << "4-OP A-D " << (BIT_STATE(0x04, 0x10) ? "enabled" : "disabled") << "\n"; | |
| sync = true; | |
| } | |
| if (BIT_TOGGLED(0x04, 0x20)) { | |
| std::cout << "4-OP B-E " << (BIT_STATE(0x04, 0x20) ? "enabled" : "disabled") << "\n"; | |
| sync = true; | |
| } | |
| } else { | |
| if (BIT_TOGGLED(0x04, 0x01)) { | |
| std::cout << "T1 " << (BIT_STATE(0x04, 0x01) ? "start" : "stop") << "\n"; | |
| sync = true; | |
| } | |
| if (BIT_TOGGLED(0x04, 0x02)) { | |
| std::cout << "T2 " << (BIT_STATE(0x04, 0x02) ? "start" : "stop") << "\n"; | |
| sync = true; | |
| } | |
| if (BIT_TOGGLED(0x04, 0x20)) { | |
| std::cout << "T1 " << (BIT_STATE(0x04, 0x20) ? "masked" : "unmasked") << "\n"; | |
| sync = true; | |
| } | |
| if (BIT_TOGGLED(0x04, 0x40)) { | |
| std::cout << "T2 " << (BIT_STATE(0x04, 0x40) ? "masked" : "unmasked") << "\n"; | |
| sync = true; | |
| } | |
| if (BIT_TOGGLED(0x04, 0x80)) { | |
| std::cout << "IRQ reset " << (BIT_STATE(0x04, 0x80) ? "(set)" : "(unset)") << "\n"; | |
| sync = true; | |
| } | |
| } | |
| if (BIT_TOGGLED(0x05, 0x01)) { | |
| // OPL3 enabled/disabled | |
| std::cout << "OPL3 mode " << (BIT_STATE(0x05, 0x01) ? "enabled" : "disabled"); | |
| if (chip != 1) { | |
| std::cout << " but on wrong chip index (so no effect)"; | |
| } | |
| std::cout << "\n"; | |
| sync = true; | |
| } | |
| if (BIT_TOGGLED(0x08, 0x80)) { | |
| std::cout << "CSW mode " << (BIT_STATE(0x08, 0x80) ? "enabled" : "disabled") << "\n"; | |
| sync = true; | |
| } | |
| if (BIT_TOGGLED(0x08, 0x40)) { | |
| std::cout << "NOTE-SEL mode " << (BIT_STATE(0x08, 0x40) ? "enabled" : "disabled") << "\n"; | |
| sync = true; | |
| } | |
| if (BIT_TOGGLED(0xBD, 0x80)) { | |
| std::cout << "Deep tremolo " << (BIT_STATE(0xBD, 0x80) ? "enabled" : "disabled") << "\n"; | |
| sync = true; | |
| } | |
| if (BIT_TOGGLED(0xBD, 0x40)) { | |
| std::cout << "Deep vibrato " << (BIT_STATE(0xBD, 0x40) ? "enabled" : "disabled") << "\n"; | |
| sync = true; | |
| } | |
| if (BIT_TOGGLED(0xBD, 0x20)) { | |
| std::cout << "Rhythm mode " << (BIT_STATE(0xBD, 0x20) ? "enabled" : "disabled") << "\n"; | |
| sync = true; | |
| } | |
| return sync; | |
| } | |
| bool isOpChanged(uint8_t *o, uint8_t *n, int d) | |
| { | |
| return | |
| (o[0x20 | d] ^ n[0x20 | d]) | | |
| (o[0x40 | d] ^ n[0x40 | d]) | | |
| (o[0x60 | d] ^ n[0x60 | d]) | | |
| (o[0x80 | d] ^ n[0x80 | d]) | | |
| (o[0xE0 | d] ^ n[0xE0 | d]) | |
| ; | |
| } | |
| #define PRINT_DELAY \ | |
| if (nextDelay) { \ | |
| std::cout << "Delay for " << std::dec << nextDelay << "ms\n"; \ | |
| nextDelay = 0; \ | |
| } | |
| bool diffChannelState(uint8_t *o, uint8_t *n, int c, int chip, unsigned long& nextDelay) | |
| { | |
| int dm = OPLOFFSET_MOD(c), dc = OPLOFFSET_CAR(c); | |
| bool patchChanged = | |
| isOpChanged(o, n, dm) || | |
| isOpChanged(o, n, dc) || | |
| (o[0xC0 | c] ^ n[0xC0 | c]); | |
| bool freqChanged = (o[0xA0 | c] ^ n[0xA0 | c]) || (o[0xB0 | c] ^ n[0xB0 | c]); | |
| bool keyStayingOn = n[0xB0 | c] & OPLBIT_KEYON; | |
| bool newKeyOn = (!(o[0xB0 | c] & OPLBIT_KEYON)) && (n[0xB0 | c] & OPLBIT_KEYON); | |
| bool sync = false; | |
| if (keyStayingOn && patchChanged) { | |
| PRINT_DELAY; | |
| std::cout << "Channel " << std::dec << c+1; | |
| if (chip > 0) std::cout << 'b'; | |
| std::cout << " patch: "; | |
| printOp(n, dm); | |
| std::cout << ' '; | |
| printOp(n, dc); | |
| std::cout << ' ' << std::hex << 0xC0 + c << '=' << std::setw(2) << | |
| (int)n[0xC0 | c] << "\n"; | |
| sync = true; | |
| } | |
| if (newKeyOn || (keyStayingOn && freqChanged)) { | |
| PRINT_DELAY; | |
| int fnum = n[0xA0 | c] | ((n[0xB0 | c] & 0x03) << 8); | |
| int block = (n[0xB0 | c] >> 2) & 7; | |
| int milliHertz = gm::fnumToMilliHertz(fnum, block, 49716); | |
| std::cout << "Channel " << std::dec << c+1 << " " | |
| << (newKeyOn ? " on" : "bend") << " @ " << | |
| block << '/' << std::hex << std::setw(3) << fnum << " = " << | |
| std::setw(7) << std::setfill(' ') << std::dec << | |
| milliHertz << " mHz" << std::setfill('0') << | |
| "\n"; | |
| sync = true; | |
| } | |
| return sync; | |
| } | |
| bool diffPercState(uint8_t *o, uint8_t *n, int p, int chip, unsigned long& nextDelay) | |
| { | |
| assert(p < 5); | |
| int bit = 1 << p; | |
| bool newKeyOn = (!(o[0xBD] & bit)) && (n[0xBD] & bit); | |
| int c; | |
| bool bm, bc; | |
| switch (p) { | |
| case 0: c = 7; bm = true; bc = false; break; // HH | |
| case 1: c = 8; bm = false; bc = true; break; // CY | |
| case 2: c = 8; bm = true; bc = false; break; // TT | |
| case 3: c = 7; bm = false; bc = true; break; // SD | |
| case 4: c = 6; bm = true; bc = true; break; // BD | |
| default: return false; | |
| } | |
| bool patchChanged = | |
| (bm && isOpChanged(o, n, OPLOFFSET_MOD(c))) || | |
| (bc && isOpChanged(o, n, OPLOFFSET_CAR(c))) || | |
| (o[0xC0 | c] ^ n[0xC0 | c]); | |
| bool freqChanged = (o[0xA0 | c] ^ n[0xA0 | c]) || (o[0xB0 | c] ^ n[0xB0 | c]); | |
| bool keyStayingOn = n[0xBD] & bit; | |
| bool sync = false; | |
| if (keyStayingOn && patchChanged) { | |
| PRINT_DELAY; | |
| std::cout << "Perc " << percName(p); | |
| if (chip > 0) std::cout << 'b'; | |
| std::cout << " patch: "; | |
| if (bm) { | |
| printOp(n, OPLOFFSET_MOD(c)); | |
| std::cout << ' '; | |
| } else std::cout << std::setfill(' ') << std::setw(30) << ' ' << std::setfill('0'); | |
| if (bc) { | |
| printOp(n, OPLOFFSET_CAR(c)); | |
| std::cout << ' '; | |
| } else std::cout << std::setfill(' ') << std::setw(30) << ' ' << std::setfill('0'); | |
| std::cout << 0xC0 + c << '=' << std::setw(2) << | |
| (int)n[0xC0 | c] << "\n"; | |
| sync = true; | |
| } | |
| if (newKeyOn || (keyStayingOn && freqChanged)) { | |
| PRINT_DELAY; | |
| int fnum = n[0xA0 | c] | ((n[0xB0 | c] & 0x03) << 8); | |
| int block = (n[0xB0 | c] >> 2) & 7; | |
| int milliHertz = gm::fnumToMilliHertz(fnum, block, 49716); | |
| std::cout << "Perc " << percName(p) << " " << (newKeyOn ? " on" : "bend") | |
| << " @ " << | |
| block << '/' << std::hex << std::setw(3) << fnum << " = " << | |
| std::setw(7) << std::setfill(' ') << std::dec << | |
| milliHertz << " mHz" << std::setfill('0') << | |
| "\n"; | |
| sync = true; | |
| } | |
| return sync; | |
| } | |
| int main(void) | |
| { | |
| auto cin = stream::open_stdin(); | |
| uint8_t cmdShortDelay, cmdLongDelay, lenCodemap; | |
| try { | |
| char sig[8]; | |
| cin->read(sig, 8); | |
| if (strncmp(sig, "DBRAWOPL", 8) != 0) { | |
| std::cerr << "ERROR: Input file is not in DOSBox .dro format." << std::endl; | |
| return 1; | |
| } | |
| uint16_t verMajor, verMinor; | |
| *cin >> u16le(verMajor) >> u16le(verMinor); | |
| if ((verMajor != 2) || (verMinor != 0)) { | |
| std::cerr << "ERROR: Only DOSBox .dro version 2.0 files are supported." << std::endl; | |
| return 1; | |
| } | |
| cin->seekg(11, stream::cur); | |
| *cin | |
| >> u8(cmdShortDelay) | |
| >> u8(cmdLongDelay) | |
| >> u8(lenCodemap) | |
| ; | |
| } catch (const stream::incomplete_read&) { | |
| std::cerr << "ERROR: Input file is not in DOSBox .dro format (short read)." | |
| << std::endl; | |
| return 1; | |
| } | |
| uint8_t nextOplState[2][256]; | |
| memset(nextOplState, 0, sizeof(nextOplState)); | |
| uint8_t oplState[2][256]; | |
| memset(oplState, 0, sizeof(oplState)); | |
| uint8_t codeMap[256]; | |
| memset(codeMap, 0, sizeof(codeMap)); | |
| std::cout << std::setfill('0'); | |
| bool first = true; | |
| bool sync = false; | |
| try { | |
| cin->read((char *)codeMap, lenCodemap); | |
| unsigned long nextDelay = 0; | |
| for (;;) { | |
| uint8_t code; | |
| *cin >> u8(code); | |
| if (code == cmdShortDelay) { | |
| uint8_t delay; | |
| *cin >> u8(delay); | |
| nextDelay += delay + 1; | |
| } else if (code == cmdLongDelay) { | |
| uint8_t delay; | |
| *cin >> u8(delay); | |
| nextDelay += (delay + 1) << 8; | |
| } else { | |
| int chip = code >> 7; // high bit | |
| uint8_t reg = codeMap[code & 0x7F]; | |
| uint8_t val; | |
| *cin >> u8(val); | |
| // Cache this value | |
| nextOplState[chip][reg] = val; | |
| } | |
| for (int chip = 0; chip < 2; chip++) { | |
| // Check for any chip-wide changes. Strictly this is only important if | |
| // a note is playing (we don't need to see changes that won't affect the | |
| // sound) but that would mean tracking the state separately between | |
| // notes. We can't just check this when a note is playing (like we used | |
| // to) because that loses any changes made during silence. | |
| if (diffGlobalState(oplState[chip], nextOplState[chip], chip)) { | |
| sync = true; | |
| } | |
| // Now run through all the normal channels and see if any notes have | |
| // been toggled. | |
| for (int c = 0; c < 9; c++) { | |
| if ( | |
| (oplState[chip][0xB0 | c] & OPLBIT_KEYON) && | |
| (!(nextOplState[chip][0xB0 | c] & OPLBIT_KEYON)) | |
| ) { | |
| // keyon bit switched off | |
| PRINT_DELAY; | |
| std::cout << "Channel " << std::dec << c+1; | |
| if (chip > 0) std::cout << 'b'; | |
| std::cout << " off\n"; | |
| sync = true; | |
| } else if (nextOplState[chip][0xB0 | c] & OPLBIT_KEYON) { | |
| // This channel is playing | |
| if (diffChannelState(oplState[chip], nextOplState[chip], c, chip, nextDelay)) { | |
| sync = true; | |
| } | |
| } | |
| } | |
| // Same again but for perc. Strictly this should be only if rhythm mode | |
| // is enabled, but we check anyway just in case. | |
| for (int p = 0; p < 5; p++) { | |
| int bit = 1 << p; | |
| if ( | |
| (oplState[chip][0xBD] & bit) && | |
| (!(nextOplState[chip][0xBD] & bit)) | |
| ) { | |
| // keyon bit switched off | |
| PRINT_DELAY; | |
| std::cout << "Perc " << percName(p) << " off\n"; | |
| sync = true; | |
| } else if (nextOplState[chip][0xBD] & bit) { | |
| // This channel is playing, or is about to | |
| if (diffPercState(oplState[chip], nextOplState[chip], p, chip, nextDelay)) { | |
| sync = true; | |
| } | |
| } | |
| } | |
| if (sync) { | |
| // Now all the differences have been shown, so sync the two register maps | |
| memcpy(oplState[chip], nextOplState[chip], sizeof(oplState[chip])); | |
| sync = false; | |
| } | |
| } | |
| } | |
| } catch (const stream::incomplete_read&) { | |
| // complete | |
| } | |
| return 0; | |
| } |