Skip to content

Commit fdaf6d8

Browse files
Add files via upload
0 parents  commit fdaf6d8

1 file changed

Lines changed: 353 additions & 0 deletions

File tree

SCI2FB.cpp

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
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

Comments
 (0)