diff --git a/ff5reader.py b/ff5reader.py index 681c578..fd8940a 100755 --- a/ff5reader.py +++ b/ff5reader.py @@ -273,6 +273,8 @@ class FF5Reader(QMainWindow): #zone_pxs += make_zone_pxs2(blocks, miniblocks, blockmaps, z, zone_px_cache) print('Zone pixmap cache results: {misses} misses, {hits} hits'.format(**zone_px_cache)) perfcount() + del block_cache + del zone_px_cache print('Generating Battle backgrounds') battle_bgs = make_battle_backgrounds(ROM_jp) diff --git a/includes/helpers.py b/includes/helpers.py index 01be1dd..34c4814 100644 --- a/includes/helpers.py +++ b/includes/helpers.py @@ -17,18 +17,21 @@ HEX_PREFIX = '#' # '#' '$' or '0x' are also nice + def divceil(numerator, denominator): ''' Reverse floor division for fast ceil ''' return -(-numerator // denominator) + def hex_length(i): ''' String length of hexadecimal representation of integer ''' return divceil(i.bit_length(), 4) + def hex(num, digits=2): ''' Consolidate hex formatting for consistency @@ -36,11 +39,13 @@ def hex(num, digits=2): #return '{:0{}X}₁₆'.format(num, digits) return HEX_PREFIX + '{:0{}X}'.format(num, digits) -def indirect(rom, start, length=2): + +def indirect(rom, start, length=2, endian='little'): ''' Read little-endian value at start address in rom ''' - return int.from_bytes(rom[start:start+length], 'little') + return int.from_bytes(rom[start:start+length], endian) + def parse_struct(rom, offset, structure): ''' @@ -97,3 +102,81 @@ def decompress_lzss(rom, start, header=False, length=None): buffer_p = (buffer_p+1) % 0x800 offset = (offset+1) % 0x800 return bytes(output[:uncompressed_length]) + + +def decompress_lzss_FFVa(rom, start, header=False, length=None): + ''' + Oops, it's just GBA BIOS decompression functions + see https://web.archive.org/web/20130323133944/http://nocash.emubase.de/gbatek.htm#biosdecompressionfunctions + ''' + ptr = start + if length: + uncompressed_length = length + else: + uncompressed_length = indirect(rom, start, endian='big') + ptr += 2 + output = [] + while len(output) < uncompressed_length: + bitmap_byte = rom[ptr] + ptr += 1 + for i in reversed(range(8)): + bit = (bitmap_byte >> i) & 1 + if not bit: + b = rom[ptr] + ptr += 1 + output.append(b) + else: + b1 = rom[ptr] + b2 = rom[ptr+1] + ptr += 2 + length = ((b1 & 0xF0) >> 4) + 3 + trackback = -1 - (b2 + ((b1 & 0x0F) << 8)) + try: + for j in range(length): + output.append(output[trackback]) + except: + print(len(output), f'0x{ptr:X}', f'0x{b1:02X}{b2:02X}', trackback) + raise + print(f'0x{ptr:X}') + return bytes(output[:uncompressed_length]) + + +def findall(rom, string): + results = [] + start = 0 + while True: + val = rom.find(string, start) + if val < 0: + return results + results.append(val) + start = val + 1 + + +def parse_ips(data): + assert data[:5] == b'PATCH' and data[-3:] == b'EOF', 'File header and footer missing!' + patches = {} + ptr = 5 + while ptr < len(data)-6: + address = int.from_bytes(data[ptr:ptr+3], 'big') + length = int.from_bytes(data[ptr+3:ptr+5], 'big') + if length > 0: + payload = data[ptr+5:ptr+5+length] + ptr += 5 + length + else: + repeats = data[ptr+5:ptr+7] + payload = data[ptr+7] * repeats + ptr += 8 + patches[address] = payload + return patches + + +if __name__ == '__main__': + with open('2564 - Final Fantasy V Advance (U)(Independent).gba', 'rb') as file: + ROM = file.read() + landmark = ROM.find(b'FINAL FANTASY V ADVANCE SYGMAB') + try: + with open('Final Fantasy V Advance (Europe) (En,Fr,De,Es,It)-spritehack.ips', 'rb') as file: + spritehack_ips = file.read() + print('spritehack_ips loaded') + except: + pass diff --git a/spc700analyser.py b/spc700analyser.py index 43a019f..ecc51c9 100755 --- a/spc700analyser.py +++ b/spc700analyser.py @@ -26,6 +26,8 @@ import sys from midiutil import MIDIFile from includes.helpers import indirect, hex from includes.const import BGM_Tracks_Safe +import struct +import wave def generate_pointer_set(data): ''' @@ -51,14 +53,27 @@ def analyse_sample(data, pointer): def decode_brr(data): - range = data[0] >> 4 + ''' + Decodes a single 9byte BRR packet + ''' + _range = data[0] >> 4 filter_designation = (data[0] & 0x0C) >> 2 loop = bool(data[0] & 0x02) end = bool(data[0] & 0x01) samples = [] - for i in range(8): - samples.append((data[1+range] >> 4) << range) - samples.append((data[1+range] & 0x0F) << range) + for i in data[1:]: + b1 = (i >> 4) + b2 = (i & 0x0F) + # Sign-extend + if b1 >= 8: + b1 |= 0xFFF0 + if b2 >= 8: + b2 |= 0xFFF0 + samples.append((b1 << _range) & 0xFFFF) + samples.append((b2 << _range) & 0xFFFF) + # For filter arithmetic the samples need to be in signed form. + sample_bytes = struct.pack('<'+'H'*16, *samples) + samples = struct.unpack('<'+'h'*16, sample_bytes) return (samples, loop, end, filter_designation) @@ -275,6 +290,46 @@ def get_song_data(rom, id): #data = rom[offset+2:offset+2+size] return tracks +def get_sample_data(rom, id): + lookup_offset = 0x043C6F + (id*3) + offset = indirect(rom, lookup_offset, 3)-0xC00000 + size = indirect(rom, offset) + data = rom[offset+2:offset+2+size] + return data + +def clamp_short(num): + return min(max(num, -0x7FFF), 0x7FFF) + +def make_sample(rom, id): + data = get_sample_data(rom, id) + packets = [data[i:i+9] for i in range(0, len(data), 9)] + samples = [0, 0] # Two zero samples for filter purposes, strip them from the actual output + for p in packets: + c_samples, loop, end, filter = decode_brr(p) + samples += c_samples + if filter == 1: + for i in range(-8, 0, 1): + samples[i] = clamp_short(samples[i] + (samples[i-1]*15)//16) + elif filter == 2: + for i in range(-8, 0, 1): + samples[i] = clamp_short(samples[i] + (samples[i-1]*61)//32 - (samples[i-2]*15)//16 ) + elif filter == 3: + for i in range(-8, 0, 1): + samples[i] = clamp_short(samples[i] + (samples[i-1]*115)//64 - (samples[i-2]*13)//16 ) + if end: + break + return samples[2:] + +def make_sample_wav(rom, id): + samples = make_sample(rom, id) + filename = 'Sample{}.wav'.format(id) + with wave.open(filename, 'wb') as file: + file.setnchannels(1) + file.setframerate(8000) + file.setsampwidth(2) + sample_bytes = struct.pack('<'+'h'*len(samples), *samples) + file.writeframes(sample_bytes) + def make_midi_file(tracks, filename='test.mid'): m = SPCParser().parse(tracks) with open(filename, 'wb') as file: