|
| 1 | +/******************************************************************** |
| 2 | +* SCI2FB conversion utility v1.00 * |
| 3 | +* by Brandon Blume * |
| 4 | +* shine62@gmail.com * |
| 5 | +* March XX, 2023 * |
| 6 | +* * |
| 7 | +* Command line tool to convert a FB-01 Sierra SCI0 Patch resource * |
| 8 | +* into two FB-01 sysex Bank files. * |
| 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 | +#include <cstring> |
| 19 | + |
| 20 | +using namespace std; |
| 21 | + |
| 22 | +float nVersion = 1.00; |
| 23 | + |
| 24 | +void read_file(ifstream& file, vector<char>& data, std::streamoff titleOffset); |
| 25 | +void nibblize_data(vector<char>& data, vector<char>& splitData1, vector<char>& splitData2); |
| 26 | +void write_to_file(std::vector<char> splitData1, std::vector<char> splitData2, const char* output_filename1, const char* output_filename2); |
| 27 | +bool check_file_exists(const char* filename); |
| 28 | +void check_output_file(string output_filename); |
| 29 | + |
| 30 | +int main(int argc, char* argv[]) { |
| 31 | + // Check if the user provided exactly three arguments |
| 32 | + |
| 33 | + std::cout << std::fixed; |
| 34 | + std::cout << std::setprecision(2); |
| 35 | + cout << "\nSCI2FB v" << nVersion << " by Brandon Blume March XX, 2023" << endl; |
| 36 | + |
| 37 | + if (argc != 4) { |
| 38 | + cout << " usage: " << argv[0] << " patfile bankfile1 bankfile2\n"; |
| 39 | + return 1; |
| 40 | + } |
| 41 | + cout << endl; |
| 42 | + |
| 43 | + // Get the filenames from the command line arguments |
| 44 | + char* input_filename = argv[1]; |
| 45 | + char* output_filename1 = argv[2]; |
| 46 | + char* output_filename2 = argv[3]; |
| 47 | + |
| 48 | + // Check if patfile exists |
| 49 | + if (!check_file_exists(input_filename)) { |
| 50 | + cout << "Error: file " << input_filename << " not found" << endl; |
| 51 | + exit(EXIT_FAILURE); |
| 52 | + } |
| 53 | + |
| 54 | + // Open patfile |
| 55 | + ifstream input_file(input_filename, ios::binary); |
| 56 | + input_file.exceptions(std::ios::failbit | std::ios::badbit); |
| 57 | + |
| 58 | + // Check if the SCI patch resource identifier header exists |
| 59 | + input_file.seekg(0x00); |
| 60 | + char buffer[2]; |
| 61 | + input_file.read(buffer, 1); |
| 62 | + if (buffer[0] == (char)0x89) { |
| 63 | + cout << "SCI patch resource header detected" << endl; |
| 64 | + } |
| 65 | + else { |
| 66 | + cout << "Error: input file is not a valid SCI patch resource." << endl; |
| 67 | + exit(EXIT_FAILURE); |
| 68 | + } |
| 69 | + |
| 70 | + // Check for title string length in second byte of header to use as offset for future file handling |
| 71 | + input_file.seekg(0x01); |
| 72 | + char titleStringSize[1]; |
| 73 | + input_file.read(titleStringSize, 1); |
| 74 | + std::streamoff titleOffset = static_cast<std::streamoff>(static_cast<int>(titleStringSize[0])); |
| 75 | + |
| 76 | + // Check size of file to ensure it's valid |
| 77 | + input_file.seekg(0, ios::end); |
| 78 | + std::streamoff length = input_file.tellg(); |
| 79 | + input_file.seekg(0, ios::beg); |
| 80 | + |
| 81 | + if (length != 6148 + titleOffset) { |
| 82 | + cout << input_filename << " is not the expected size (6148 bytes + title string length). Not a valid FB-01 SCI0 Patch file." |
| 83 | + << endl << "Actual size: " << length << endl << "Title string length: " << static_cast<int>(titleStringSize[0]) << endl; |
| 84 | + exit(EXIT_FAILURE); |
| 85 | + } |
| 86 | + |
| 87 | + // Ensure the ABCDh bytes exist at address 0xC02 |
| 88 | + // (offset by the length of the title string defined in the header which we stored earlier) |
| 89 | + input_file.seekg(0xC02 + titleOffset); |
| 90 | + input_file.read(buffer, 2); |
| 91 | + if (buffer[0] == (char)0xAB && buffer[1] == (char)0xCD) { |
| 92 | + cout << "Bank separator bytes found. Input patch file is valid" << endl; |
| 93 | + } |
| 94 | + else { |
| 95 | + cout << "Error: input file is not a valid FB-01 SCI patch resource." << endl; |
| 96 | + exit(EXIT_FAILURE); |
| 97 | + } |
| 98 | + |
| 99 | + |
| 100 | + // Check if output bank files 1 and 2 already exist. If they do, ask user whether to overwrite or abort |
| 101 | + check_output_file(output_filename1); |
| 102 | + check_output_file(output_filename2); |
| 103 | + |
| 104 | + |
| 105 | + // |
| 106 | + // Finished file error handling, continue with the actual operation |
| 107 | + // |
| 108 | + |
| 109 | + // Read the input patch file into memory |
| 110 | + vector<char> data; |
| 111 | + read_file(input_file, data, titleOffset); |
| 112 | + |
| 113 | + // Close the input file |
| 114 | + input_file.close(); |
| 115 | + |
| 116 | + |
| 117 | + // Split the bytes of each instrument voice packet in order of: low nibble = high byte, high nibble = low byte |
| 118 | + vector<char> splitData1; |
| 119 | + vector<char> splitData2; |
| 120 | + splitData1.reserve(data.size()); |
| 121 | + splitData2.reserve(data.size()); |
| 122 | + splitData1.clear(); |
| 123 | + splitData2.clear(); |
| 124 | + nibblize_data(data, splitData1, splitData2); |
| 125 | + |
| 126 | + // Create the sysex bank files with the new "nibblized" data |
| 127 | + write_to_file(splitData1, splitData2, output_filename1, output_filename2); |
| 128 | + |
| 129 | + cout << "FB-01 sysex banks created successfully!" << endl; |
| 130 | + |
| 131 | + return 0; |
| 132 | +} |
| 133 | + |
| 134 | +void read_file(ifstream& file, vector<char>& data, std::streamoff titleOffset) { |
| 135 | + // Read the file starting at the first voice data byte after the header bytes. Iterate through each |
| 136 | + // byte for each of the 96 voices. (we must skip the separator bytes ABCDh on voice 49, which is |
| 137 | + // voice 1 of bank B) |
| 138 | + streampos pos = 0x02 + titleOffset; |
| 139 | + |
| 140 | + for (int i = 0; i < 96; i++) { |
| 141 | + // If we're on the 49th voice we're in bank B and need to skip the ABCDh seperator bytes first |
| 142 | + if (i == 48) pos += 2; |
| 143 | + // Store the instrument voice data into "data" |
| 144 | + file.seekg(pos); |
| 145 | + char buffer[64]; |
| 146 | + file.read(buffer, 64); |
| 147 | + data.insert(data.end(), buffer, buffer + 64); |
| 148 | + pos += 64; |
| 149 | + } |
| 150 | +} |
| 151 | + |
| 152 | +void nibblize_data(vector<char>& data, vector<char>& splitData1, vector<char>& splitData2) { |
| 153 | + // Check to ensure that both sets of instrument voice packets equal 6144 bytes in length (64 bytes per 96 voices form the patch file). |
| 154 | + if (data.size() != 6144) { |
| 155 | + cout << "Error: data vector not the expected size (6144)" << endl; |
| 156 | + cout << "Actual size = " << data.size() << endl; |
| 157 | + exit(EXIT_FAILURE); |
| 158 | + } |
| 159 | + |
| 160 | + ////////////////////////////////////////////////////////////////////////////////////////////// |
| 161 | + // Now we must nibblize the voice patch data by splitting each byte into pairs and // |
| 162 | + // and storing them in order: low nibble = high byte, high nibble = low byte's low nibble. // |
| 163 | + // This will prepare the voice data properly for the sysex format and double the size of // |
| 164 | + // each voice's byte data adding leading bytes (0x01 0x00) to signify the size of the // |
| 165 | + // packet (128) and a checksum byte calculate with 2's complement of sum following it. // |
| 166 | + // (131 bytes total per instrument voice) // |
| 167 | + ////////////////////////////////////////////////////////////////////////////////////////////// |
| 168 | + |
| 169 | + // First 48 instrument voice packets (bank A) |
| 170 | + for (int i = 0; i < 48; i++) { |
| 171 | + |
| 172 | + // set the high-byted packet size value bytes that precede the packet (0x01 0x00 = 128) |
| 173 | + splitData1.push_back(0x01); |
| 174 | + splitData1.push_back(0x00); |
| 175 | + |
| 176 | + // 64 bytes per packet |
| 177 | + for (int j = 0; j < 64; j++) { |
| 178 | + int index = i * 64 + j; |
| 179 | + splitData1.push_back(data[index] & 0x0F); |
| 180 | + splitData1.push_back((data[index] >> 4) & 0x0F); |
| 181 | + } |
| 182 | + |
| 183 | + // calculate checksum with 2's complement of sum |
| 184 | + unsigned char checksum = 0; |
| 185 | + for (int j = 2; j < 130; j++) { |
| 186 | + checksum += static_cast<unsigned int>(splitData1[i * 131 + j]); |
| 187 | + } |
| 188 | + checksum = ((~(checksum & 0xFF)) + 1) & 0x7F; |
| 189 | + splitData1.push_back(checksum); |
| 190 | + } |
| 191 | + |
| 192 | + // Second 48 instrument voice packets (bank B) |
| 193 | + for (int i = 0; i < 48; i++) { |
| 194 | + // set packet size of nibblized bytes preceding the packet: |
| 195 | + // 0x01 0x00 = 128 (%0000000h, %0lllllll -> %00000000, %hlllllll) |
| 196 | + splitData2.push_back(0x01); |
| 197 | + splitData2.push_back(0x00); |
| 198 | + |
| 199 | + // 64 bytes per packet |
| 200 | + for (int j = 0; j < 64; j++) { |
| 201 | + int index = (48 + i) * 64 + j; |
| 202 | + splitData2.push_back(data[index] & 0x0F); |
| 203 | + splitData2.push_back((data[index] >> 4) & 0x0F); |
| 204 | + } |
| 205 | + |
| 206 | + // calculate checksum with 2's complement of sum |
| 207 | + unsigned char checksum = 0; |
| 208 | + for (int j = 2; j < 130; j++) { |
| 209 | + checksum += static_cast<unsigned int>(splitData2[i * 131 + j]); |
| 210 | + } |
| 211 | + checksum = ((~(checksum & 0xFF)) + 1) & 0x7F; |
| 212 | + splitData2.push_back(checksum); |
| 213 | + } |
| 214 | +} |
| 215 | + |
| 216 | +void write_to_file(std::vector<char> splitData1, std::vector<char> splitData2, const char* output_filename1, const char* output_filename2) { |
| 217 | + |
| 218 | + // Open the output file in binary mode for writing |
| 219 | + std::ofstream out_file1(output_filename1, std::ios::binary); |
| 220 | + std::ofstream out_file2(output_filename2, std::ios::binary); |
| 221 | + |
| 222 | + ////////////////////////////////////////////////////////////////////////////////////////// |
| 223 | + // The format of the FB-01 bank sysex files we must create is structured like so: // |
| 224 | + // // |
| 225 | + // For bank A: // |
| 226 | + // $00- // |
| 227 | + // $06: F0 43 75 00 00 00 00h.......FB-01's "send bank A" sysex code // |
| 228 | + //--------------------------------------------------------------------------------------// |
| 229 | + // For bank B: // |
| 230 | + // $00- // |
| 231 | + // $06: F0 43 75 00 00 00 01h.......FB-01's "send bank B" sysex code // |
| 232 | + //--------------------------------------------------------------------------------------// |
| 233 | + // $07- // |
| 234 | + // $08: 00 40h......................Bank info packet size (64) // |
| 235 | + // $19- // |
| 236 | + // $48: <bank description>..........8-byte string for name + reserved empty bytes // |
| 237 | + // $49 : Checksum....................2's complement of sum // |
| 238 | + // $4A- // |
| 239 | + // $4B: 01 00h......................Bank voice #1 packet size (128) // |
| 240 | + // $4C- // |
| 241 | + // $CB: <patch data>................Voice #1 patch data // |
| 242 | + // $CC : Checksum....................2's complement of sum // |
| 243 | + // " " " // |
| 244 | + // " " " // |
| 245 | + // " ....."......................Voice #48 // |
| 246 | + // $18DA: F7h.........................End sysex // |
| 247 | + // // |
| 248 | + // The resulting files will each be exactly 6363 bytes long. // |
| 249 | + ////////////////////////////////////////////////////////////////////////////////////////// |
| 250 | + |
| 251 | + // Prepare an array for the whole header for the bank 1 output file |
| 252 | + char bank1header[74] = { '\xF0', '\x43', '\x75', '\x00', '\x00', '\x00', '\x00', '\x00', '\x40', '\0' }; |
| 253 | + |
| 254 | + // Prepare bank 1 info packet, pulling the name of the bank from the first output filename given. |
| 255 | + // The first 7 characters of outfile1 are pulled from the filename. If it is less than 7 characters, |
| 256 | + // it fills the remaining spaces with 0x20 (space character) and the 8th character with a '1' (bank 1). |
| 257 | + |
| 258 | + char bank1InfoPacket[32] = { 0 }; |
| 259 | + char* bank1name = bank1InfoPacket; |
| 260 | + |
| 261 | + int len = strlen(output_filename1); |
| 262 | + strncpy(bank1name, output_filename1, 7); // Copy up to 7 characters from output_filename1 into bank1name |
| 263 | + // If the length of output_filename1 is less than 7, fill the remaining characters with spaces |
| 264 | + if (len < 7) { |
| 265 | + memset(bank1name + len, 0x20, 7 - len); |
| 266 | + } |
| 267 | + bank1name[7] = '1'; // Set the 8th character to '1' (bank 1) |
| 268 | + |
| 269 | + // Now nibblize the packet which will double its length and be stored in bank1header |
| 270 | + for (int i = 0; i < sizeof(bank1InfoPacket); i++) { |
| 271 | + unsigned char high_nibble = (bank1InfoPacket[i] >> 4) & 0x0F; // extract high nibble |
| 272 | + unsigned char low_nibble = bank1InfoPacket[i] & 0x0F; // extract low nibble |
| 273 | + |
| 274 | + bank1header[9 + i * 2] = low_nibble; // write low nibble to even-indexed output array |
| 275 | + bank1header[9 + i * 2 + 1] = high_nibble; // write high nibble to odd-indexed output array |
| 276 | + } |
| 277 | + // One final step, calculate the packet's checksum and store it at the end of the header in the last index |
| 278 | + unsigned char checksum = 0; |
| 279 | + for (int i = 0; i < 64; i++) { |
| 280 | + checksum += static_cast<unsigned int>(bank1header[9 + i]); // Sum all the bytes in the packet together |
| 281 | + } |
| 282 | + checksum = ((~(checksum & 0xFF)) + 1) & 0x7F; // Drop all but the lowest 8 bits, flip the bits, add 1, then mask the lowest 7 bits for the correct checksum value |
| 283 | + bank1header[73] = checksum; |
| 284 | + |
| 285 | + // |
| 286 | + // Now for bank 2's header and info packet |
| 287 | + char bank2header[74] = { '\xF0', '\x43', '\x75', '\x00', '\x00', '\x00', '\x01', '\x00', '\x40', '\0' }; |
| 288 | + char bank2InfoPacket[32] = { 0 }; |
| 289 | + char* bank2name = bank2InfoPacket; |
| 290 | + |
| 291 | + len = strlen(output_filename2); |
| 292 | + strncpy(bank2name, output_filename2, 7); // Copy up to 7 characters from output_filename2 into bank2name |
| 293 | + // If the length of output_filename2 is less than 7, fill the remaining characters with spaces |
| 294 | + if (len < 7) { |
| 295 | + memset(bank2name + len, 0x20, 7 - len); |
| 296 | + } |
| 297 | + bank2name[7] = '2'; // Set the 8th character to '2' (bank 2) |
| 298 | + |
| 299 | + // Nibblize bank 2's info packet |
| 300 | + for (int i = 0; i < sizeof(bank2InfoPacket); i++) { |
| 301 | + unsigned char high_nibble = (bank2InfoPacket[i] >> 4) & 0x0F; |
| 302 | + unsigned char low_nibble = bank2InfoPacket[i] & 0x0F; |
| 303 | + |
| 304 | + bank2header[9 + i * 2] = low_nibble; |
| 305 | + bank2header[9 + i * 2 + 1] = high_nibble; |
| 306 | + } |
| 307 | + // Calculate and store bank 2's checksum |
| 308 | + checksum = 0; |
| 309 | + for (int i = 0; i < 64; i++) { |
| 310 | + checksum += static_cast<unsigned int>(bank2header[9 + i]); |
| 311 | + } |
| 312 | + checksum = ((~(checksum & 0xFF)) + 1) & 0x7F; |
| 313 | + bank2header[73] = checksum; |
| 314 | + |
| 315 | + // Begin writing bank A sysex file |
| 316 | + out_file1.seekp(0, std::ios::beg); |
| 317 | + out_file1.write(reinterpret_cast<char*>(&bank1header), sizeof(bank1header)); // Write the header to outfile1 |
| 318 | + out_file1.write(splitData1.data(), splitData1.size()); // Write the already nibblized and checksummed voice data packets |
| 319 | + out_file1.write("\xF7", 1); // Write the final closing byte to end the exclusive message |
| 320 | + |
| 321 | + // Begin writing bank B sysex file |
| 322 | + out_file2.seekp(0, std::ios::beg); |
| 323 | + out_file2.write(reinterpret_cast<char*>(&bank2header), sizeof(bank2header)); |
| 324 | + out_file2.write(splitData2.data(), splitData2.size()); |
| 325 | + out_file2.write("\xF7", 1); |
| 326 | + |
| 327 | + // Close both files |
| 328 | + out_file1.close(); |
| 329 | + out_file2.close(); |
| 330 | +} |
| 331 | + |
| 332 | +bool check_file_exists(const char* filename) { |
| 333 | + std::ifstream infile(filename); |
| 334 | + return infile.good(); |
| 335 | +} |
| 336 | + |
| 337 | +void check_output_file(string output_filename) { |
| 338 | + ifstream file(output_filename); |
| 339 | + if (file.good()) { |
| 340 | + cout << "\nOutput file already exists. Do you want to overwrite it? (Y/N): "; |
| 341 | + string answer; |
| 342 | + cin >> answer; |
| 343 | + if (answer == "Y" || answer == "y") { |
| 344 | + ofstream file(output_filename, ios::trunc); |
| 345 | + cout << "File " << output_filename << " successfully wiped.\n" << endl; |
| 346 | + file.close(); |
| 347 | + } |
| 348 | + else { |
| 349 | + cout << "Aborting operation..." << endl; |
| 350 | + exit(EXIT_FAILURE); |
| 351 | + } |
| 352 | + } |
| 353 | +} |
0 commit comments