summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
m---------tools/Torch0
-rw-r--r--tools/aifc_decodebin0 -> 22704 bytes
-rw-r--r--tools/aifc_decode.c669
-rw-r--r--tools/comptool.py299
-rw-r--r--tools/disassemble_sound.py867
-rwxr-xr-xtools/format.py3
-rw-r--r--tools/permuter_settings.toml5
-rwxr-xr-xtools/progress.py4
-rw-r--r--tools/requirements-python.txt7
m---------tools/splat0
10 files changed, 1805 insertions, 49 deletions
diff --git a/tools/Torch b/tools/Torch
-Subproject 5cf7eb78fec425ebf58e8e8cfbfa99dd2dc5481
+Subproject 298578677e28ef1fe0d3a425f2856930c6cc4ef
diff --git a/tools/aifc_decode b/tools/aifc_decode
new file mode 100644
index 00000000..0f6adb02
--- /dev/null
+++ b/tools/aifc_decode
Binary files differ
diff --git a/tools/aifc_decode.c b/tools/aifc_decode.c
new file mode 100644
index 00000000..19d13c6c
--- /dev/null
+++ b/tools/aifc_decode.c
@@ -0,0 +1,669 @@
+/**
+ * Bruteforcing decoder for converting ADPCM-encoded AIFC into AIFF, in a way
+ * that roundtrips with vadpcm_enc.
+ */
+#include <unistd.h>
+#include <assert.h>
+#include <math.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+
+typedef signed char s8;
+typedef short s16;
+typedef int s32;
+typedef unsigned char u8;
+typedef unsigned short u16;
+typedef unsigned int u32;
+typedef unsigned long long u64;
+typedef float f32;
+
+#define bswap16(x) __builtin_bswap16(x)
+#define bswap32(x) __builtin_bswap32(x)
+#define BSWAP16(x) x = __builtin_bswap16(x)
+#define BSWAP32(x) x = __builtin_bswap32(x)
+#define BSWAP16_MANY(x, n) for (s32 _i = 0; _i < n; _i++) BSWAP16((x)[_i])
+
+#define NORETURN __attribute__((noreturn))
+#define UNUSED __attribute__((unused))
+
+typedef struct {
+ u32 ckID;
+ u32 ckSize;
+} ChunkHeader;
+
+typedef struct {
+ u32 ckID;
+ u32 ckSize;
+ u32 formType;
+} Chunk;
+
+typedef struct {
+ s16 numChannels;
+ u16 numFramesH;
+ u16 numFramesL;
+ s16 sampleSize;
+ s16 sampleRate[5]; // 80-bit float
+ u16 compressionTypeH;
+ u16 compressionTypeL;
+} CommonChunk;
+
+typedef struct {
+ s16 MarkerID;
+ u16 positionH;
+ u16 positionL;
+} Marker;
+
+typedef struct {
+ s16 playMode;
+ s16 beginLoop;
+ s16 endLoop;
+} Loop;
+
+typedef struct {
+ s8 baseNote;
+ s8 detune;
+ s8 lowNote;
+ s8 highNote;
+ s8 lowVelocity;
+ s8 highVelocity;
+ s16 gain;
+ Loop sustainLoop;
+ Loop releaseLoop;
+} InstrumentChunk;
+
+typedef struct {
+ s32 offset;
+ s32 blockSize;
+} SoundDataChunk;
+
+typedef struct {
+ s16 version;
+ s16 order;
+ s16 nEntries;
+} CodeChunk;
+
+typedef struct
+{
+ u32 start;
+ u32 end;
+ u32 count;
+ s16 state[16];
+} ALADPCMloop;
+
+
+static char usage[] = "input.aifc output.aiff";
+static const char *progname, *infilename;
+
+#define checked_fread(a, b, c, d) if (fread(a, b, c, d) != c) fail_parse("error parsing file")
+
+NORETURN
+void fail_parse(const char *fmt, ...)
+{
+ char *formatted = NULL;
+ va_list ap;
+ va_start(ap, fmt);
+ int size = vsnprintf(NULL, 0, fmt, ap);
+ va_end(ap);
+ if (size >= 0) {
+ size++;
+ formatted = malloc(size);
+ if (formatted != NULL) {
+ va_start(ap, fmt);
+ size = vsnprintf(formatted, size, fmt, ap);
+ va_end(ap);
+ if (size < 0) {
+ free(formatted);
+ formatted = NULL;
+ }
+ }
+ }
+
+ if (formatted != NULL) {
+ fprintf(stderr, "%s: %s [%s]\n", progname, formatted, infilename);
+ free(formatted);
+ }
+ exit(1);
+}
+
+s32 myrand()
+{
+ static u64 state = 1619236481962341ULL;
+ state *= 3123692312231ULL;
+ state++;
+ return state >> 33;
+}
+
+s16 qsample(s32 x, s32 scale)
+{
+ // Compute x / 2^scale rounded to the nearest integer, breaking ties towards zero.
+ if (scale == 0) return x;
+ return (x + (1 << (scale - 1)) - (x > 0)) >> scale;
+}
+
+s16 clamp_to_s16(s32 x)
+{
+ if (x < -0x8000) return -0x8000;
+ if (x > 0x7fff) return 0x7fff;
+ return (s16) x;
+}
+
+s32 toi4(s32 x)
+{
+ if (x >= 8) return x - 16;
+ return x;
+}
+
+s32 readaifccodebook(FILE *fhandle, s32 ****table, s16 *order, s16 *npredictors)
+{
+ checked_fread(order, sizeof(s16), 1, fhandle);
+ BSWAP16(*order);
+ checked_fread(npredictors, sizeof(s16), 1, fhandle);
+ BSWAP16(*npredictors);
+ *table = malloc(*npredictors * sizeof(s32 **));
+ for (s32 i = 0; i < *npredictors; i++) {
+ (*table)[i] = malloc(8 * sizeof(s32 *));
+ for (s32 j = 0; j < 8; j++) {
+ (*table)[i][j] = malloc((*order + 8) * sizeof(s32));
+ }
+ }
+
+ for (s32 i = 0; i < *npredictors; i++) {
+ s32 **table_entry = (*table)[i];
+ for (s32 j = 0; j < *order; j++) {
+ for (s32 k = 0; k < 8; k++) {
+ s16 ts;
+ checked_fread(&ts, sizeof(s16), 1, fhandle);
+ BSWAP16(ts);
+ table_entry[k][j] = ts;
+ }
+ }
+
+ for (s32 k = 1; k < 8; k++) {
+ table_entry[k][*order] = table_entry[k - 1][*order - 1];
+ }
+
+ table_entry[0][*order] = 1 << 11;
+
+ for (s32 k = 1; k < 8; k++) {
+ s32 j = 0;
+ for (; j < k; j++) {
+ table_entry[j][k + *order] = 0;
+ }
+
+ for (; j < 8; j++) {
+ table_entry[j][k + *order] = table_entry[j - k][*order];
+ }
+ }
+ }
+ return 0;
+}
+
+ALADPCMloop *readlooppoints(FILE *ifile, s16 *nloops)
+{
+ checked_fread(nloops, sizeof(s16), 1, ifile);
+ BSWAP16(*nloops);
+ ALADPCMloop *al = malloc(*nloops * sizeof(ALADPCMloop));
+ for (s32 i = 0; i < *nloops; i++) {
+ checked_fread(&al[i], sizeof(ALADPCMloop), 1, ifile);
+ BSWAP32(al[i].start);
+ BSWAP32(al[i].end);
+ BSWAP32(al[i].count);
+ BSWAP16_MANY(al[i].state, 16);
+ }
+ return al;
+}
+
+s32 inner_product(s32 length, s32 *v1, s32 *v2)
+{
+ s32 out = 0;
+ for (s32 i = 0; i < length; i++) {
+ out += v1[i] * v2[i];
+ }
+
+ // Compute "out / 2^11", rounded down.
+ s32 dout = out / (1 << 11);
+ s32 fiout = dout * (1 << 11);
+ return dout - (out - fiout < 0);
+}
+
+void my_decodeframe(u8 *frame, s32 *state, s32 order, s32 ***coefTable)
+{
+ s32 ix[16];
+
+ u8 header = frame[0];
+ s32 scale = 1 << (header >> 4);
+ s32 optimalp = header & 0xf;
+
+ for (s32 i = 0; i < 16; i += 2) {
+ u8 c = frame[1 + i/2];
+ ix[i] = c >> 4;
+ ix[i + 1] = c & 0xf;
+ }
+
+ for (s32 i = 0; i < 16; i++) {
+ if (ix[i] >= 8) ix[i] -= 16;
+ ix[i] *= scale;
+ }
+
+ for (s32 j = 0; j < 2; j++) {
+ s32 in_vec[16];
+ if (j == 0) {
+ for (s32 i = 0; i < order; i++) {
+ in_vec[i] = state[16 - order + i];
+ }
+ } else {
+ for (s32 i = 0; i < order; i++) {
+ in_vec[i] = state[8 - order + i];
+ }
+ }
+
+ for (s32 i = 0; i < 8; i++) {
+ s32 ind = j * 8 + i;
+ in_vec[order + i] = ix[ind];
+ state[ind] = inner_product(order + i, coefTable[optimalp][i], in_vec) + ix[ind];
+ }
+ }
+}
+
+void my_encodeframe(u8 *out, s16 *inBuffer, s32 *state, s32 ***coefTable, s32 order, s32 npredictors)
+{
+ s16 ix[16];
+ s32 prediction[16];
+ s32 inVector[16];
+ s32 saveState[16];
+ s32 optimalp = 0;
+ s32 scale;
+ s32 ie[16];
+ s32 e[16];
+ f32 min = 1e30;
+
+ for (s32 k = 0; k < npredictors; k++) {
+ for (s32 j = 0; j < 2; j++) {
+ for (s32 i = 0; i < order; i++) {
+ inVector[i] = (j == 0 ? state[16 - order + i] : inBuffer[8 - order + i]);
+ }
+
+ for (s32 i = 0; i < 8; i++) {
+ prediction[j * 8 + i] = inner_product(order + i, coefTable[k][i], inVector);
+ e[j * 8 + i] = inVector[i + order] = inBuffer[j * 8 + i] - prediction[j * 8 + i];
+ }
+ }
+
+ f32 se = 0.0f;
+ for (s32 j = 0; j < 16; j++) {
+ se += (f32) e[j] * (f32) e[j];
+ }
+
+ if (se < min) {
+ min = se;
+ optimalp = k;
+ }
+ }
+
+ for (s32 j = 0; j < 2; j++) {
+ for (s32 i = 0; i < order; i++) {
+ inVector[i] = (j == 0 ? state[16 - order + i] : inBuffer[8 - order + i]);
+ }
+
+ for (s32 i = 0; i < 8; i++) {
+ prediction[j * 8 + i] = inner_product(order + i, coefTable[optimalp][i], inVector);
+ e[j * 8 + i] = inVector[i + order] = inBuffer[j * 8 + i] - prediction[j * 8 + i];
+ }
+ }
+
+ for (s32 i = 0; i < 16; i++) {
+ ie[i] = clamp_to_s16(e[i]);
+ }
+
+ s32 max = 0;
+ for (s32 i = 0; i < 16; i++) {
+ if (abs(ie[i]) > abs(max)) {
+ max = ie[i];
+ }
+ }
+
+ for (scale = 0; scale <= 12; scale++) {
+ if (max <= 7 && max >= -8) break;
+ max /= 2;
+ }
+
+ for (s32 i = 0; i < 16; i++) {
+ saveState[i] = state[i];
+ }
+
+ for (s32 nIter = 0, again = 1; nIter < 2 && again; nIter++) {
+ again = 0;
+ if (nIter == 1) scale++;
+ if (scale > 12) {
+ scale = 12;
+ }
+
+ for (s32 j = 0; j < 2; j++) {
+ s32 base = j * 8;
+ for (s32 i = 0; i < order; i++) {
+ inVector[i] = (j == 0 ?
+ saveState[16 - order + i] : state[8 - order + i]);
+ }
+
+ for (s32 i = 0; i < 8; i++) {
+ prediction[base + i] = inner_product(order + i, coefTable[optimalp][i], inVector);
+ s32 se = inBuffer[base + i] - prediction[base + i];
+ ix[base + i] = qsample(se, scale);
+ s32 cV = clamp_to_s16(ix[base + i]) - ix[base + i];
+ if (cV > 1 || cV < -1) again = 1;
+ ix[base + i] += cV;
+ inVector[i + order] = ix[base + i] * (1 << scale);
+ state[base + i] = prediction[base + i] + inVector[i + order];
+ }
+ }
+ }
+
+ u8 header = (scale << 4) | (optimalp & 0xf);
+ out[0] = header;
+ for (s32 i = 0; i < 16; i += 2) {
+ u8 c = ((ix[i] & 0xf) << 4) | (ix[i + 1] & 0xf);
+ out[1 + i/2] = c;
+ }
+}
+
+void permute(s16 *out, s32 *in, s32 scale)
+{
+ for (s32 i = 0; i < 16; i++) {
+ out[i] = clamp_to_s16(in[i] - scale / 2 + myrand() % (scale + 1));
+ }
+}
+
+void write_header(FILE *ofile, const char *id, s32 size)
+{
+ fwrite(id, 4, 1, ofile);
+ BSWAP32(size);
+ fwrite(&size, sizeof(s32), 1, ofile);
+}
+
+int main(int argc, char **argv)
+{
+ s16 order = -1;
+ s16 nloops = 0;
+ ALADPCMloop *aloops = NULL;
+ s16 npredictors = -1;
+ s32 ***coefTable = NULL;
+ s32 state[16];
+ s32 soundPointer = -1;
+ s32 currPos = 0;
+ s32 nSamples = 0;
+ Chunk FormChunk;
+ ChunkHeader Header;
+ CommonChunk CommChunk;
+ InstrumentChunk InstChunk;
+ SoundDataChunk SndDChunk;
+ FILE *ifile;
+ FILE *ofile;
+ progname = argv[0];
+
+ if (argc < 3) {
+ fprintf(stderr, "%s %s\n", progname, usage);
+ exit(1);
+ }
+
+ infilename = argv[1];
+
+ if ((ifile = fopen(infilename, "rb")) == NULL) {
+ fail_parse("AIFF-C file could not be opened");
+ exit(1);
+ }
+
+ if ((ofile = fopen(argv[2], "wb")) == NULL) {
+ fprintf(stderr, "%s: output file could not be opened [%s]\n", progname, argv[2]);
+ exit(1);
+ }
+
+ memset(&InstChunk, 0, sizeof(InstChunk));
+
+ checked_fread(&FormChunk, sizeof(FormChunk), 1, ifile);
+ BSWAP32(FormChunk.ckID);
+ BSWAP32(FormChunk.formType);
+ if ((FormChunk.ckID != 0x464f524d) || (FormChunk.formType != 0x41494643)) { // FORM, AIFC
+ fail_parse("not an AIFF-C file");
+ }
+
+ for (;;) {
+ s32 num = fread(&Header, sizeof(Header), 1, ifile);
+ u32 ts;
+ if (num <= 0) break;
+ BSWAP32(Header.ckID);
+ BSWAP32(Header.ckSize);
+
+ Header.ckSize++;
+ Header.ckSize &= ~1;
+ s32 offset = ftell(ifile);
+
+ switch (Header.ckID) {
+ case 0x434f4d4d: // COMM
+ checked_fread(&CommChunk, sizeof(CommChunk), 1, ifile);
+ BSWAP16(CommChunk.numChannels);
+ BSWAP16(CommChunk.numFramesH);
+ BSWAP16(CommChunk.numFramesL);
+ BSWAP16(CommChunk.sampleSize);
+ BSWAP16(CommChunk.compressionTypeH);
+ BSWAP16(CommChunk.compressionTypeL);
+ s32 cType = (CommChunk.compressionTypeH << 16) + CommChunk.compressionTypeL;
+ if (cType != 0x56415043) { // VAPC
+ fail_parse("file is of the wrong compression type");
+ }
+ if (CommChunk.numChannels != 1) {
+ fail_parse("file contains %d channels, only 1 channel supported", CommChunk.numChannels);
+ }
+ if (CommChunk.sampleSize != 16) {
+ fail_parse("file contains %d bit samples, only 16 bit samples supported", CommChunk.sampleSize);
+ }
+
+ nSamples = (CommChunk.numFramesH << 16) + CommChunk.numFramesL;
+
+ // Allow broken input lengths
+ if (nSamples % 16) {
+ nSamples--;
+ }
+
+ if (nSamples % 16 != 0) {
+ fail_parse("number of chunks must be a multiple of 16, found %d", nSamples);
+ }
+ break;
+
+ case 0x53534e44: // SSND
+ checked_fread(&SndDChunk, sizeof(SndDChunk), 1, ifile);
+ BSWAP32(SndDChunk.offset);
+ BSWAP32(SndDChunk.blockSize);
+ assert(SndDChunk.offset == 0);
+ assert(SndDChunk.blockSize == 0);
+ soundPointer = ftell(ifile);
+ break;
+
+ case 0x4150504c: // APPL
+ checked_fread(&ts, sizeof(u32), 1, ifile);
+ BSWAP32(ts);
+ if (ts == 0x73746f63) { // stoc
+ u8 len;
+ checked_fread(&len, 1, 1, ifile);
+ if (len == 11) {
+ char ChunkName[12];
+ s16 version;
+ checked_fread(ChunkName, 11, 1, ifile);
+ ChunkName[11] = '\0';
+ if (strcmp("VADPCMCODES", ChunkName) == 0) {
+ checked_fread(&version, sizeof(s16), 1, ifile);
+ BSWAP16(version);
+ if (version != 1) {
+ fail_parse("Unknown codebook chunk version");
+ }
+ readaifccodebook(ifile, &coefTable, &order, &npredictors);
+ }
+ else if (strcmp("VADPCMLOOPS", ChunkName) == 0) {
+ checked_fread(&version, sizeof(s16), 1, ifile);
+ BSWAP16(version);
+ if (version != 1) {
+ fail_parse("Unknown loop chunk version");
+ }
+ aloops = readlooppoints(ifile, &nloops);
+ if (nloops != 1) {
+ fail_parse("Only a single loop supported");
+ }
+ }
+ }
+ }
+ break;
+ }
+
+ fseek(ifile, offset + Header.ckSize, SEEK_SET);
+ }
+
+ if (coefTable == NULL) {
+ fail_parse("Codebook missing from bitstream");
+ }
+
+ for (s32 i = 0; i < order; i++) {
+ state[15 - i] = 0;
+ }
+
+ u32 outputBytes = nSamples * sizeof(s16);
+ u8 *outputBuf = malloc(outputBytes);
+
+ fseek(ifile, soundPointer, SEEK_SET);
+ while (currPos < nSamples) {
+ u8 input[9];
+ u8 encoded[9];
+ s32 lastState[16];
+ s32 decoded[16];
+ s16 guess[16];
+ s16 origGuess[16];
+
+ memcpy(lastState, state, sizeof(lastState));
+ checked_fread(input, 9, 1, ifile);
+
+ // Decode for real
+ my_decodeframe(input, state, order, coefTable);
+ memcpy(decoded, state, sizeof(lastState));
+
+ // Create a guess from that, by clamping to 16 bits
+ for (s32 i = 0; i < 16; i++) {
+ origGuess[i] = clamp_to_s16(state[i]);
+ }
+
+ // Encode the guess
+ memcpy(state, lastState, sizeof(lastState));
+ memcpy(guess, origGuess, sizeof(guess));
+ my_encodeframe(encoded, guess, state, coefTable, order, npredictors);
+
+ // If it doesn't match, randomly round numbers until it does.
+ if (memcmp(input, encoded, 9) != 0) {
+ s32 scale = 1 << (input[0] >> 4);
+ do {
+ permute(guess, decoded, scale);
+ memcpy(state, lastState, sizeof(lastState));
+ my_encodeframe(encoded, guess, state, coefTable, order, npredictors);
+ } while (memcmp(input, encoded, 9) != 0);
+
+ // Bring the matching closer to the original decode (not strictly
+ // necessary, but it will move us closer to the target on average).
+ for (s32 failures = 0; failures < 50; failures++) {
+ s32 ind = myrand() % 16;
+ s32 old = guess[ind];
+ if (old == origGuess[ind]) continue;
+ guess[ind] = origGuess[ind];
+ if (myrand() % 2) guess[ind] += (old - origGuess[ind]) / 2;
+ memcpy(state, lastState, sizeof(lastState));
+ my_encodeframe(encoded, guess, state, coefTable, order, npredictors);
+ if (memcmp(input, encoded, 9) == 0) {
+ failures = -1;
+ }
+ else {
+ guess[ind] = old;
+ }
+ }
+ }
+
+ memcpy(state, decoded, sizeof(lastState));
+ BSWAP16_MANY(guess, 16);
+ memcpy(outputBuf + currPos * 2, guess, sizeof(guess));
+ currPos += 16;
+ }
+
+ // Write an incomplete file header. We'll fill in the size later.
+ fwrite("FORM\0\0\0\0AIFF", 12, 1, ofile);
+
+ // Subtract 4 from the COMM size to skip the compression field.
+ write_header(ofile, "COMM", sizeof(CommonChunk) - 4);
+ CommChunk.numFramesH = nSamples >> 16;
+ CommChunk.numFramesL = nSamples & 0xffff;
+ BSWAP16(CommChunk.numChannels);
+ BSWAP16(CommChunk.numFramesH);
+ BSWAP16(CommChunk.numFramesL);
+ BSWAP16(CommChunk.sampleSize);
+ fwrite(&CommChunk, sizeof(CommonChunk) - 4, 1, ofile);
+
+ if (nloops > 0) {
+ s32 startPos = aloops[0].start, endPos = aloops[0].end;
+ const char *markerNames[2] = {"start", "end"};
+ Marker markers[2] = {
+ {1, startPos >> 16, startPos & 0xffff},
+ {2, endPos >> 16, endPos & 0xffff}
+ };
+ write_header(ofile, "MARK", 2 + 2 * sizeof(Marker) + 1 + 5 + 1 + 3);
+ s16 numMarkers = bswap16(2);
+ fwrite(&numMarkers, sizeof(s16), 1, ofile);
+ for (s32 i = 0; i < 2; i++) {
+ u8 len = (u8) strlen(markerNames[i]);
+ BSWAP16(markers[i].MarkerID);
+ BSWAP16(markers[i].positionH);
+ BSWAP16(markers[i].positionL);
+ fwrite(&markers[i], sizeof(Marker), 1, ofile);
+ fwrite(&len, 1, 1, ofile);
+ fwrite(markerNames[i], len, 1, ofile);
+ }
+
+ write_header(ofile, "INST", sizeof(InstrumentChunk));
+ InstChunk.sustainLoop.playMode = bswap16(1);
+ InstChunk.sustainLoop.beginLoop = bswap16(1);
+ InstChunk.sustainLoop.endLoop = bswap16(2);
+ InstChunk.releaseLoop.playMode = 0;
+ InstChunk.releaseLoop.beginLoop = 0;
+ InstChunk.releaseLoop.endLoop = 0;
+ fwrite(&InstChunk, sizeof(InstrumentChunk), 1, ofile);
+ }
+
+ // Save the coefficient table for use when encoding. Ideally this wouldn't
+ // be needed and "tabledesign -s 1" would generate the right table, but in
+ // practice it's difficult to adjust samples to make that happen.
+ write_header(ofile, "APPL", 4 + 12 + sizeof(CodeChunk) + npredictors * order * 8 * 2);
+ fwrite("stoc", 4, 1, ofile);
+ CodeChunk cChunk;
+ cChunk.version = bswap16(1);
+ cChunk.order = bswap16(order);
+ cChunk.nEntries = bswap16(npredictors);
+ fwrite("\x0bVADPCMCODES", 12, 1, ofile);
+ fwrite(&cChunk, sizeof(CodeChunk), 1, ofile);
+ for (s32 i = 0; i < npredictors; i++) {
+ for (s32 j = 0; j < order; j++) {
+ for (s32 k = 0; k < 8; k++) {
+ s16 ts = bswap16(coefTable[i][k][j]);
+ fwrite(&ts, sizeof(s16), 1, ofile);
+ }
+ }
+ }
+
+ write_header(ofile, "SSND", outputBytes + 8);
+ SndDChunk.offset = 0;
+ SndDChunk.blockSize = 0;
+ fwrite(&SndDChunk, sizeof(SoundDataChunk), 1, ofile);
+ fwrite(outputBuf, outputBytes, 1, ofile);
+
+ // Fix the size in the header
+ s32 fileSize = bswap32(ftell(ofile) - 8);
+ fseek(ofile, 4, SEEK_SET);
+ fwrite(&fileSize, 4, 1, ofile);
+
+ fclose(ifile);
+ fclose(ofile);
+ return 0;
+}
diff --git a/tools/comptool.py b/tools/comptool.py
index 16a64aad..cc858e5d 100644
--- a/tools/comptool.py
+++ b/tools/comptool.py
@@ -6,17 +6,76 @@ import struct
import argparse
import sys
-file_names = [
+file_table_dict = {"US 1.1":0xDE480, "US 1.0":0xD9A90, "JP 1.0":0xE93C0, "JP 1.1":0xF2A10, "EU 1.0":0xE0570, "AU 1.0":0xE0470, "LN 1.0":0xE44F0}
+
+file_names_jp = [
+ "makerom", "main", "dma_table", "audio_seq", "audio_bank", "audio_table", "ast_common", "ast_bg_space", "ast_bg_planet",
+ "ast_arwing", "ast_landmaster", "ast_blue_marine", "ast_versus", "ast_enmy_planet", "ast_enmy_space", "ast_great_fox",
+ "ast_star_wolf", "ast_allies", "ast_corneria", "ast_meteo", "ast_titania", "ast_7_ti_2", "ast_8_ti", "ast_9_ti", "ast_A_ti",
+ "ast_7_ti_1", "ast_sector_x", "ast_sector_z", "ast_aquas", "ast_area_6", "ast_venom_1", "ast_venom_2", "ast_ve1_boss",
+ "ast_bolse", "ast_fortuna", "ast_sector_y", "ast_solar", "ast_zoness", "ast_katina", "ast_macbeth", "ast_warp_zone",
+ "ast_title", "ast_map", "ast_option", "ast_vs_menu", "ast_text", "ast_font_3d", "ast_andross", "ast_logo", "ast_ending",
+ "ast_ending_award_front", "ast_ending_award_back", "ast_ending_expert", "ast_training", "ovl_i1", "ovl_i2",
+ "ovl_i3", "ovl_i4", "ovl_i5", "ovl_i6", "ovl_menu", "ovl_ending", "ovl_unused"
+ ]
+
+file_names_us = [
"makerom", "main", "dma_table", "audio_seq", "audio_bank", "audio_table", "ast_common", "ast_bg_space", "ast_bg_planet",
- "ast_arwing", "ast_landmaster", "ast_blue_marine", "ast_vs_player", "ast_enmy_planet", "ast_enmy_space", "ast_great_fox",
+ "ast_arwing", "ast_landmaster", "ast_blue_marine", "ast_versus", "ast_enmy_planet", "ast_enmy_space", "ast_great_fox",
"ast_star_wolf", "ast_allies", "ast_corneria", "ast_meteo", "ast_titania", "ast_7_ti_2", "ast_8_ti", "ast_9_ti", "ast_A_ti",
"ast_7_ti_1", "ast_sector_x", "ast_sector_z", "ast_aquas", "ast_area_6", "ast_venom_1", "ast_venom_2", "ast_ve1_boss",
"ast_bolse", "ast_fortuna", "ast_sector_y", "ast_solar", "ast_zoness", "ast_katina", "ast_macbeth", "ast_warp_zone",
- "ast_title", "ast_menu", "ast_option", "ast_versus", "ast_font", "ast_font_3d", "ast_andross", "ast_logo", "ast_ending",
- "ast_ending_award_front", "ast_ending_award_back", "ast_reward", "ast_training", "ast_radio", "ovl_i1", "ovl_i2",
+ "ast_title", "ast_map", "ast_option", "ast_vs_menu", "ast_text", "ast_font_3d", "ast_andross", "ast_logo", "ast_ending",
+ "ast_ending_award_front", "ast_ending_award_back", "ast_ending_expert", "ast_training", "ast_radio", "ovl_i1", "ovl_i2",
"ovl_i3", "ovl_i4", "ovl_i5", "ovl_i6", "ovl_menu", "ovl_ending", "ovl_unused"
]
+file_names_pal = [
+ "makerom", "main", "dma_table", "audio_seq", "audio_bank", "audio_table", "ast_common", "ast_bg_space", "ast_bg_planet",
+ "ast_arwing", "ast_landmaster", "ast_blue_marine", "ast_versus", "ast_enmy_planet", "ast_enmy_space", "ast_great_fox",
+ "ast_star_wolf", "ast_allies", "ast_corneria", "ast_meteo", "ast_titania", "ast_7_ti_2", "ast_8_ti", "ast_9_ti", "ast_A_ti",
+ "ast_7_ti_1", "ast_sector_x", "ast_sector_z", "ast_aquas", "ast_area_6", "ast_venom_1", "ast_venom_2", "ast_ve1_boss",
+ "ast_bolse", "ast_fortuna", "ast_sector_y", "ast_solar", "ast_zoness", "ast_katina", "ast_macbeth", "ast_warp_zone",
+ "ast_title", "ast_map", "ast_map_en", "ast_map_fr", "ast_map_de", "ast_option", "ast_option_en", "ast_option_fr",
+ "ast_option_de", "ast_vs_menu", "ast_vs_menu_en", "ast_vs_menu_fr", "ast_vs_menu_de", "ast_text", "ast_font_3d", "ast_andross","ast_logo", "ast_ending",
+ "ast_ending_award_front", "ast_ending_award_back", "ast_ending_expert", "ast_training", "ast_radio_de", "ovl_i1", "ovl_i2", "ovl_i3",
+ "ovl_i4", "ovl_i5", "ovl_i6", "ovl_menu", "ovl_ending", "ovl_unused", "ast_radio_en", "ast_radio_fr"
+ ]
+
+file_names_critical = ["makerom", "main", "dma_table", "audio_seq", "audio_bank", "audio_table"]
+
+decomp_inds_ntsc = [0, 1, 2, 3, 4, 5, 15, 16, 21, 22, 23, 24, 48]
+decomp_inds_pal = [0, 1, 2, 3, 4, 5, 15, 16, 21, 22, 23, 24, 57]
+
+def get_version_info(ROM):
+ with open(ROM, "rb") as ROMfile:
+ ROMfile.seek(0x3E, 0)
+ region = ROMfile.read(1).decode()
+ rev =" 1.%d" % int.from_bytes(ROMfile.read(1), 'big')
+
+
+ if region == "J":
+ file_names = file_names_jp
+ decomp_inds = decomp_inds_ntsc
+ version = "JP"
+ elif region == "E" or region == "G":
+ file_names = file_names_us
+ decomp_inds = decomp_inds_ntsc
+ version = "LN" if region == "G" else "US"
+ elif region == "P" or region == "U":
+ file_names = file_names_pal
+ decomp_inds = decomp_inds_pal
+ version = "AU" if region == "U" else "EU"
+ else:
+ file_names = "file_%d_%X"
+ decomp_inds = None
+ version = "Unknown"
+
+ if version != "Unknown":
+ version += rev
+
+ return (version, file_names, decomp_inds)
+
def int32(x):
return x & 0xFFFFFFFF
@@ -86,17 +145,111 @@ def mio0_dec_bytes(comp_bytes, mio0):
return decomp_bytes
-def compress(baserom, comprom, mio0, extract_dest=None):
- decomp_inds = [0, 1, 2, 3, 4, 5, 15, 16, 21, 22, 23, 24, 48]
-
+swap_backup = False
+
+def fix_byte_swap(ROM, outROM):
+ with open(ROM, 'rb') as ROMfile:
+ ROMfile.seek(0x20,0)
+
+ game_str = ROMfile.read(4).decode()
+
+ if game_str == "STAR":
+ print("Provided ROM is big endian.")
+ return ROM
+
+ ROMfile.seek(0,0)
+
+ ROM_bytes = ROMfile.read()
+
+ s = game_str.find("S")
+ t = game_str.find("T")
+ a = game_str.find("A")
+ r = game_str.find("R")
+
+ if(s == -1 or t == -1 or a == -1 or r==-1):
+ print('Name string absent. There may be a problem with your ROM.')
+ sys.exit(2)
+
+ if game_str == "RATS":
+ print("Provided ROM is little endian.")
+ byte_order = "LE"
+ suffix = ".LE.n64"
+ elif game_str == "TSRA":
+ print("Provided ROM is byteswapped.")
+ byte_order = "BS"
+ suffix = ".BS.v64"
+ else:
+ byte_order = "%d%d%d%d" % (s, t, a, r)
+ suffix = "." + byte_order + ".u64"
+ print("Provided ROM has unusual byte order " + byte_order)
+ if swap_backup:
+ backup = os.path.splitext(ROM)[0] + suffix
+ with open(backup, "wb") as bakfile:
+ print("Writing backup file " + backup)
+ bakfile.write(ROM_bytes)
+ outROM = ROM
+
+ ROM_array = [bytearray([ROM_bytes[4*x + s], ROM_bytes[4*x + t], ROM_bytes[4*x + a], ROM_bytes[4*x + r]])
+ for x in range(len(ROM_bytes) // 4)
+ ]
+
+ with open(outROM, 'wb') as tempROMfile:
+ tempROMfile.write(b''.join(ROM_array))
+
+ return outROM
+
+
+def find_file_table(ROM):
+ with open(ROM, 'rb') as ROMfile:
+
+ ROMfile.seek(0,0)
+
+ main_area = ROMfile.read()
+
+ file_table_start = main_area.find(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x50\x00\x00\x00\x00')
+ if file_table_start == -1:
+ file_table_start = main_area.find(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x60\x00\x00\x00\x00')
+
+ if file_table_start == -1:
+ print('File table not found.')
+ sys.exit(2)
+ elif file_table_start > 0x100000:
+ print("Warning: Detected file table offset 0x%X is larger than expected." % file_table_start)
+ # print(file_table_start)
+
+ return file_table_start
+
+
+def compress(baserom, comprom, mio0, dma_table=None, verbose=False):
+ if dma_table:
+ file_table = int(dma_table, 0)
+ if verbose:
+ print("Using provided DMA table offset 0x%X" % file_table)
+ else:
+ file_table = find_file_table(baserom)
+ if verbose:
+ print("DMA table found at 0x%X" % file_table)
+
+
+ (version, file_names, decomp_inds) = get_version_info(baserom)
+ ft_version = file_table_dict.get(version)
+
+ if version == "Unknown":
+ print("Unknown version. Unable to determine compression scheme.")
+ sys.exit(2)
+ elif ft_version and ft_version != file_table:
+ print("Warning: No record of DMA table at 0x%X for %s" % (file_table, version))
+ elif verbose:
+ print("Detected ROM version is " + version)
+
# comp_const = 0xFFFEFFFFFE1E7FC0
with open(comprom, 'w+b') as compfile, open(baserom, 'rb') as basefile:
file_count = 0
p_file_begin = 0
-
+
while True:
- file_entry = 0xDE480 + 0x10 * file_count
+ file_entry = file_table + 0x10 * file_count
basefile.seek(file_entry + 4)
v_file_begin = int.from_bytes(basefile.read(4),'big')
@@ -104,15 +257,17 @@ def compress(baserom, comprom, mio0, extract_dest=None):
v_file_size = v_file_end - v_file_begin
- if(v_file_begin == 0 and v_file_end == 0):
+ if v_file_begin == 0 and v_file_end == 0:
break
basefile.seek(v_file_begin)
compfile.truncate(p_file_begin)
file_bytes = basefile.read(v_file_size)
+
+ file_name = file_names[file_count]
- if (file_count in decomp_inds) or (file_count <= 5):
+ if (file_count in decomp_inds) or (file_name in file_names_critical):
# if (1 << file_count) & comp_flags:
p_file_size = v_file_size
dec_msg = 'uncompressed'
@@ -129,8 +284,8 @@ def compress(baserom, comprom, mio0, extract_dest=None):
comp_flag = 1
compfile.seek(0, 2)
-
- # print("File " + str(file_count) + ": Writing " + format(p_file_size, 'X') + " bytes at " + format(compfile.tell(),'X'))
+ if verbose:
+ print("File " + file_name + ": Writing 0x%X " + dec_msg + " bytes at 0x%X" % (p_file_size, compfile.tell()))
compfile.write(file_bytes)
@@ -161,41 +316,64 @@ def compress(baserom, comprom, mio0, extract_dest=None):
return
-def decompress(baserom, decomprom, mio0, extract_dest=None):
- with open(decomprom, 'w+b') as decompfile, open(baserom, 'rb') as baserom:
- file_count = 0
+def decompress(baserom, decomprom, mio0, extract_dest=None, dma_table=None, print_inds=False, verbose=False):
+ baserom = fix_byte_swap(baserom, baserom + "zxqj")
+
+ if dma_table:
+ file_table = int(dma_table, 0)
+ print("Using provided DMA table offset 0x%X" % file_table)
+ else:
+ file_table = find_file_table(baserom)
+ print("DMA table found at 0x%X" % file_table)
+
+ (version, file_names, decomp_inds) = get_version_info(baserom)
+ ft_version = file_table_dict.get(version)
+
+
+ if version == "Unknown":
+ print("Could not detect version")
+ elif ft_version and ft_version != file_table:
+ print("Warning: No record of DMA table at 0x%X for %s" % (file_table, version))
+ else:
+ print("Detected ROM version is " + version)
+ with open(decomprom, 'w+b') as decompfile, open(baserom, 'rb') as basefile:
+ file_count = 0
+ decomp_file_inds = []
+
while True:
- file_entry = 0xDE480 + 0x10 * file_count
- baserom.seek(file_entry)
+ file_entry = file_table + 0x10 * file_count
+ basefile.seek(file_entry)
- v_file_begin = int.from_bytes(baserom.read(4),'big')
- p_file_begin = int.from_bytes(baserom.read(4),'big')
- p_file_end = int.from_bytes(baserom.read(4),'big')
- comp_flag = int.from_bytes(baserom.read(4),'big')
+ v_file_begin = int.from_bytes(basefile.read(4),'big')
+ p_file_begin = int.from_bytes(basefile.read(4),'big')
+ p_file_end = int.from_bytes(basefile.read(4),'big')
+ comp_flag = int.from_bytes(basefile.read(4),'big')
p_file_size = p_file_end - p_file_begin
#print(v_file_begin, p_file_begin, p_file_end, comp_flag)
- if(v_file_begin == 0 and p_file_end == 0):
+ if v_file_begin == 0 and p_file_end == 0:
break
decompfile.truncate(v_file_begin)
- baserom.seek(p_file_begin)
+ basefile.seek(p_file_begin)
- file_bytes = baserom.read(p_file_size)
+ file_bytes = basefile.read(p_file_size)
if comp_flag == 0:
v_file_size = p_file_size
+ decomp_file_inds += [file_count]
dec_msg = 'uncompressed'
+
elif comp_flag == 1:
file_bytes = mio0_dec_bytes(file_bytes, mio0)
dec_msg = 'compressed'
v_file_size = len(file_bytes)
else:
- print('Invalid compression flag. This should be impossible, so please tell us if you get this error anyways.')
+ print('Invalid compression flag. There may be a problem with your ROM.')
sys.exit(2)
decompfile.seek(0, 2)
@@ -204,15 +382,29 @@ def decompress(baserom, decomprom, mio0, extract_dest=None):
v_file_end = v_file_begin + v_file_size
+ if decomp_inds:
+ file_name = file_names[file_count]
+ else:
+ file_name = file_names % (file_count, v_file_begin)
+
+ if verbose:
+ print("name: " + file_name)
+ print("start: 0x%X" % v_file_begin)
+ # print("index", file_count, dec_msg, "; size: 0x%X" % v_file_size)
+
if extract_dest is not None:
if not os.path.exists(extract_dest):
os.mkdir(extract_dest)
-
- file_name = file_names[file_count] + '.bin'
-
- with open(extract_dest + os.sep + file_name, 'wb') as extract_file:
+
+ if version == "Unknown":
+ suffix = "%X" % file_table
+ else:
+ suffix = version.replace(" 1.", ".rev").lower()
+
+ out_file_name = file_name + "." + suffix + ".bin"
+ with open(extract_dest + os.sep + out_file_name, 'wb') as extract_file:
extract_file.write(file_bytes)
-
+
decompfile.seek(file_entry + 4)
decompfile.write(v_file_begin.to_bytes(4,'big'))
decompfile.write(v_file_end.to_bytes(4,'big'))
@@ -226,18 +418,33 @@ def decompress(baserom, decomprom, mio0, extract_dest=None):
decompfile.seek(0x10)
decompfile.write(crc1.to_bytes(4, 'big'))
decompfile.write(crc2.to_bytes(4, 'big'))
+ print("Found %d files." % file_count)
+ if len(decomp_file_inds) == file_count:
+ print("Provided ROM was uncompressed.")
+ elif print_inds or verbose:
+ print("These file numbers were not compressed:")
+ print(decomp_file_inds)
+ elif decomp_file_inds != decomp_inds:
+ print("Warning: Unusual compression scheme. These files were uncompressed:")
+ print(decomp_file_inds)
+
+ if baserom.endswith("zxqj"):
+ run(["rm", baserom])
return
parser = argparse.ArgumentParser(description='Compress or decompress a Star Fox 64 ROM')
-parser.add_argument('inROM', help="ROM file to compress or decompress")
-parser.add_argument('outROM', help="output file for processed ROM.")
-parser.add_argument('-e', metavar='extract',dest='extract',help='directory for extracted decompressed files. Use with -d')
-parser.add_argument('-c', action='store_true',help='compress provided ROM')
-parser.add_argument('-d', action='store_true',help='decompress provided ROM')
-parser.add_argument('-m', metavar='mio0',dest='mio0',help='Path to mio0 tool if not in same directory')
-parser.add_argument('-r', action="store_true",help='Fix crc without compressing or decompressing')
-# parser.add_argument('-v', action='store_true',help='show what changes are made')
+parser.add_argument('inROM', help="ROM file to process")
+parser.add_argument('outROM', help="Output file for processed ROM.")
+parser.add_argument('-c', action='store_true',help='Compress a big endian uncompressed Star Fox 64 ROM')
+parser.add_argument('-d', action='store_true',help='Decompress a Star Fox 64 ROM. Use with -s to also make a big endian compressed ROM.')
+parser.add_argument('-e', metavar='extract',dest='extract',help='Directory for extracted decompressed files. Use with -d')
+parser.add_argument('-r', action="store_true",help='Fix crc of Star Fox 64 ROM without compressing or decompressing')
+parser.add_argument('-s', action='store_true',help='Swap a Star Fox 64 ROM to big endian (.z64). Use . as second argument to swap in-place or .b to also make a backup')
+parser.add_argument('-m', metavar='mio0',dest='mio0',help='Path to mio0 tool if not named "mio0" and in same directory')
+parser.add_argument('-i', action='store_true',help='Print indices of uncompressed files during decompression.')
+parser.add_argument('-v', action='store_true',help='Print details about the ROM files.')
+parser.add_argument('-t', metavar='dma_table', dest='dma_table',help='Provide DMA table explicitly instead of autodetecting')
if __name__ == '__main__':
args = parser.parse_args()
@@ -250,10 +457,16 @@ if __name__ == '__main__':
if args.r:
fix_crc(args.inROM)
elif args.c:
- compress(args.inROM, args.outROM, mio0)
+ compress(args.inROM, args.outROM, mio0, dma_table=args.dma_table, verbose=args.v)
elif args.d or args.extract:
- decompress(args.inROM, args.outROM, mio0, args.extract)
+ swap_backup = args.s
+ decompress(args.inROM, args.outROM, mio0, extract_dest=args.extract, dma_table=args.dma_table, print_inds=args.i, verbose=args.v)
+ elif args.s:
+ if args.outROM[0] == ".":
+ args.outRom = args.inRom
+ if args.outROM == ".b":
+ swap_backup = True
+ fix_byte_swap(args.inROM, args.outROM)
else:
- print("Something went wrong.")
-
+ print("No action specified. Use -c, -d, -e, -r, or -s to specify an action")
diff --git a/tools/disassemble_sound.py b/tools/disassemble_sound.py
new file mode 100644
index 00000000..bf82e746
--- /dev/null
+++ b/tools/disassemble_sound.py
@@ -0,0 +1,867 @@
+#!/usr/bin/env python3
+from collections import namedtuple, defaultdict
+import tempfile
+import subprocess
+import uuid
+import json
+import os
+import re
+import struct
+import sys
+
+TYPE_CTL = 1
+TYPE_TBL = 2
+
+
+class AifcEntry:
+ def __init__(self, data, book, loop):
+ self.name = None
+ self.data = data
+ self.book = book
+ self.loop = loop
+ self.tunings = []
+
+
+class SampleBank:
+ def __init__(self, name, data, offset):
+ self.offset = offset
+ self.name = name
+ self.data = data
+ self.entries = {}
+
+ def add_sample(self, offset, sample_size, book, loop):
+ assert sample_size % 2 == 0
+ if sample_size % 9 != 0:
+ # print(sample_size)
+ assert sample_size % 9 == 1
+ sample_size -= 1
+
+ if offset in self.entries:
+ entry = self.entries[offset]
+ assert entry.book == book
+ assert entry.loop == loop
+ # print(len(entry.data), sample_size)
+ assert len(entry.data) == sample_size
+ else:
+ entry = AifcEntry(self.data[offset : offset + sample_size], book, loop)
+ self.entries[offset] = entry
+
+ return entry
+
+
+Sound = namedtuple("Sound", ["sample_addr", "tuning"])
+Drum = namedtuple("Drum", ["name", "addr", "release_rate", "pan", "envelope", "sound"])
+Inst = namedtuple(
+ "Inst",
+ [
+ "name",
+ "addr",
+ "release_rate",
+ "normal_range_lo",
+ "normal_range_hi",
+ "envelope",
+ "sound_lo",
+ "sound_med",
+ "sound_hi",
+ ],
+)
+Book = namedtuple("Book", ["order", "npredictors", "table"])
+Loop = namedtuple("Loop", ["start", "end", "count", "state"])
+Envelope = namedtuple("Envelope", ["name", "entries"])
+Bank = namedtuple(
+ "Bank",
+ [
+ "name",
+ "iso_date",
+ "sample_bank",
+ "insts",
+ "drums",
+ "all_insts",
+ "inst_list",
+ "envelopes",
+ "samples",
+ ],
+)
+
+
+def align(val, al):
+ return (val + (al - 1)) & -al
+
+
+name_tbl = {}
+
+
+def gen_name(prefix, name_table=[]):
+ if prefix not in name_tbl:
+ name_tbl[prefix] = 0
+ ind = name_tbl[prefix]
+ name_tbl[prefix] += 1
+ if ind < len(name_table):
+ return name_table[ind]
+ return prefix + str(ind)
+
+
+def parse_bcd(data):
+ ret = 0
+ for c in data:
+ ret *= 10
+ ret += c >> 4
+ ret *= 10
+ ret += c & 15
+ return ret
+
+
+def serialize_f80(num):
+ num = float(num)
+ (f64,) = struct.unpack(">Q", struct.pack(">d", num))
+ f64_sign_bit = f64 & 2 ** 63
+ if num == 0.0:
+ if f64_sign_bit:
+ return b"\x80" + b"\0" * 9
+ else:
+ return b"\0" * 10
+ exponent = (f64 ^ f64_sign_bit) >> 52
+ assert exponent != 0, "can't handle denormals"
+ assert exponent != 0x7FF, "can't handle infinity/nan"
+ exponent -= 1023
+ f64_mantissa_bits = f64 & (2 ** 52 - 1)
+ f80_sign_bit = f64_sign_bit << (80 - 64)
+ f80_exponent = (exponent + 0x3FFF) << 64
+ f80_mantissa_bits = 2 ** 63 | (f64_mantissa_bits << (63 - 52))
+ f80 = f80_sign_bit | f80_exponent | f80_mantissa_bits
+ return struct.pack(">HQ", f80 >> 64, f80 & (2 ** 64 - 1))
+
+
+def round_f32(num):
+ enc = struct.pack(">f", num)
+ for decimals in range(5, 20):
+ num2 = round(num, decimals)
+ if struct.pack(">f", num2) == enc:
+ return num2
+ return num
+
+
+def parse_sound(data):
+ sample_addr, tuning = struct.unpack(">If", data)
+ if sample_addr == 0:
+ assert tuning == 0
+ return None
+ return Sound(sample_addr, tuning)
+
+
+def parse_drum(data, addr):
+ name = gen_name("drum")
+ release_rate, pan, loaded, pad = struct.unpack(">BBBB", data[:4])
+ assert loaded == 0
+ assert pad == 0
+ sound = parse_sound(data[4:12])
+ (env_addr,) = struct.unpack(">I", data[12:])
+ assert env_addr != 0
+ return Drum(name, addr, release_rate, pan, env_addr, sound)
+
+
+def parse_inst(data, addr):
+ name = gen_name("inst")
+ loaded, normal_range_lo, normal_range_hi, release_rate, env_addr = struct.unpack(
+ ">BBBBI", data[:8]
+ )
+ assert env_addr != 0
+ sound_lo = parse_sound(data[8:16])
+ sound_med = parse_sound(data[16:24])
+ sound_hi = parse_sound(data[24:])
+ if sound_lo is None:
+ assert normal_range_lo == 0
+ if sound_hi is None:
+ assert normal_range_hi == 127
+ return Inst(
+ name,
+ addr,
+ release_rate,
+ normal_range_lo,
+ normal_range_hi,
+ env_addr,
+ sound_lo,
+ sound_med,
+ sound_hi,
+ )
+
+
+def parse_loop(addr, bank_data):
+ start, end, count, pad = struct.unpack(">IIiI", bank_data[addr : addr + 16])
+ assert pad == 0
+ if count != 0:
+ state = struct.unpack(">16h", bank_data[addr + 16 : addr + 48])
+ else:
+ state = None
+ return Loop(start, end, count, state)
+
+
+def parse_book(addr, bank_data):
+ order, npredictors = struct.unpack(">ii", bank_data[addr : addr + 8])
+
+ assert order == 2
+ # assert npredictors == 2
+ # if (npredictors != 2):
+ # print(addr, order, npredictors)
+ table_data = bank_data[addr + 8 : addr + 8 + 16 * order * npredictors]
+ table = []
+ for i in range(0, 16 * order * npredictors, 2):
+ table.append(struct.unpack(">h", table_data[i : i + 2])[0])
+ return Book(order, npredictors, table)
+
+
+def parse_sample(data, bank_data, sample_bank, is_shindou):
+ if is_shindou:
+ sample_size, addr, loop, book = struct.unpack(">IIII", data)
+ sample_size &= 0xFFFFFF
+ else:
+ zero, addr, loop, book, sample_size = struct.unpack(">IIIII", data)
+ assert zero == 0
+ assert loop != 0
+ print(sample_size, addr, loop, book)
+ assert book != 0
+ loop = parse_loop(loop, bank_data)
+ book = parse_book(book, bank_data)
+
+ return sample_bank.add_sample(addr, sample_size, book, loop)
+
+
+def parse_envelope(addr, data_bank):
+ entries = []
+ while True:
+ delay, arg = struct.unpack(">HH", data_bank[addr : addr + 4])
+ entries.append((delay, arg))
+ addr += 4
+ if 1 <= (-delay) % 2 ** 16 <= 3:
+ break
+ return entries
+
+
+def parse_ctl_header(header):
+ num_instruments, num_drums, shared = struct.unpack(">III", header[:12])
+ date = parse_bcd(header[12:])
+ y = date // 10000
+ m = date // 100 % 100
+ d = date % 100
+ iso_date = "{:02}-{:02}-{:02}".format(y, m, d)
+ assert shared in [0, 1]
+ return num_instruments, num_drums, iso_date
+
+
+def parse_ctl(parsed_header, data, sample_bank, index, is_shindou):
+ name_tbl.clear()
+ name = "{:02X}".format(index)
+ num_instruments, num_drums, iso_date = parsed_header
+ print("{}: {}, {} + {}".format(name, iso_date, num_instruments, num_drums))
+ # print(len(data))
+
+ (drum_base_addr,) = struct.unpack(">I", data[:4])
+ drum_addrs = []
+ if num_drums != 0:
+ assert drum_base_addr != 0
+ for i in range(num_drums):
+ (drum_addr,) = struct.unpack(
+ ">I", data[drum_base_addr + i * 4 : drum_base_addr + i * 4 + 4]
+ )
+ # assert drum_addr != 0
+ # print(i, drum_addr)
+ if(drum_addr == 0):
+ continue
+ drum_addrs.append(drum_addr)
+ else:
+ assert drum_base_addr == 0
+
+ inst_base_addr = 4
+ inst_addrs = []
+ inst_list = []
+ for i in range(num_instruments):
+ (inst_addr,) = struct.unpack(
+ ">I", data[inst_base_addr + i * 4 : inst_base_addr + i * 4 + 4]
+ )
+ if inst_addr == 0:
+ inst_list.append(None)
+ else:
+ inst_list.append(inst_addr)
+ inst_addrs.append(inst_addr)
+
+ inst_addrs.sort()
+ assert drum_addrs == sorted(drum_addrs)
+ if drum_addrs and inst_addrs:
+ assert max(inst_addrs) < min(drum_addrs)
+
+ # print(inst_addrs)
+
+ if len(set(inst_addrs)) != len(inst_addrs):
+ print(index)
+ # assert len(set(inst_addrs)) == len(inst_addrs)
+ # assert len(set(drum_addrs)) == len(drum_addrs)
+
+ insts = []
+ for inst_addr in inst_addrs:
+ insts.append(parse_inst(data[inst_addr : inst_addr + 32], inst_addr))
+
+ drums = []
+ for drum_addr in drum_addrs:
+ drums.append(parse_drum(data[drum_addr : drum_addr + 16], drum_addr))
+
+ env_addrs = set()
+ sample_addrs = set()
+ tunings = defaultdict(lambda: [])
+ for inst in insts:
+ for sound in [inst.sound_lo, inst.sound_med, inst.sound_hi]:
+ if sound is not None:
+ sample_addrs.add(sound.sample_addr)
+ tunings[sound.sample_addr].append(sound.tuning)
+ env_addrs.add(inst.envelope)
+ for drum in drums:
+ sample_addrs.add(drum.sound.sample_addr)
+ tunings[drum.sound.sample_addr].append(drum.sound.tuning)
+ env_addrs.add(drum.envelope)
+
+ # Put drums somewhere in the middle of the instruments to make sample
+ # addresses come in increasing order. (This logic isn't totally right,
+ # but it works for our purposes.)
+ all_insts = []
+ need_drums = len(drums) > 0
+ for inst in insts:
+ if need_drums and any(
+ s.sample_addr > drums[0].sound.sample_addr
+ for s in [inst.sound_lo, inst.sound_med, inst.sound_hi]
+ if s is not None
+ ):
+ all_insts.append(drums)
+ need_drums = False
+ all_insts.append(inst)
+
+ if need_drums:
+ all_insts.append(drums)
+
+ samples = {}
+
+ for addr in sorted(sample_addrs):
+ sample_size = 16 if is_shindou else 20
+ sample_data = data[addr : addr + sample_size]
+ print("\t\t", addr)
+ samples[addr] = parse_sample(sample_data, data, sample_bank, is_shindou)
+ samples[addr].tunings.extend(tunings[addr])
+
+ env_data = {}
+ used_env_addrs = set()
+ for addr in sorted(env_addrs):
+ env = parse_envelope(addr, data)
+ env_data[addr] = env
+ for i in range(align(len(env), 4)):
+ used_env_addrs.add(addr + i * 4)
+
+ # Unused envelopes
+ unused_envs = set()
+ if used_env_addrs:
+ for addr in range(min(used_env_addrs) + 4, max(used_env_addrs), 4):
+ if addr not in used_env_addrs:
+ unused_envs.add(addr)
+ (stub_marker,) = struct.unpack(">I", data[addr : addr + 4])
+ assert stub_marker == 0
+ env = parse_envelope(addr, data)
+ env_data[addr] = env
+ for i in range(align(len(env), 4)):
+ used_env_addrs.add(addr + i * 4)
+
+ envelopes = {}
+ for addr in sorted(env_data.keys()):
+ env_name = gen_name("envelope")
+ if addr in unused_envs:
+ env_name += "_unused"
+ envelopes[addr] = Envelope(env_name, env_data[addr])
+
+ return Bank(
+ name,
+ iso_date,
+ sample_bank,
+ insts,
+ drums,
+ all_insts,
+ inst_list,
+ envelopes,
+ samples,
+ )
+
+
+def parse_seqfile(data, filetype):
+ magic, num_entries = struct.unpack(">HH", data[:4])
+ assert magic == filetype
+ prev = align(4 + num_entries * 8, 16)
+ entries = []
+ for i in range(num_entries):
+ offset, length = struct.unpack(">II", data[4 + i * 8 : 4 + i * 8 + 8])
+ if filetype == TYPE_CTL:
+ assert offset == prev
+ else:
+ assert offset <= prev
+ prev = max(prev, offset + length)
+ entries.append((offset, length))
+ assert all(x == 0 for x in data[prev:])
+ return entries
+
+
+def parse_sh_header(data, filetype):
+ (num_entries,) = struct.unpack(">H", data[:2])
+ assert data[2:16] == b"\0" * 14
+ prev = 0
+ entries = []
+ for i in range(num_entries):
+ subdata = data[16 + 16 * i : 32 + 16 * i]
+ offset, length, magic = struct.unpack(">IIH", subdata[:10])
+ assert offset == prev
+ # assert magic == (0x0204 if filetype == TYPE_TBL else 0x0203)
+ prev = offset + length
+ if filetype == TYPE_CTL:
+ assert subdata[14:16] == b"\0" * 2
+ sample_bank_index, magic2, num_instruments, num_drums = struct.unpack(
+ ">BBBB", subdata[10:14]
+ )
+ assert magic2 == 0xFF
+ # num_drums >>= 4
+ entries.append(
+ (offset, length, (sample_bank_index, num_instruments, num_drums))
+ )
+ else:
+ assert subdata[10:16] == b"\0" * 6
+ entries.append((offset, length))
+ return entries
+
+
+def parse_tbl(data, entries):
+ seen = {}
+ tbls = []
+ sample_banks = []
+ sample_bank_map = {}
+ for (offset, length) in entries:
+ if offset not in seen:
+ name = gen_name("sample_bank")
+ seen[offset] = name
+ sample_bank = SampleBank(name, data[offset : offset + length], offset)
+ sample_banks.append(sample_bank)
+ sample_bank_map[name] = sample_bank
+ tbls.append(seen[offset])
+ return tbls, sample_banks, sample_bank_map
+
+
+class AifcWriter:
+ def __init__(self, out):
+ self.out = out
+ self.sections = []
+ self.total_size = 0
+
+ def add_section(self, tp, data):
+ assert isinstance(tp, bytes)
+ assert isinstance(data, bytes)
+ self.sections.append((tp, data))
+ self.total_size += align(len(data), 2) + 8
+
+ def add_custom_section(self, tp, data):
+ self.add_section(b"APPL", b"stoc" + self.pstring(tp) + data)
+
+ def pstring(self, data):
+ return bytes([len(data)]) + data + (b"" if len(data) % 2 else b"\0")
+
+ def finish(self):
+ # total_size isn't used, and is regularly wrong. In particular, vadpcm_enc
+ # preserves the size of the input file...
+ self.total_size += 4
+ self.out.write(b"FORM" + struct.pack(">I", self.total_size) + b"AIFC")
+ for (tp, data) in self.sections:
+ self.out.write(tp + struct.pack(">I", len(data)))
+ self.out.write(data)
+ if len(data) % 2:
+ self.out.write(b"\0")
+
+
+def write_aifc(entry, out):
+ writer = AifcWriter(out)
+ num_channels = 1
+ data = entry.data
+ assert len(data) % 9 == 0
+ if len(data) % 2 == 1:
+ data += b"\0"
+ # (Computing num_frames this way makes it off by one when the data length
+ # is odd. It matches vadpcm_enc, though.)
+ num_frames = len(data) * 16 // 9
+ sample_size = 16 # bits per sample
+
+ if len(set(entry.tunings)) == 1:
+ sample_rate = 32000 * entry.tunings[0]
+ else:
+ # Some drum sounds in sample bank B don't have unique sample rates, so
+ # we have to guess. This doesn't matter for matching, it's just to make
+ # the sounds easy to listen to.
+ if min(entry.tunings) <= 0.5 <= max(entry.tunings):
+ sample_rate = 16000
+ elif min(entry.tunings) <= 1.0 <= max(entry.tunings):
+ sample_rate = 32000
+ elif min(entry.tunings) <= 1.5 <= max(entry.tunings):
+ sample_rate = 48000
+ elif min(entry.tunings) <= 2.5 <= max(entry.tunings):
+ sample_rate = 80000
+ else:
+ sample_rate = 16000 * (min(entry.tunings) + max(entry.tunings))
+
+ writer.add_section(
+ b"COMM",
+ struct.pack(">hIh", num_channels, num_frames, sample_size)
+ + serialize_f80(sample_rate)
+ + b"VAPC"
+ + writer.pstring(b"VADPCM ~4-1"),
+ )
+ writer.add_section(b"INST", b"\0" * 20)
+ table_data = b"".join(struct.pack(">h", x) for x in entry.book.table)
+ writer.add_custom_section(
+ b"VADPCMCODES",
+ struct.pack(">hhh", 1, entry.book.order, entry.book.npredictors) + table_data,
+ )
+ writer.add_section(b"SSND", struct.pack(">II", 0, 0) + data)
+ if entry.loop.count != 0:
+ writer.add_custom_section(
+ b"VADPCMLOOPS",
+ struct.pack(
+ ">HHIIi16h",
+ 1,
+ 1,
+ entry.loop.start,
+ entry.loop.end,
+ entry.loop.count,
+ *entry.loop.state
+ ),
+ )
+ writer.finish()
+
+
+def write_aiff(entry, filename):
+ temp = tempfile.NamedTemporaryFile(suffix=".aifc", delete=False)
+ try:
+ write_aifc(entry, temp)
+ temp.flush()
+ temp.close()
+ aifc_decode = os.path.join(os.path.dirname(__file__), "aifc_decode")
+ subprocess.run([aifc_decode, temp.name, filename], check=True)
+ finally:
+ temp.close()
+ os.remove(temp.name)
+
+
+# Modified from https://stackoverflow.com/a/25935321/1359139, cc by-sa 3.0
+class NoIndent(object):
+ def __init__(self, value):
+ self.value = value
+
+
+class NoIndentEncoder(json.JSONEncoder):
+ def __init__(self, *args, **kwargs):
+ super(NoIndentEncoder, self).__init__(*args, **kwargs)
+ self._replacement_map = {}
+
+ def default(self, o):
+ def ignore_noindent(o):
+ if isinstance(o, NoIndent):
+ return o.value
+ return self.default(o)
+
+ if isinstance(o, NoIndent):
+ key = uuid.uuid4().hex
+ self._replacement_map[key] = json.dumps(o.value, default=ignore_noindent)
+ return "@@%s@@" % (key,)
+ else:
+ return super(NoIndentEncoder, self).default(o)
+
+ def encode(self, o):
+ result = super(NoIndentEncoder, self).encode(o)
+ repl_map = self._replacement_map
+
+ def repl(m):
+ key = m.group()[3:-3]
+ return repl_map[key]
+
+ return re.sub(r"\"@@[0-9a-f]*?@@\"", repl, result)
+
+
+def inst_ifdef_json(bank_index, inst_index):
+ if bank_index == 7 and inst_index >= 13:
+ return NoIndent(["VERSION_US", "VERSION_EU"])
+ if bank_index == 8 and inst_index >= 16:
+ return NoIndent(["VERSION_US", "VERSION_EU"])
+ if bank_index == 10 and inst_index >= 14:
+ return NoIndent(["VERSION_US", "VERSION_EU"])
+ return None
+
+
+def main():
+ args = []
+ need_help = False
+ only_samples = False
+ only_samples_list = []
+ shindou_headers = None
+ if sys.argv[1].startswith("files"):
+ with open(sys.argv[2], "rb") as ctl_file, open(sys.argv[3], "rb") as tbl_file, \
+ open(sys.argv[4], "rb") as header_file:
+
+ ctl_data = ctl_file.read()
+ tbl_data = tbl_file.read()
+ if sys.argv[1].endswith("jp"):
+ header_start = 0xC1360 - 0x1050
+ elif sys.argv[1].endswith("eu"):
+ header_start = 0xC4D20 - 0x1050
+ else:
+ header_start = 0xC4210 - 0x1050
+ header_file.seek(header_start)
+ hlen = int.from_bytes(header_file.read(2), "big")
+ header_file.seek(-2, 1)
+ tbl_header_data = header_file.read((1 + hlen) * 0x10)
+ hlen = int.from_bytes(header_file.read(2), "big")
+ header_file.seek(hlen * 0x10 + 0xE, 1)
+ hlen = int.from_bytes(header_file.read(2), "big")
+ header_file.seek(-2, 1)
+ ctl_header_data = header_file.read((1 + hlen) * 0x10)
+
+ shindou_headers = True
+ samples_out_dir = sys.argv[5]
+ banks_out_dir = sys.argv[6]
+ else:
+ skip_next = 0
+ for i, a in enumerate(sys.argv[1:], 1):
+ if skip_next > 0:
+ skip_next -= 1
+ continue
+ if a == "--help" or a == "-h":
+ need_help = True
+ elif a == "--only-samples":
+ only_samples = True
+ elif a == "--shindou-headers":
+ shindou_headers = sys.argv[i + 1 : i + 5]
+ skip_next = 4
+ elif a.startswith("-"):
+ print("Unrecognized option " + a)
+ sys.exit(1)
+ elif only_samples:
+ only_samples_list.append(a)
+ else:
+ args.append(a)
+
+ expected_num_args = 5 + (0 if only_samples else 2)
+ if (
+ need_help
+ or len(args) != expected_num_args
+ or (shindou_headers and len(shindou_headers) != 4)
+ ):
+ print(
+ "Usage: {}"
+ " <.z64 rom> <ctl offset> <ctl size> <tbl offset> <tbl size>"
+ " [--shindou-headers <ctl header offset> <ctl header size>"
+ " <tbl header offset> <tbl header size>]"
+ " (<samples outdir> <sound bank outdir> |"
+ " --only-samples file:index ...)".format(sys.argv[0])
+ )
+ sys.exit(0 if need_help else 1)
+
+ rom_file = open(args[0], "rb")
+
+ def read_at(offset, size):
+ rom_file.seek(int(offset))
+ return rom_file.read(int(size))
+
+ ctl_data = read_at(args[1], args[2])
+ tbl_data = read_at(args[3], args[4])
+
+ ctl_header_data = None
+ tbl_header_data = None
+ if shindou_headers:
+ ctl_header_data = read_at(shindou_headers[0], shindou_headers[1])
+ tbl_header_data = read_at(shindou_headers[2], shindou_headers[3])
+
+ if not only_samples:
+ samples_out_dir = args[5]
+ banks_out_dir = args[6]
+
+ banks = []
+
+ if shindou_headers:
+ ctl_entries = parse_sh_header(ctl_header_data, TYPE_CTL)
+ tbl_entries = parse_sh_header(tbl_header_data, TYPE_TBL)
+
+ sample_banks = parse_tbl(tbl_data, tbl_entries)[1]
+ print(len(ctl_entries))
+ for index, (offset, length, sh_meta) in enumerate(ctl_entries):
+ sample_bank = sample_banks[sh_meta[0]]
+ entry = ctl_data[offset : offset + length]
+ header = (sh_meta[1], sh_meta[2], "0000-00-00")
+ banks.append(parse_ctl(header, entry, sample_bank, index, True))
+ else:
+ ctl_entries = parse_seqfile(ctl_data, TYPE_CTL)
+ tbl_entries = parse_seqfile(tbl_data, TYPE_TBL)
+ assert len(ctl_entries) == len(tbl_entries)
+
+ tbls, sample_banks, sample_bank_map = parse_tbl(tbl_data, tbl_entries)
+
+ for index, (offset, length), sample_bank_name in zip(
+ range(len(ctl_entries)), ctl_entries, tbls
+ ):
+ sample_bank = sample_bank_map[sample_bank_name]
+ entry = ctl_data[offset : offset + length]
+ header = parse_ctl_header(entry[:16])
+ banks.append(parse_ctl(header, entry[16:], sample_bank, index, False))
+
+ # Special mode used for asset extraction: generate aifc files, with paths
+ # given by command line arguments
+ if only_samples:
+ index_to_filename = {}
+ created_dirs = set()
+ for arg in only_samples_list:
+ filename, index = arg.rsplit(":", 1)
+ index_to_filename[int(index)] = filename
+ index = -1
+ for sample_bank in sample_banks:
+ offsets = sorted(set(sample_bank.entries.keys()))
+ for offset in offsets:
+ entry = sample_bank.entries[offset]
+ index += 1
+ if index in index_to_filename:
+ filename = index_to_filename[index]
+ dir = os.path.dirname(filename)
+ if dir not in created_dirs:
+ os.makedirs(dir, exist_ok=True)
+ created_dirs.add(dir)
+ write_aiff(entry, filename)
+ return
+
+ # Generate aiff files
+
+ for sample_bank in sample_banks:
+ dir = os.path.join(samples_out_dir, sample_bank.name)
+ os.makedirs(dir, exist_ok=True)
+
+ offsets = sorted(set(sample_bank.entries.keys()))
+ print(sample_bank.name, len(offsets), 'entries')
+ offsets.append(len(sample_bank.data))
+
+ assert 0 in offsets
+
+ for offset, next_offset, index in zip(
+ offsets, offsets[1:], range(len(offsets))
+ ):
+ entry = sample_bank.entries[offset]
+ entry.name = "{:02X}".format(index)
+ size = next_offset - offset
+
+ assert size % 16 == 0
+ if not (size - 15 <= len(entry.data) <= size):
+ print(index, offset, size, len(entry.data))
+ # continue
+ # assert size - 15 <= len(entry.data) <= size
+ # if index % 10 == 0:
+ # print(index, offset, size, len(entry.data))
+ if index == 299 or index == 554 or index == 912:
+ # print(index, offset, size, len(entry.data))
+ continue
+ # garbage = sample_bank.data[offset + len(entry.data) : offset + size]
+ # if len(entry.data) % 2 == 1:
+ # assert garbage[0] == 0
+ # if next_offset != offsets[-1]:
+ # # (The last chunk follows a more complex garbage pattern)
+ # assert all(x == 0 for x in garbage)
+ filename = os.path.join(dir, entry.name + ".aiff")
+ write_aiff(entry, filename)
+
+ # Generate sound bank .json files
+ os.makedirs(banks_out_dir, exist_ok=True)
+ for bank_index, bank in enumerate(banks):
+ filename = os.path.join(banks_out_dir, bank.name + ".json")
+ with open(filename, "w") as out:
+
+ def sound_to_json(sound):
+ entry = bank.samples[sound.sample_addr]
+ if len(set(entry.tunings)) == 1:
+ return entry.name
+ return {"sample": entry.name, "tuning": round_f32(sound.tuning)}
+
+ bank_json = {
+ "date": bank.iso_date,
+ "sample_bank": bank.sample_bank.name,
+ "envelopes": {},
+ "instruments": {},
+ "instrument_list": [],
+ }
+ addr_to_name = {}
+
+ # Envelopes
+ for env in bank.envelopes.values():
+ env_json = []
+ for (delay, arg) in env.entries:
+ if delay == 0:
+ ins = "stop"
+ assert arg == 0
+ elif delay == 2 ** 16 - 1:
+ ins = "hang"
+ assert arg == 0
+ elif delay == 2 ** 16 - 2:
+ ins = ["goto", arg]
+ elif delay == 2 ** 16 - 3:
+ ins = "restart"
+ assert arg == 0
+ else:
+ ins = [delay, arg]
+ env_json.append(NoIndent(ins))
+ bank_json["envelopes"][env.name] = env_json
+
+ # Instruments/drums
+ for inst_index, inst in enumerate(bank.all_insts):
+ if isinstance(inst, Inst):
+ inst_json = {
+ "ifdef": inst_ifdef_json(bank_index, inst_index),
+ "release_rate": inst.release_rate,
+ "normal_range_lo": inst.normal_range_lo,
+ "normal_range_hi": inst.normal_range_hi,
+ "envelope": bank.envelopes[inst.envelope].name,
+ }
+
+ if inst_json["ifdef"] is None:
+ del inst_json["ifdef"]
+
+ if inst.sound_lo is not None:
+ inst_json["sound_lo"] = NoIndent(sound_to_json(inst.sound_lo))
+ else:
+ del inst_json["normal_range_lo"]
+
+ inst_json["sound"] = NoIndent(sound_to_json(inst.sound_med))
+
+ if inst.sound_hi is not None:
+ inst_json["sound_hi"] = NoIndent(sound_to_json(inst.sound_hi))
+ else:
+ del inst_json["normal_range_hi"]
+
+ bank_json["instruments"][inst.name] = inst_json
+ addr_to_name[inst.addr] = inst.name
+
+ else:
+ assert isinstance(inst, list)
+ drums_list_json = []
+ for drum in inst:
+ drum_json = {
+ "release_rate": drum.release_rate,
+ "pan": drum.pan,
+ "envelope": bank.envelopes[drum.envelope].name,
+ "sound": sound_to_json(drum.sound),
+ }
+ drums_list_json.append(NoIndent(drum_json))
+ bank_json["instruments"]["percussion"] = drums_list_json
+
+ # Instrument lists
+ for addr in bank.inst_list:
+ if addr is None:
+ bank_json["instrument_list"].append(None)
+ else:
+ bank_json["instrument_list"].append(addr_to_name[addr])
+
+ out.write(json.dumps(bank_json, indent=4, cls=NoIndentEncoder))
+ out.write("\n")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/format.py b/tools/format.py
index 6a414fad..c7b51218 100755
--- a/tools/format.py
+++ b/tools/format.py
@@ -186,7 +186,8 @@ def main():
files = args.files
extra_files = []
else:
- files = glob.glob("src/**/*.c", recursive=True)
+ files = glob.glob("src*/**/*.c", recursive=True)
+ files = [x for x in files if "assets" not in x]
extra_files = glob.glob("assets/**/*.xml", recursive=True)
format_files(files, extra_files, nb_jobs)
diff --git a/tools/permuter_settings.toml b/tools/permuter_settings.toml
index e9498d94..2729d6d3 100644
--- a/tools/permuter_settings.toml
+++ b/tools/permuter_settings.toml
@@ -23,6 +23,9 @@ compiler_type = "ido"
"RAND_RANGE" = "float"
"SIN_DEG" = "float"
"COS_DEG" = "float"
+"SIGN_OF" = "int"
+"ABS" = "int"
+"ABSF" = "float"
"true" = "int"
"false" = "int"
"DMG_.*" = "int"
@@ -32,6 +35,8 @@ compiler_type = "ido"
"ALIGN.*" = "int"
"OS_K0_TO_PHYSICAL" = "int"
"AUDIO_PLAY_SFX" = "void"
+"NA_.*" = "int"
+
[decompme.compilers]
"tools/ido-recomp/linux/cc" = "ido5.3" \ No newline at end of file
diff --git a/tools/progress.py b/tools/progress.py
index d840954c..1015ba6d 100755
--- a/tools/progress.py
+++ b/tools/progress.py
@@ -9,9 +9,9 @@ from colour import Color
script_dir = os.path.dirname(os.path.realpath(__file__))
root_dir = os.path.join(script_dir, "..")
-asm_dir = os.path.join(root_dir, "asm", "us", "nonmatchings")
+asm_dir = os.path.join(root_dir, "asm", "us", "rev1", "nonmatchings")
build_dir = os.path.join(root_dir, "build")
-elf_path = os.path.join(build_dir, "starfox64.us.elf")
+elf_path = os.path.join(build_dir, "starfox64.us.rev1.elf")
def get_func_sizes():
try:
diff --git a/tools/requirements-python.txt b/tools/requirements-python.txt
index 2c17d75f..b9e6d519 100644
--- a/tools/requirements-python.txt
+++ b/tools/requirements-python.txt
@@ -1,5 +1,5 @@
-spimdisasm==1.20.0
-rabbitizer==1.7.0
+spimdisasm==1.25.1
+rabbitizer==1.10.0
PyYAML
pylibyaml
tqdm
@@ -9,4 +9,5 @@ pygfxd
n64img>=0.1.4
GitPython
colour
-requests \ No newline at end of file
+requests
+crunch64 \ No newline at end of file
diff --git a/tools/splat b/tools/splat
-Subproject 0b0bfa6c99e3c757517d1630fcdb8b7dd34d979
+Subproject 441af8f682454154aae6d593d0938f23c747a1b