diff options
Diffstat (limited to 'tools')
| m--------- | tools/Torch | 0 | ||||
| -rw-r--r-- | tools/aifc_decode | bin | 0 -> 22704 bytes | |||
| -rw-r--r-- | tools/aifc_decode.c | 669 | ||||
| -rw-r--r-- | tools/comptool.py | 299 | ||||
| -rw-r--r-- | tools/disassemble_sound.py | 867 | ||||
| -rwxr-xr-x | tools/format.py | 3 | ||||
| -rw-r--r-- | tools/permuter_settings.toml | 5 | ||||
| -rwxr-xr-x | tools/progress.py | 4 | ||||
| -rw-r--r-- | tools/requirements-python.txt | 7 | ||||
| m--------- | tools/splat | 0 |
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 Binary files differnew file mode 100644 index 00000000..0f6adb02 --- /dev/null +++ b/tools/aifc_decode 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 |
