forked from CaptainSwag101/gba-mus-ripper
-
Notifications
You must be signed in to change notification settings - Fork 0
/
gba_mus_ripper.cpp
387 lines (333 loc) · 11.9 KB
/
gba_mus_ripper.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
/**
* GBAMusRipper (c) 2012-2015 Bregalad, (c) 2017-2018 CaptainSwag101
* This is free and open source software
*
* This program analyze a Game Boy Advance ROM and search for a sound engine
* named "Sappy" which is used in ~90% of commercial GBA games.
*
* If the engine is found it rips all musics to MIDI (.mid) format and all
* instruments to SoundFont 2.0 (.sf2) format.
*/
#include <errno.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <vector>
#include <set>
#include "hex_string.hpp"
#ifndef WIN32
namespace sappy_detector
{
#include "sappy_detector.c" // The main::function is called directly on Linux
}
#define GBA_MUS_RIPPER_NAME "gba_mus_ripper"
#define SONG_RIPPER_NAME "song_ripper"
#define SOUND_FRONT_RIPPER_NAME "sound_font_ripper"
#else
#define GBA_MUS_RIPPER_NAME "gba_mus_ripper.exe"
#define SONG_RIPPER_NAME "song_ripper.exe"
#define SOUND_FRONT_RIPPER_NAME "sound_font_ripper.exe"
#endif
static FILE *inGBA;
static std::string inGBA_path;
static std::string outPath;
static size_t inGBA_size;
static std::string name;
static std::string path;
static bool gm = false;
static bool xg = false;
static bool rc = false;
static bool sb = false;
static bool raw = false;
static uint32_t song_tbl_ptr = 0;
static const int sample_rates[] = {-1, 5734, 7884, 10512, 13379, 15768, 18157, 21024, 26758, 31536, 36314, 40137, 42048};
static void print_instructions()
{
puts(
" /==========================================================================\\\n"
"-< GBA Mus Ripper 3.3 (c) 2012-2015 Bregalad, (c) 2017-2018 CaptainSwag101 >-\n"
" \\==========================================================================/\n\n"
"Usage: gba_mus_ripper (input_file) [-o output_directory] [address] [flags]\n\n"
"-gm : Give General MIDI names to presets. Note that this will only change the names and will NOT magically turn the soundfont into a General MIDI compliant soundfont.\n"
"-rc : Rearrange channels in output MIDIs so channel 10 is avoided. Needed by sound cards where it's impossible to disable \"drums\" on channel 10 even with GS or XG commands.\n"
"-xg : Output MIDI will be compliant to XG standard (instead of default GS standard).\n"
"-sb : Separate banks. Every sound bank is riper to a different .sf2 file and placed into different sub-folders (instead of doing it in a single .sf2 file and a single folder).\n"
"-raw : Output MIDIs exactly as they're encoded in ROM, without linearise volume and velocities and without simulating vibratos.\n"
"[address]: Force address of the song table manually. This is required for manually dumping music data from ROMs where the location can't be detected automatically.\n"
);
exit(0);
}
static uint32_t get_GBA_pointer()
{
uint32_t p;
fread(&p, 4, 1, inGBA);
return p - 0x8000000;
}
static void mkdir(std::string name)
{
#ifdef _WIN32
system(("md \"" + name + "\"").c_str());
#else
system(("mkdir -p \"" + name + "\"").c_str());
#endif
}
// Convert number to string with always 4 digits (even if leading zeroes)
// Mother 3 is a game which needs the 4 digits.
static std::string dec4(unsigned int n)
{
std::string s;
s += "0123456789"[n / 1000 % 10];
s += "0123456789"[n / 100 % 10];
s += "0123456789"[n / 10 % 10];
s += "0123456789"[n % 10];
return s;
}
static void parse_args(const int argc, char *const args[])
{
if (argc < 1) print_instructions();
bool path_found = false, song_tbl_found = false;
for (int i = 0; i < argc; i++)
{
if (args[i][0] == '-')
{
if (!strcmp(args[i], "--help"))
print_instructions();
else if (!strcmp(args[i], "-gm"))
gm = true;
else if (!strcmp(args[i], "-xg"))
xg = true;
else if (!strcmp(args[i], "-rc"))
rc = true;
else if (!strcmp(args[i], "-sb"))
sb = true;
else if (!strcmp(args[i], "-raw"))
raw = true;
else if (!strcmp(args[i], "-o") && argc >= i + 1)
{
outPath = args[i + 1];
}
else
{
fprintf(stderr, "Error: Unknown command line option: %s. Try with --help to get information.\n", args[i]);
exit(-1);
}
}
// Convert given address to binary, use it instead of automatically detected one
else if (!path_found)
{
// Get GBA file
inGBA = fopen(args[i], "rb");
if (!inGBA)
{
fprintf(stderr, "Error: Can't open file %s for reading.\n", args[i]);
exit(-1);
}
// Name is filename without the extention and without path
inGBA_path = std::string(args[i]);
size_t separator_index = inGBA_path.find_last_of("/\\") + 1;
name = inGBA_path.substr(separator_index, inGBA_path.find_last_of('.') - separator_index);
// Path where the input GBA file is located
path = inGBA_path.substr(0, separator_index);
path_found = true;
}
else if (!song_tbl_found)
{
errno = 0;
song_tbl_ptr = strtoul(args[i], 0, 0);
if (errno)
{
fprintf(stderr, "Error: %s is not a valid song table address.\n", args[i]);
exit(-1);
}
song_tbl_found = true;
}
else
{
fprintf(stderr, "Error: Don't know what to do with %s. Try with --help to get more information.\n", args[i]);
exit(-1);
}
}
if (!path_found)
{
fputs("Error: No input GBA file. Try with --help to get more information.\n", stderr);
exit(-1);
}
}
int main(int argc, char *const argv[])
{
// Parse arguments (without program name)
parse_args(argc - 1, argv + 1);
if (outPath.size() == 0)
{
outPath = ".";
}
// Compute program prefix (should be "", "./", "../" or whathever)
std::string prg_name = argv[0];
std::string prg_prefix = prg_name.substr(0, prg_name.rfind(GBA_MUS_RIPPER_NAME));
int sample_rate = 0, main_volume = 0; // Use default values when those are '0'
// If the user hasn't provided an address manually, we'll try to automatically detect it
if (!song_tbl_ptr)
{
// Auto-detect address of sappy engine
#ifdef WIN32
// On windows, just use the 32-bit return code of the sappy_detector executable
std::string sappy_detector_cmd = prg_prefix + "sappy_detector \"" + inGBA_path + "\"";
printf("DEBUG: Going to call system(%s)\n", sappy_detector_cmd.c_str());
int sound_engine_adr = std::system(sappy_detector_cmd.c_str());
#else
// On linux the function is duplicated in this executable
const char *sappy_detector_argv1 = inGBA_path.c_str();
int sound_engine_adr = sappy_detector::main(2, &sappy_detector_argv1 - 1);
#endif
// Exit if no sappy engine was found
if (!sound_engine_adr) exit(0);
if (fseek(inGBA, sound_engine_adr, SEEK_SET))
{
fprintf(stderr, "Error: Invalid offset within input GBA file: 0x%x\n", sound_engine_adr);
exit(0);
}
// Engine parameter's word
uint32_t parameter_word;
fread(¶meter_word, 4, 1, inGBA);
// Get sampling rate
sample_rate = sample_rates[(parameter_word >> 16) & 0xf];
main_volume = (parameter_word >> 12) & 0xf;
// Compute address of song table
uint32_t song_levels; // Read # of song levels
fread(&song_levels, 4, 1, inGBA);
printf("# of song levels: %d\n", song_levels);
song_tbl_ptr = get_GBA_pointer() + 12 * song_levels;
}
// Create a directory named like the input ROM, without the .gba extention
mkdir(outPath);
// Get the size of the input GBA file
fseek(inGBA, 0L, SEEK_END);
inGBA_size = ftell(inGBA);
if (song_tbl_ptr >= inGBA_size)
{
fprintf(stderr, "Fatal error: Song table at 0x%x is past the end of the file.\n", song_tbl_ptr);
exit(-2);
}
printf("Parsing song table...");
// New list of songs
std::vector<uint32_t> song_list;
// New list of sound banks
std::set<uint32_t> sound_bank_list;
if (fseek(inGBA, song_tbl_ptr, SEEK_SET))
{
fprintf(stderr, "Fatal error: Can't seek to song table at: 0x%x\n", song_tbl_ptr);
exit(-3);
}
// Ignores entries which are made of 0s at the start of the song table
// this fix was necessarily for the game Fire Emblem
uint32_t song_pointer;
while (true)
{
fread(&song_pointer, 4, 1, inGBA);
if (song_pointer != 0) break;
song_tbl_ptr += 4;
}
unsigned int i = 0;
while (true)
{
song_pointer -= 0x8000000; // Adjust pointer
// Stop as soon as we met with an invalid pointer
if (song_pointer == 0 || song_pointer >= inGBA_size) break;
for (int j = 4; j != 0; --j) fgetc(inGBA); // Discard 4 bytes (sound group)
song_list.push_back(song_pointer); // Add pointer to list
i++;
fread(&song_pointer, 4, 1, inGBA);
};
// As soon as data that is not a valid pointer is found, the song table is terminated
// End of song table
uint32_t song_tbl_end_ptr = 8*i + song_tbl_ptr;
puts("Collecting sound bank list...");
typedef std::set<uint32_t>::iterator bank_t;
bank_t *sound_bank_index_list = new bank_t[song_list.size()];
for (i = 0; i < song_list.size(); i++)
{
// Ignore unused song, which points to the end of the song table (for some reason)
if (song_list[i] != song_tbl_end_ptr)
{
// Seek to song data
if (fseek(inGBA, song_list[i] + 4, SEEK_SET)) continue;
uint32_t sound_bank_ptr = get_GBA_pointer();
// Add sound bank to list if not already in the list
sound_bank_index_list[i] = sound_bank_list.insert(sound_bank_ptr).first;
}
}
// Close GBA file so that SongRipper can access it
fclose(inGBA);
// Create directories for each sound bank if separate banks is enabled
if (sb)
{
for (bank_t j = sound_bank_list.begin(); j != sound_bank_list.end(); ++j)
{
unsigned int d = std::distance(sound_bank_list.begin(), j);
std::string subdir = outPath + '/' + "soundbank_" + dec4(d);
mkdir(subdir);
}
}
for (i = 0; i < song_list.size(); i++)
{
if (song_list[i] != song_tbl_end_ptr)
{
unsigned int bank_index = distance(sound_bank_list.begin(), sound_bank_index_list[i]);
std::string seq_rip_cmd = prg_prefix + SONG_RIPPER_NAME + " \"" + inGBA_path + "\" \"" + outPath;
// Add leading zeroes to file name
if (sb) seq_rip_cmd += "/soundbank_" + dec4(bank_index);
seq_rip_cmd += "/song" + dec4(i) + ".mid\"";
seq_rip_cmd += " 0x" + hex(song_list[i]);
seq_rip_cmd += rc ? " -rc" : (xg ? " -xg": " -gs");
if (!raw)
{
seq_rip_cmd += " -sv";
seq_rip_cmd += " -lv";
}
// Bank number, if banks are not separated
if (!sb)
seq_rip_cmd += " -b" + std::to_string(bank_index);
printf("Song %u\n", i);
printf("DEBUG: Going to call system(%s)\n", seq_rip_cmd.c_str());
if (!system(seq_rip_cmd.c_str())) puts("An error occurred while calling song_ripper.");
}
}
delete[] sound_bank_index_list;
if (sb)
{
// Rips each sound bank in a different file/folder
for (bank_t j = sound_bank_list.begin(); j != sound_bank_list.end(); ++j)
{
unsigned int bank_index = distance(sound_bank_list.begin(), j);
std::string sbnumber = dec4(bank_index);
std::string foldername = "soundbank_" + sbnumber;
std::string sf_rip_args = prg_prefix + SOUND_FRONT_RIPPER_NAME + " \"" + inGBA_path + "\" \"" + outPath + '/';
sf_rip_args += foldername + '/' + foldername /* + "_@" + hex(*j) */ + ".sf2\"";
if (sample_rate) sf_rip_args += " -s" + std::to_string(sample_rate);
if (main_volume) sf_rip_args += " -mv" + std::to_string(main_volume);
if (gm) sf_rip_args += " -gm";
sf_rip_args += " 0x" + hex(*j);
printf("DEBUG: Goint to call system(%s)\n", sf_rip_args.c_str());
system(sf_rip_args.c_str());
}
}
else
{
// Rips each sound bank in a single soundfont file
// Build argument list to call sound_font_riper
// Output sound font named after the input ROM
std::string sf_rip_args = prg_prefix + SOUND_FRONT_RIPPER_NAME + " \"" + inGBA_path + "\" \"" + outPath + '/' + name + ".sf2\"";
if (sample_rate) sf_rip_args += " -s" + std::to_string(sample_rate);
if (main_volume) sf_rip_args += " -mv" + std::to_string(main_volume);
// Pass -gm argument if necessary
if (gm) sf_rip_args += " -gm";
// Make sound banks addresses list.
for (bank_t j = sound_bank_list.begin(); j != sound_bank_list.end(); ++j)
sf_rip_args += " 0x" + hex(*j);
// Call sound font ripper
printf("DEBUG: Going to call system(%s)\n", sf_rip_args.c_str());
system(sf_rip_args.c_str());
}
puts("Rip completed!");
return 0;
}