diff options
-rwxr-xr-x | pokemontools/redmusicdisasm.py | 492 |
1 files changed, 254 insertions, 238 deletions
diff --git a/pokemontools/redmusicdisasm.py b/pokemontools/redmusicdisasm.py index 51b1901..482964d 100755 --- a/pokemontools/redmusicdisasm.py +++ b/pokemontools/redmusicdisasm.py @@ -1,9 +1,10 @@ -from __future__ import absolute_import -from . import configuration -config = configuration.Config() -rom = bytearray(open(config.rom_path, "r").read()) +#!/usr/bin/env python +rom = None + +# songs in RBY songs = [ + # song group 1 "PalletTown", "Pokecenter", "Gym", @@ -24,6 +25,7 @@ songs = [ "Routes3", "Routes4", "IndigoPlateau", + # song group 2 "GymLeaderBattle", "TrainerBattle", "WildBattle", @@ -31,6 +33,7 @@ songs = [ "DefeatedTrainer", "DefeatedWildMon", "DefeatedGymLeader", + # song group 3 "TitleScreen", "Credits", "HallOfFame", @@ -50,39 +53,73 @@ songs = [ "MeetFemaleTrainer", "MeetMaleTrainer", "UnusedSong", - ] -""" -songs = [ +] + +# songs exclusively in Yellow +songs_yellow = [ + # song group 4 "YellowIntro", + # song group 5 "SurfingPikachu", "MeetJessieJames", "YellowUnusedSong", - ] -""" +] + +# starting addresses of all 5 song groups +header_addresses = { + "PalletTown": 0x0822e, + "GymLeaderBattle": 0x202be, + "TitleScreen": 0x7c249, + "YellowIntro": 0x7c294, + "SurfingPikachu": 0x801cb, +} + +# songs with an alternate start not pointed to by a song header +alternate_start_songs = [ + "Cities1", + "MeetRival", +] + +# music command names and parameter lists music_commands = { - 0xd0: ["notetype", {"type": "nibble"}, 2], - 0xe0: ["octave", 1], - 0xe8: ["toggleperfectpitch", 1], - 0xea: ["vibrato", {"type": "byte"}, {"type": "nibble"}, 3], - 0xeb: ["pitchbend", {"type": "byte"}, {"type": "byte"}, 3], - 0xec: ["duty", {"type": "byte"}, 2], - 0xed: ["tempo", {"type": "word"}, 3], - 0xee: ["stereopanning", {"type": "byte"}, 2], - 0xf0: ["volume", {"type": "nibble"}, 2], - 0xf8: ["executemusic", 1], - 0xfc: ["dutycycle", {"type": "byte"}, 2], - 0xfd: ["callchannel", {"type": "label"}, 3], - 0xfe: ["loopchannel", {"type": "byte"}, {"type": "label"}, 4], - 0xff: ["endchannel", 1], - } + 0x00: { "name": "note", "params": [ "note", "lower_nibble_off_by_one" ] }, + 0xb0: { "name": "dnote", "params": [ "byte", "lower_nibble_off_by_one" ] }, + 0xc0: { "name": "rest", "params": [ "lower_nibble_off_by_one" ] }, + 0xd0: { "name": "note_type", "params": [ "lower_nibble", "nibbles_unsigned_signed" ] }, + 0xd1: { "name": "dspeed", "params": [ "lower_nibble" ] }, + 0xe0: { "name": "octave", "params": [ "octave" ] }, + 0xe8: { "name": "toggle_perfect_pitch", "params": [] }, + 0xea: { "name": "vibrato", "params": [ "byte", "nibbles" ] }, + 0xeb: { "name": "pitch_slide", "params": [ "byte_off_by_one", "nibbles_octave_note" ] }, + 0xec: { "name": "duty_cycle", "params": [ "byte" ] }, + 0xed: { "name": "tempo", "params": [ "word" ] }, + 0xee: { "name": "stereo_panning", "params": [ "nibbles_binary" ] }, + 0xf0: { "name": "volume", "params": [ "nibbles" ] }, + 0xf8: { "name": "execute_music", "params": [] }, + 0xfc: { "name": "duty_cycle_pattern", "params": [ "crumbs" ] }, + 0xfd: { "name": "sound_call", "params": [ "label" ] }, + 0xfe: { "name": "sound_loop", "params": [ "byte", "label" ] }, + 0xff: { "name": "sound_ret", "params": [] }, +} +# length in bytes of each type of parameter param_lengths = { - "nibble": 1, - "byte": 1, - "word": 2, - "label": 2, - } + "note": 0, + "lower_nibble": 0, + "lower_nibble_off_by_one": 0, + "octave": 0, + "crumbs": 1, + "nibbles": 1, + "nibbles_binary": 1, + "nibbles_unsigned_signed": 1, + "nibbles_octave_note": 1, + "byte": 1, + "byte_off_by_one": 1, + "word": 2, + "label": 2, +} +# constants used for note commands music_notes = { 0x0: "C_", 0x1: "C#", @@ -96,222 +133,201 @@ music_notes = { 0x9: "A_", 0xa: "A#", 0xb: "B_", - } +} + +# get length in bytes of a music command by ID +# returns 1 (command ID) + length of all params +def get_command_length(command_id): + length = 1 + for param in music_commands[command_id]["params"]: + length += param_lengths[param] + return length + +def get_base_command_id(command_id, channel): + # dnote + if command_id < 0xc0 and channel == 4: + return 0xb0 + # dspeed + elif command_id >= 0xd0 and command_id < 0xe0 and channel == 4: + return 0xd1 + # note + elif command_id < 0xc0: + return 0x00 + # rest + elif command_id < 0xd0: + return 0xc0 + # notetype + elif command_id < 0xe0: + return 0xd0 + # octave + elif command_id < 0xe8: + return 0xe0 + else: + return command_id + +# get absolute pointer stored at an address in the rom +# assumes the pointer refers to the same bank as the bank it is located in +def get_pointer(address): + bank = int(address / 0x4000) + return (rom[address + 1] * 0x100 + rom[address]) % 0x4000 + bank * 0x4000 + +# return True if the command at address is a loop command +# and the loop count is 0 (infinite) +# and the destination address is "backwards" (less than the command address) +def is_infinite_loop(address): + return rom[address] == 0xfe and rom[address + 1] == 0 and get_pointer(address + 2) <= address -def printnoisechannel(songname, songfile, startingaddress, bank, output): - noise_commands = { - 0xfd: ["callchannel", {"type": "label"}, 3], - 0xfe: ["loopchannel", {"type": "byte"}, {"type": "label"}, 4], - 0xff: ["endchannel", 1], - } - - noise_instruments = { - 0x01: "snare1", - 0x02: "snare2", - 0x03: "snare3", - 0x04: "snare4", - 0x05: "snare5", - 0x06: "triangle1", - 0x07: "triangle2", - 0x08: "snare6", - 0x09: "snare7", - 0x0a: "snare8", - 0x0b: "snare9", - 0x0c: "cymbal1", - 0x0d: "cymbal2", - 0x0e: "cymbal3", - 0x0f: "mutedsnare1", - 0x10: "triangle3", - 0x11: "mutedsnare2", - 0x12: "mutedsnare3", - 0x13: "mutedsnare4", - } - +# scan a single channel of a song +# returns: a set of all unique addresses pointed to by calls and loops +# and the end address of the channel +def scan_for_labels(start_address, song_name, channel, final_channel, header): # pass 1, build a list of all addresses pointed to by calls and loops - address = startingaddress - labels = [] - labelsleft= [] + address = start_address + all_labels = set() + future_labels = set() + # MeetRival has some labels that cannot be seen by calls and loops + if song_name == "MeetRival": + if channel == 1: + all_labels.add(0xb19b) + future_labels.add(0xb19b) + all_labels.add(0xb1a2) + future_labels.add(0xb1a2) + if channel == 2: + all_labels.add(0xb21d) + future_labels.add(0xb21d) + if channel == 3: + all_labels.add(0xb2b5) + future_labels.add(0xb2b5) while 1: - byte = rom[address] - if byte < 0xc0: - command_length = 2 - elif byte < 0xe0: - command_length = 1 - else: - command_length = noise_commands[byte][-1] - if byte == 0xfd or byte == 0xfe: - label = rom[address + command_length - 1] * 0x100 + rom[address + command_length - 2] - labels.append(label) - if label > address % 0x4000 + 0x4000: labelsleft.append(label) + command_id = get_base_command_id(rom[address], channel) + command_length = get_command_length(command_id) + # if call or loop + if command_id == 0xfd or command_id == 0xfe: + label = get_pointer(address + command_length - 2) + all_labels.add(label) + if label > address: + future_labels.add(label) + future_labels.discard(address) address += command_length - if len(labelsleft) == 0 and (byte == 0xfe and rom[address - command_length + 1] == 0 and rom[address - 1] * 0x100 + rom[address - 2] < address % 0x4000 + 0x4000 or byte == 0xff): break - while address % 0x4000 + 0x4000 in labelsleft: labelsleft.remove(address % 0x4000 + 0x4000) - # once the loop ends, start over from first address - if rom[address] == 0xff: address += 1 - end = address - address = startingaddress - byte = rom[address] - output += "Music_{}_Ch4:: ; {:02x} ({:0x}:{:02x})\n".format(songname, address, bank, address % 0x4000 + 0x4000) + # we are only finished when there are no more unvisited labels + # and we hit an infinite loop or a ret command + # and also channel 1 of an alternate start song must advance at least 7 bytes + if (len(future_labels) == 0 and + (is_infinite_loop(address - command_length) or command_id == 0xff) and + (song_name not in alternate_start_songs or channel != 1 or address > start_address + 7)): + break + # some songs have an extra ret command after an infinite loop + if rom[address] == 0xff and song_name != "MeetJessieJames": + address += 1 + # if this is not the last channel of the song, + # then the end address is simply the start address of the next channel + # otherwise, use the computed end address + if channel != final_channel and song_name != "UnusedSong": + address = get_pointer(header + 4) + return all_labels, address + +# using the list of labels and end address from pass 1, parse a single channel of a song +# returns a string of all labels and commands +def dump_channel(start_address, end_address, song_name, channel, labels): + address = start_address + output = "" + # if song has an alternate start to channel 1, print a label and set start_address to true channel start + if song_name in alternate_start_songs and channel == 1: + output += "Music_{}_branch_{:x}::\n".format(song_name, address) + start_address += 7 # pass 2, print commands and labels for addresses that are in labels - while address != end: - if address % 0x4000 + 0x4000 in labels and address != startingaddress: - output += "\nMusic_{}_branch_{:02x}::\n".format(songname, address) - if byte < 0xc0: - output += "\t{} {}".format(noise_instruments[rom[address + 1]], byte % 0x10 + 1) - command_length = 2 - elif byte < 0xd0: - output += "\trest {}".format(byte % 0x10 + 1) - command_length = 1 - elif byte < 0xe0: - output += "\tdspeed {}".format(byte % 0x10) - command_length = 1 - else: - command = noise_commands[byte] - output += "\t{}".format(command[0]) - command_length = 1 - params = 1 - # print all params for current command - while params != len(noise_commands[byte]) - 1: - param_type = noise_commands[byte][params]["type"] - address += command_length - command_length = param_lengths[param_type] - param = rom[address] - if param_type == "byte": - output += " {}".format(param) + while address != end_address: + if address == start_address: + if song_name in alternate_start_songs and channel == 1: + output += "\n" + output += "Music_{}_Ch{}::\n".format(song_name, channel) + elif address in labels: + output += "\nMusic_{}_branch_{:x}::\n".format(song_name, address) + command_id = rom[address] + command = music_commands[get_base_command_id(command_id, channel)] + output += "\t{}".format(command["name"]) + address += 1 + # print all params for current command + for i in range(len(command["params"])): + param = rom[address] + param_type = command["params"][i] + param_length = param_lengths[param_type] + if param_type == "note": + output += " {}".format(music_notes[command_id >> 4]) + elif param_type == "lower_nibble": + output += " {}".format(command_id & 0b1111) + elif param_type == "lower_nibble_off_by_one": + output += " {}".format((command_id & 0b1111) + 1) + elif param_type == "octave": + output += " {}".format(8 - (command_id & 0b1111)) + elif param_type == "crumbs": + output += " {}, {}, {}, {}".format((param >> 6) & 0b11, (param >> 4) & 0b11, (param >> 2) & 0b11, (param >> 0) & 0b11) + elif param_type == "nibbles": + output += " {}, {}".format(param >> 4, param & 0b1111) + elif param_type == "nibbles_binary": + output += " %{:b}, %{:b}".format(param >> 4, param & 0b1111) + elif param_type == "nibbles_unsigned_signed": + output += " {}, {}".format(param >> 4, (param & 0b0111) * (-1 if param & 0b1000 else 1)) + elif param_type == "nibbles_octave_note": + output += " {}, {}".format(8 - (param >> 4), music_notes[param & 0b1111]) + elif param_type == "byte": + output += " {}".format(param) + elif param_type == "byte_off_by_one": + output += " {}".format(param + 1) + elif param_type == "word": + output += " {}".format(param * 0x100 + rom[address + 1]) + elif param_type == "label": + param = get_pointer(address) + if param == start_address: + output += " Music_{}_Ch{}".format(song_name, channel) else: - param += rom[address + 1] * 0x100 - 0x4000 + (bank * 0x4000) - if param == startingaddress: output += " Music_{}_Ch4".format(songname) - else: output += " Music_{}_branch_{:02x}".format(songname, param) - params += 1 - if params != len(noise_commands[byte]) - 1: output += "," + output += " Music_{}_branch_{:x}".format(song_name, param) + address += param_length + if i < len(command["params"]) - 1: + output += "," output += "\n" - address += command_length - byte = rom[address] - output += "; {}\n".format(hex(address)) - songfile.write(output) + return output -for i, songname in enumerate(songs): - songfile = open("music/" + songname.lower() + ".asm", 'a') - if songname == "PalletTown": header = 0x822e - if songname == "GymLeaderBattle": header = 0x202be - if songname == "TitleScreen": header = 0x7c249 - if songname == "YellowIntro": header = 0x7c294 - if songname == "SurfingPikachu": header = 0x801cb - bank = header / 0x4000 - startingaddress = rom[header + 2] * 0x100 + rom[header + 1] - 0x4000 + (0x4000 * bank) - curchannel = 1 - lastchannel = (rom[header] >> 6) + 1 - exception = False - if songname == "MeetRival" or songname == "Cities1": - startingaddress -= 7 - exception = True - if songname == "UnusedSong": - bank = 2 - startingaddress = 0xa913 - lastchannel = 2 - output = '' - while 1: - # pass 1, build a list of all addresses pointed to by calls and loops - address = startingaddress - labels = [] - labelsleft = [] - if songname == "MeetRival": - if curchannel == 1: - labels.append(0x719b) - labelsleft.append(0x719b) - labels.append(0x71a2) - labelsleft.append(0x71a2) - if curchannel == 2: - labels.append(0x721d) - labelsleft.append(0x721d) - if curchannel == 3: - labels.append(0x72b5) - labelsleft.append(0x72b5) +def dump_all_songs(song_names, path): + for song_name in song_names: + if song_name in header_addresses: + header = header_addresses[song_name] + # UnusedSong does not have a header + if song_name == "UnusedSong": + final_channel = 2 + start_address = 0xa913 + else: + final_channel = (rom[header] >> 6) + 1 + start_address = get_pointer(header + 1) + if song_name in alternate_start_songs: + start_address -= 7 + cur_channel = 1 + output = "" while 1: - byte = rom[address] - if byte < 0xd0: - command_length = 1 - elif byte < 0xe0: - command_length = 2 - elif byte < 0xe8: - command_length = 1 - else: - command_length = music_commands[byte][-1] - if byte == 0xfd or byte == 0xfe: - label = rom[address + command_length - 1] * 0x100 + rom[address + command_length - 2] - labels.append(label) - if label > address % 0x4000 + 0x4000: labelsleft.append(label) - address += command_length - if len(labelsleft) == 0 and (exception == False or address > startingaddress + 7) and (byte == 0xfe and rom[address - command_length + 1] == 0 and rom[address - 1] * 0x100 + rom[address - 2] < address % 0x4000 + 0x4000 or byte == 0xff): break - while address % 0x4000 + 0x4000 in labelsleft: labelsleft.remove(address % 0x4000 + 0x4000) - # once the loop breaks, start over from first address - if rom[address] == 0xff: address += 1 - end = address - if curchannel != lastchannel and songname != "UnusedSong": end = rom[header + 5] * 0x100 + rom[header + 4] + (0x4000 * (bank - 1)) - address = startingaddress - byte = rom[address] - # if song has an alternate start to channel 1, print a label and set startingaddress to true channel start - if exception: - output += "Music_{}_branch_{:02x}::\n".format(songname, address) - startingaddress += 7 - # pass 2, print commands and labels for addresses that are in labels - while address != end: - if address == startingaddress: - if exception: output += "\n" - output += "Music_{}_Ch{}:: ; {:02x} ({:0x}:{:02x})\n".format(songname, curchannel, address, bank, address % 0x4000 + 0x4000) - elif address % 0x4000 + 0x4000 in labels: - output += "\nMusic_{}_branch_{:02x}::\n".format(songname, address) - if byte < 0xc0: - output += "\t{} {}".format(music_notes[byte >> 4], byte % 0x10 + 1) - command_length = 1 - elif byte < 0xd0: - output += "\trest {}".format(byte % 0x10 + 1) - command_length = 1 - else: - if byte < 0xe0: - command = music_commands[0xd0] - output += "\t{} {},".format(command[0], byte % 0x10) - byte = 0xd0 - elif byte < 0xe8: - command = music_commands[0xe0] - output += "\t{} {}".format(command[0], 0xe8 - byte) - byte = 0xe0 - else: - command = music_commands[byte] - output += "\t{}".format(command[0]) - command_length = 1 - params = 1 - # print all params for current command - while params != len(music_commands[byte]) - 1: - param_type = music_commands[byte][params]["type"] - address += command_length - command_length = param_lengths[param_type] - param = rom[address] - if param_type == "nibble": - output += " {}, {}".format(param >> 4, param % 0x10) - elif param_type == "byte": - output += " {}".format(param) - elif param_type == "word": - output += " {}".format(param * 0x100 + rom[address + 1]) - else: - param += rom[address + 1] * 0x100 - 0x4000 + (bank * 0x4000) - if param == startingaddress: output += " Music_{}_Ch{}".format(songname, curchannel) - else: output += " Music_{}_branch_{:02x}".format(songname, param) - params += 1 - if params != len(music_commands[byte]) - 1: output += "," - output += "\n" - address += command_length - byte = rom[address] - header += 3 - if curchannel == lastchannel: - output += "; {}\n".format(hex(address)) - songfile.write(output) - break - curchannel += 1 - output += "\n\n" - startingaddress = end - exception = False - if curchannel == 4: - printnoisechannel(songname, songfile, startingaddress, bank, output) + labels, end_address = scan_for_labels(start_address, song_name, cur_channel, final_channel, header) + output += dump_channel(start_address, end_address, song_name, cur_channel, labels) header += 3 - break
\ No newline at end of file + if cur_channel == final_channel: + break + cur_channel += 1 + output += "\n\n" + start_address = end_address + song_file = open(path + song_name.lower() + ".asm", "w") + song_file.write(output) + song_file.close() + +def dump_all_songs_from_rom(rom_file, song_names, path): + import os + global rom + try: + print("Parsing {}...".format(rom_file)) + rom = bytearray(open(rom_file, "rb").read()) + os.makedirs(path, exist_ok=True) + dump_all_songs(song_names, path) + except IOError as ex: + print("Error parsing {}".format(rom_file)) + print(ex) + +if __name__ == "__main__": + dump_all_songs_from_rom("pokered.gbc", songs, "audio/music/") + dump_all_songs_from_rom("pokeyellow.gbc", songs_yellow, "audio/music/yellow/") |