|
| 1 | +/******************************************************************** |
| 2 | +* FB2SCI conversion utility v1.00 * |
| 3 | +* by Brandon Blume * |
| 4 | +* shine62@gmail.com * |
| 5 | +* February 25, 2023 * |
| 6 | +* * |
| 7 | +* Command line tool to convert 2 FB-01 sysex bank files into * |
| 8 | +* Sierra's IMF/FB-01 patch resource format for SCI0 games. * |
| 9 | +* * |
| 10 | +* You're free to do with it as you please. This program could * |
| 11 | +* probably be vastly improved to be more efficient, but it works. * |
| 12 | +********************************************************************/ |
| 13 | + |
| 14 | +#include <fstream> |
| 15 | +#include <iostream> |
| 16 | +#include <vector> |
| 17 | +#include <iomanip> |
| 18 | + |
| 19 | +using namespace std; |
| 20 | + |
| 21 | +float nVersion = 1.00; |
| 22 | + |
| 23 | +void read_files(ifstream& file1, ifstream& file2, vector<char>& data1, vector<char>& data2); |
| 24 | +void reorganize_data(vector<char>& data1, vector<char>& data2); |
| 25 | +void write_to_file(std::vector<char> data1, std::vector<char> data2, const char* output_filename); |
| 26 | +bool check_file_exists(const char* filename); |
| 27 | +void check_output_file(string output_filename); |
| 28 | + |
| 29 | +int main(int argc, char* argv[]) { |
| 30 | + // Check if the user provided exactly three arguments |
| 31 | + |
| 32 | + std::cout << std::fixed; |
| 33 | + std::cout << std::setprecision(2); |
| 34 | + cout << "\nFB2SCI v" << nVersion << " by Brandon Blume February 25, 2023" << endl; |
| 35 | + |
| 36 | + if (argc != 4) { |
| 37 | + cout << " usage: " << argv[0] << " bankfile1 bankfile2 patfile\n"; |
| 38 | + return 1; |
| 39 | + } |
| 40 | + cout << endl; |
| 41 | + |
| 42 | + // Get the filenames from the command line arguments |
| 43 | + char* input_filename1 = argv[1]; |
| 44 | + char* input_filename2 = argv[2]; |
| 45 | + char* output_filename = argv[3]; |
| 46 | + |
| 47 | + // Open the first input bank file (Bank A) |
| 48 | + ifstream input_file1(input_filename1, ios::binary); |
| 49 | + |
| 50 | + // Check if bankfile1 exists |
| 51 | + if (!check_file_exists(input_filename1)) { |
| 52 | + cout << "Error: file " << input_filename1 << " not found" << endl; |
| 53 | + exit(EXIT_FAILURE); |
| 54 | + } |
| 55 | + |
| 56 | + // Read the first 7 bytes from the file |
| 57 | + char header[7]; |
| 58 | + input_file1.read(header, 7); |
| 59 | + |
| 60 | + // Check if the header for bankfile1 matches the expected value for the FB-01's send Bank A sysex code |
| 61 | + if (memcmp(header, "\xF0\x43\x75\x00\x00\x00\x00", 7) != 0) { |
| 62 | + cout << "Error: " << input_filename1 << " is not a valid FB-01 sysex bank file (missing expected sysex header)." << endl; |
| 63 | + exit(EXIT_FAILURE); |
| 64 | + } |
| 65 | + |
| 66 | + // Get the length of the file |
| 67 | + input_file1.seekg(0, ios::end); |
| 68 | + std::streamoff length = input_file1.tellg(); |
| 69 | + input_file1.seekg(0, ios::beg); |
| 70 | + |
| 71 | + // Check if the length is 6363 bytes (must be no larger or smaller |
| 72 | + if (length != 6363) { |
| 73 | + cout << input_filename1 << " is not the expected size (6363 bytes). Not a valid FB-01 sysex bank file." << endl << "Actual size: " << length << endl; |
| 74 | + exit(EXIT_FAILURE); |
| 75 | + } |
| 76 | + |
| 77 | + // Open the second input bank file (Bank B) |
| 78 | + ifstream input_file2(input_filename2, ios::binary); |
| 79 | + |
| 80 | + // Check if bankfile2 exists |
| 81 | + if (!check_file_exists(input_filename1)) { |
| 82 | + cout << "Error: file " << input_filename2 << " not found" << endl; |
| 83 | + exit(EXIT_FAILURE); |
| 84 | + } |
| 85 | + |
| 86 | + // Read the first 7 bytes from the file |
| 87 | + input_file2.read(header, 7); |
| 88 | + |
| 89 | + // Check if the header for bankfile2 matches the expected value for the FB-01's send Bank B sysex code |
| 90 | + if (memcmp(header, "\xF0\x43\x75\x00\x00\x00\x01", 7) != 0) { |
| 91 | + cout << "Error: " << input_filename2 << " is not a valid FB-01 sysex bank file (missing expected sysex header)." << endl; |
| 92 | + exit(EXIT_FAILURE); |
| 93 | + } |
| 94 | + |
| 95 | + // Get the length of the file |
| 96 | + input_file2.seekg(0, ios::end); |
| 97 | + length = input_file2.tellg(); |
| 98 | + input_file2.seekg(0, ios::beg); |
| 99 | + |
| 100 | + // Check if the length is 6363 bytes (no larger or smaller) |
| 101 | + if (length != 6363) { |
| 102 | + cout << input_filename2 << " is not the expected size (6363 bytes). Not a valid FB-01 sysex bank file." << endl << "Actual size: " << length << endl; |
| 103 | + exit(EXIT_FAILURE); |
| 104 | + } |
| 105 | + |
| 106 | + // Check if output file already exists. If it does, ask user whether to overwrite or abort. |
| 107 | + check_output_file(output_filename); |
| 108 | + |
| 109 | + // Read the files into memory |
| 110 | + vector<char> data1, data2; |
| 111 | + read_files(input_file1, input_file2, data1, data2); |
| 112 | + |
| 113 | + // Close the input files |
| 114 | + input_file1.close(); |
| 115 | + input_file2.close(); |
| 116 | + |
| 117 | + |
| 118 | + // Byte-swap then nibble-merge the data, overwriting and truncating the vectors by half |
| 119 | + reorganize_data(data1, data2); |
| 120 | + // Create the patch file with the new "denibbled" data |
| 121 | + write_to_file(data1, data2, output_filename); |
| 122 | + |
| 123 | + cout << "SCI FB-01 Patch created successfully!" << endl; |
| 124 | + |
| 125 | + return 0; |
| 126 | +} |
| 127 | + |
| 128 | +void read_files(ifstream& file1, ifstream& file2, vector<char>& data1, vector<char>& data2) { |
| 129 | + // Set the initial read position for each bank file to address 0x4C (this is where the first instrument packet's patch data is located) |
| 130 | + streampos pos1 = 0x4c; |
| 131 | + streampos pos2 = 0x4C; |
| 132 | + |
| 133 | + // Read the files one at a time, skipping 3 bytes between each 128-byte instrument patch block |
| 134 | + // (the last byte in a packet is that packet's checksum and the first two bytes of the next packet are the next packet's |
| 135 | + // size identifier. we already skipped the two packet size indentifier bytes in the first packet by jumping straight to address 0x4C) |
| 136 | + for (int i = 0; i < 48; i++) { |
| 137 | + // Read 128 bytes from bankfile1 and bankfile2 |
| 138 | + file1.seekg(pos1); |
| 139 | + file2.seekg(pos2); |
| 140 | + char buffer1[128]; |
| 141 | + char buffer2[128]; |
| 142 | + file1.read(buffer1, 128); |
| 143 | + file2.read(buffer2, 128); |
| 144 | + data1.insert(data1.end(), buffer1, buffer1 + 128); |
| 145 | + data2.insert(data2.end(), buffer2, buffer2 + 128); |
| 146 | + |
| 147 | + // Skip 3 bytes (the current packet's checksum and the following packet's size identifier bytes) to get to the next instrument's patch data |
| 148 | + pos1 += 131; |
| 149 | + pos2 += 131; |
| 150 | + } |
| 151 | +} |
| 152 | + |
| 153 | +void reorganize_data(vector<char>& data1, vector<char>& data2) { |
| 154 | + // Check if the data vectors have the same size. (really, if the input files passed the format checks, this should never error) |
| 155 | + if (data1.size() != data2.size()) { |
| 156 | + cout << "Error: data vectors have different sizes" << endl; |
| 157 | + cout << "data1 size = " << data1.size() << endl << "data2 size = " << data2.size() << endl; |
| 158 | + return; |
| 159 | + } |
| 160 | + // Double check to ensure that both sets of instrument packets equal 6144 bytes in length (128 bytes per 48 instruments per bank file). |
| 161 | + // Again, this should never error. Probably superfluous. |
| 162 | + else if (data1.size() != 6144) { |
| 163 | + cout << "Error: data vectors not the expected size (6144)" << endl; |
| 164 | + cout << "data1 size = " << data1.size() << endl << "data2 size = " << data2.size() << endl; |
| 165 | + } |
| 166 | + |
| 167 | + ////////////////////////////////////////////////////////////////////////////////////////////// |
| 168 | + // Now we must byte-swap and nibble-merge each byte pair in every instrument packet. // |
| 169 | + // This will extract the raw patch data that SCI's patch format needs. This will reduce // |
| 170 | + // the packet size for each instrument from 128 bytes to 64 bytes so after the bytes // |
| 171 | + // are "denibblized", we will halve the data vectors sizes // |
| 172 | + ////////////////////////////////////////////////////////////////////////////////////////////// |
| 173 | + |
| 174 | + // Iterate over each byte pair of the data vectors |
| 175 | + for (int i = 0; i < static_cast<int>(data1.size()); i += 2) { |
| 176 | + // Extract the high and low bytes for the first byte pairs |
| 177 | + char high_byte1 = data1[i]; |
| 178 | + char low_byte1 = data1[i + 1]; |
| 179 | + char high_byte2 = data2[i]; |
| 180 | + char low_byte2 = data2[i + 1]; |
| 181 | + |
| 182 | + // Merge the byte pairs by shifting the low byte's low nibble to its high nibble and OR-ing the high byte's low nibble together |
| 183 | + // with the low byte's new high nibble |
| 184 | + unsigned char merged_byte1 = ((low_byte1 & 0x0F) << 4) | (high_byte1 & 0x0F); |
| 185 | + unsigned char merged_byte2 = ((low_byte2 & 0x0F) << 4) | (high_byte2 & 0x0F); |
| 186 | + |
| 187 | + // Store the reorganized data back to the data vector at the beginning where it left off |
| 188 | + data1[i / 2] = merged_byte1; |
| 189 | + data1[(i / 2) + 1] = 0x00; |
| 190 | + data2[i / 2] = merged_byte2; |
| 191 | + data2[(i / 2) + 1] = 0x00; |
| 192 | + } |
| 193 | + |
| 194 | + // Halve the size of both data vectors now that the "denibblized" data is half the original size |
| 195 | + data1.resize(data1.size() / 2); |
| 196 | + data2.resize(data2.size() / 2); |
| 197 | +} |
| 198 | + |
| 199 | +void write_to_file(std::vector<char> data1, std::vector<char> data2, const char* output_filename) { |
| 200 | + // Open the output file in binary mode for writing |
| 201 | + std::ofstream out_file(output_filename, std::ios::binary); |
| 202 | + out_file.seekp(0, std::ios::beg); |
| 203 | + |
| 204 | + ////////////////////////////////////////////////////////////////////////////////////////// |
| 205 | + // The FB-01 SCI Patch file format we must create is structured like so: // |
| 206 | + // // |
| 207 | + // $00 : 8900h.......................SCI's resource type identifier header // |
| 208 | + // $02 : Bank 1 data.................First 48 instrument patches (64 bytes each) // |
| 209 | + // $C02: ABCDh.......................Seperator bytes between the two banks // |
| 210 | + // $C04: Bank 2 data.................Last 48 instrument patches (64 bytes each) // |
| 211 | + // // |
| 212 | + // The resulting file will be exactly 6148 bytes long. // |
| 213 | + ////////////////////////////////////////////////////////////////////////////////////////// |
| 214 | + |
| 215 | + char sciPatchHeader[2] = { '\x89', '\x00' }; |
| 216 | + out_file.write(reinterpret_cast<char*>(&sciPatchHeader), sizeof(sciPatchHeader)); |
| 217 | + out_file.write(data1.data(), data1.size()); |
| 218 | + char bankSeparator[2] = { '\xAB', '\xCD' }; |
| 219 | + out_file.write(reinterpret_cast<char*>(&bankSeparator), sizeof(bankSeparator)); |
| 220 | + out_file.write(data2.data(), data2.size()); |
| 221 | + out_file.close(); |
| 222 | +} |
| 223 | + |
| 224 | +bool check_file_exists(const char* filename) { |
| 225 | + std::ifstream infile(filename); |
| 226 | + return infile.good(); |
| 227 | +} |
| 228 | + |
| 229 | +void check_output_file(string output_filename) { |
| 230 | + ifstream file(output_filename); |
| 231 | + if (file.good()) { |
| 232 | + cout << "Output file already exists. Do you want to overwrite it? (Y/N): "; |
| 233 | + string answer; |
| 234 | + cin >> answer; |
| 235 | + if (answer == "Y" || answer == "y") { |
| 236 | + ofstream file(output_filename, ios::trunc); |
| 237 | + cout << "\nFile " << output_filename << " successfully wiped.\n" << endl; |
| 238 | + file.close(); |
| 239 | + } |
| 240 | + else { |
| 241 | + cout << "Aborting operation..." << endl; |
| 242 | + exit(EXIT_FAILURE); |
| 243 | + } |
| 244 | + } |
| 245 | +} |
0 commit comments