summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordannye <33dannye@gmail.com>2019-08-18 23:32:36 -0500
committerdannye <33dannye@gmail.com>2019-08-18 23:32:36 -0500
commit37b9310dcb2d788365909000f5f64788c0fb9153 (patch)
treebf20af6bf3b8939ecd4e97984b4207fefc8b0d7e
parentbe6d6c9ce9a86231482eafacd4be867285809366 (diff)
Rewrite redmusicdisasm.py
-rwxr-xr-xpokemontools/redmusicdisasm.py492
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/")