diff options
-rw-r--r-- | pokemontools/audio.py | 308 | ||||
-rw-r--r-- | pokemontools/crystal.py | 2 | ||||
-rw-r--r-- | pokemontools/labels.py | 2 | ||||
-rw-r--r-- | pokemontools/sfx_names.py | 6 |
4 files changed, 237 insertions, 81 deletions
diff --git a/pokemontools/audio.py b/pokemontools/audio.py index 0cdfcbc..896ceef 100644 --- a/pokemontools/audio.py +++ b/pokemontools/audio.py @@ -4,6 +4,10 @@ import os from math import ceil +from song_names import song_names +from sfx_names import sfx_names +from cry_names import cry_names + from gbz80disasm import get_global_address, get_local_address from labels import line_has_label from crystal import music_classes as sound_classes @@ -48,7 +52,52 @@ def sort_asms(asms): Format: [(address, asm, last_address), ...] """ - return sorted(set(asms), key=asm_sort) + asms = sorted(set(asms), key=asm_sort) + trimmed = [] + address, last_address = None, None + for asm in asms: + if asm == (address, asm[1], last_address) and last_address - address: + continue + trimmed += [asm] + address, last_address = asm[0], asm[2] + return trimmed + +def insert_asm_incbins(asms): + """ + Insert baserom incbins between address gaps in asm lists. + """ + new_asms = [] + for i, asm in enumerate(asms): + new_asms += [asm] + if i + 1 < len(asms): + last_address, next_address = asm[2], asms[i + 1][0] + if last_address < next_address and last_address / 0x4000 == next_address / 0x4000: + new_asms += [generate_incbin_asm(last_address, next_address)] + return new_asms + +def generate_incbin_asm(start_address, end_address): + """ + Return baserom incbin text for an address range. + + Format: 'INCBIN "baserom.gbc", {start}, {end} - {start}' + """ + incbin = ( + start_address, + '\nINCBIN "baserom.gbc", $%x, $%x - $%x\n\n' % ( + start_address, end_address, start_address + ), + end_address + ) + return incbin + +def generate_label_asm(label, address): + """ + Return label definition text at a given address. + + Format: '{label}: ; {address}' + """ + label_text = '%s: ; %x' % (label, address) + return (address, label_text, address) class NybbleParam: @@ -147,30 +196,52 @@ class Note(Command): return True -class Noise(Note): - macro_name = "noise" +class SoundCommand(Note): + macro_name = "sound" end = False param_types = { - 0: {"name": "duration", "class": LoNybbleParam}, + 0: {"name": "duration", "class": SingleByteParam}, 1: {"name": "intensity", "class": SingleByteParam}, 2: {"name": "frequency", "class": MultiByteParam}, } - allowed_lengths = [2,3] + allowed_lengths = [3] override_byte_check = True is_rgbasm_macro = False +class Noise(SoundCommand): + macro_name = "noise" + param_types = { + 0: {"name": "duration", "class": SingleByteParam}, + 1: {"name": "intensity", "class": SingleByteParam}, + 2: {"name": "frequency", "class": SingleByteParam}, + } class Channel: """A sound channel data parser.""" - def __init__(self, address, channel=1, base_label='Sound'): + def __init__(self, address, channel=1, base_label=None, sfx=False, label=None, used_labels=[]): self.start_address = address self.address = address self.channel = channel + self.base_label = base_label - self.output = [] + if self.base_label == None: + self.base_label = 'Sound_' + hex(self.start_address) + + self.label = label + if self.label == None: + self.label = self.base_label + + self.sfx = sfx + + self.used_labels = used_labels self.labels = [] + used_label = generate_label_asm(self.label, self.start_address) + self.labels += [used_label] + self.used_labels += [used_label] + + self.output = [] self.parse() def parse(self): @@ -194,6 +265,9 @@ class Channel: del class_.params[class_.size - 1] noise = not noise + elif class_.macro_name == 'togglesfx': + self.sfx = not self.sfx + asm = class_.to_asm() # label any jumps or calls @@ -204,12 +278,7 @@ class Channel: self.base_label, label_address ) - label_output = ( - label_address, - '\n%s: ; %x' % (label, label_address), - label_address - ) - self.labels += [label_output] + self.labels += [generate_label_asm(label, label_address)] asm = asm.replace( '$%x' % (get_local_address(label_address)), label @@ -224,19 +293,35 @@ class Channel: if class_.params[0].byte == 0: done = True - # keep going past enders if theres more to parse - if any(self.address <= address for address, asm, last_address in self.output + self.labels): - if done: - self.output += [(self.address, '; %x' % self.address, self.address)] - done = False - # dumb safety checks if ( self.address >= len(rom) or self.address / 0x4000 != self.start_address / 0x4000 ) and not done: done = True - raise Exception, 'reached the end of the bank without finishing!' + raise Exception, self.label + ': reached the end of the bank without finishing!' + + self.output += [(self.address, '; %x\n' % self.address, self.address)] + + # parse any other branches too + self.labels = list(set(self.labels)) + for address, asm, last_address in self.labels: + if ( + address >= self.address + and (address, asm, last_address) not in self.used_labels + ): + + self.used_labels += [(address, asm, last_address)] + sub = Channel( + address=address, + channel=self.channel, + base_label=self.base_label, + label=asm.split(':')[0], + used_labels=self.used_labels, + sfx=self.sfx, + ) + self.output += sub.output + self.labels += sub.labels def to_asm(self): output = sort_asms(self.output + self.labels) @@ -257,17 +342,23 @@ class Channel: for class_ in sound_classes: if class_.id == i: return class_ - if self.channel == 8: return Noise + if self.sfx: + if self.channel in [4, 8]: + return Noise + return SoundCommand return Note class Sound: - """Interprets a sound data header.""" + """ + Interprets a sound data header and its channel data. + """ - def __init__(self, address, name=''): + def __init__(self, address, name='', sfx=False): self.start_address = address self.bank = address / 0x4000 self.address = address + self.sfx = sfx self.name = name self.base_label = 'Sound_%x' % self.start_address @@ -288,36 +379,31 @@ class Sound: address = rom[self.address] + rom[self.address + 1] * 0x100 address = self.bank * 0x4000 + address % 0x4000 self.address += 2 - channel = Channel(address, current_channel, self.base_label) + channel = Channel(address, current_channel, self.base_label, self.sfx, label='%s_Ch%d' % (self.base_label, current_channel)) self.channels += [(current_channel, channel)] - self.labels += channel.labels - label_text = '\n%s_Ch%d: ; %x' % ( - self.base_label, - current_channel, - channel.start_address - ) - label_output = (channel.start_address, label_text, channel.start_address) - self.labels += [label_output] - asms = [] - text = '%s: ; %x' % (self.base_label, self.start_address) + '\n' + asms += [generate_label_asm(self.base_label, self.start_address)] + for i, (num, channel) in enumerate(self.channels): channel_id = num - 1 if i == 0: channel_id += (len(self.channels) - 1) << 6 - text += '\tdbw $%.2x, %s_Ch%d' % (channel_id, self.base_label, num) + '\n' - text += '; %x\n' % self.address - asms += [(self.start_address, text, self.start_address + len(self.channels) * 3)] + address = self.start_address + i * 3 + text = '\tdbw $%.2x, %s_Ch%d' % (channel_id, self.base_label, num) + asms += [(address, text, address + 3)] + + comment_text = '; %x\n' % self.address + asms += [(self.address, comment_text, self.address)] for num, channel in self.channels: asms += channel.output asms = sort_asms(asms) self.last_address = asms[-1][2] - asms += [(self.last_address,'; %x' % self.last_address, self.last_address)] + asms += [(self.last_address,'; %x\n' % self.last_address, self.last_address)] self.asms += asms @@ -325,14 +411,9 @@ class Sound: """insert outside labels here""" asms = self.asms - # incbins dont really count as parsed data - incbins = [] - for i, (address, asm, last_address) in enumerate(asms): - if i + 1 < len(asms): - next_address = asms[i + 1][0] - if last_address != next_address: - incbins += [(last_address, 'INCBIN "baserom.gbc", $%x, $%x - $%x' % (last_address, next_address, last_address), next_address)] - asms += incbins + # Incbin trailing commands that didnt get picked up + asms = insert_asm_incbins(asms) + for label in self.labels + labels: if self.start_address <= label[0] < self.last_address: asms += [label] @@ -341,38 +422,48 @@ class Sound: def read_bank_address_pointer(addr): + """ + Return a bank and address at a given rom offset. + """ bank, address = rom[addr], rom[addr+1] + rom[addr+2] * 0x100 return get_global_address(address, bank) def dump_sounds(origin, names, base_label='Sound_'): - """Dump sound data from a pointer table.""" + """ + Dump sound data from a pointer table. + """ + + # Some songs share labels. + # Do an extra pass to grab shared labels before writing output. - # first pass to grab labels and boundaries + sounds = [] labels = [] addresses = [] for i, name in enumerate(names): sound_at = read_bank_address_pointer(origin + i * 3) sound = Sound(sound_at, base_label + name) + sounds += [sound] labels += sound.labels - addresses += [(sound.start_address, sound.last_address)] - addresses = sorted(addresses) + addresses += [sound_at] + addresses.sort() outputs = [] for i, name in enumerate(names): - sound_at = read_bank_address_pointer(origin + i * 3) - sound = Sound(sound_at, base_label + name) - output = sound.to_asm(labels) + '\n' - # incbin trailing commands that didnt get picked up - index = addresses.index((sound.start_address, sound.last_address)) - if index + 1 < len(addresses): - next_address = addresses[index + 1][0] - if 5 > next_address - sound.last_address > 0: - if next_address / 0x4000 == sound.last_address / 0x4000: - output += '\nINCBIN "baserom.gbc", $%x, $%x - $%x\n' % (sound.last_address, next_address, sound.last_address) + sound = sounds[i] + + # Place a dummy asm at the end to catch end-of-file incbins. + index = addresses.index(sound.start_address) + 1 + if index < len(addresses): + next_address = addresses[index] + max_command_length = 5 + if next_address - sound.last_address <= max_command_length: + sound.asms += [(next_address, '', next_address)] + output = sound.to_asm(labels) + '\n' filename = name.lower() + '.asm' outputs += [(filename, output)] + return outputs @@ -382,50 +473,117 @@ def export_sounds(origin, names, path, base_label='Sound_'): out.write(output) -def dump_sound_clump(origin, names, base_label='Sound_'): - """some sounds are grouped together and/or share most components. - these can't reasonably be split into files for each sound.""" +def dump_sound_clump(origin, names, base_label='Sound_', sfx=False): + """ + Some sounds are grouped together and/or share most components. + These can't reasonably be split into separate files for each sound. + """ output = [] for i, name in enumerate(names): sound_at = read_bank_address_pointer(origin + i * 3) - sound = Sound(sound_at, base_label + name) + sound = Sound(sound_at, base_label + name, sfx) output += sound.asms + sound.labels output = sort_asms(output) return output -def export_sound_clump(origin, names, path, base_label='Sound_'): - output = dump_sound_clump(origin, names, base_label) +def export_sound_clump(origin, names, path, base_label='Sound_', sfx=False): + """ + Dump and export a sound clump to a given file path. + """ + output = dump_sound_clump(origin, names, base_label, sfx) + output = insert_asm_incbins(output) with open(path, 'w') as out: out.write('\n'.join(asm for address, asm, last_address in output)) def dump_crystal_music(): - from song_names import song_names + """ + Dump and export Pokemon Crystal music to files in audio/music/. + """ export_sounds(0xe906e, song_names, os.path.join(conf.path, 'audio', 'music'), 'Music_') def generate_crystal_music_pointers(): - from song_names import song_names + """ + Return a pointer table for Pokemon Crystal music. + """ return '\n'.join('\tdbw BANK({0}), {0}'.format('Music_' + label) for label in song_names) def dump_crystal_sfx(): - from sfx_names import sfx_names - export_sound_clump(0xe927c, sfx_names, os.path.join(conf.path, 'audio', 'sfx.asm'), 'Sfx_') + """ + Dump and export Pokemon Crystal sound effects to audio/sfx.asm and audio/sfx_crystal.asm. + """ + sfx_pointers_address = 0xe927c + + sfx = dump_sound_clump(sfx_pointers_address, sfx_names, 'Sfx_', sfx=True) + + unknown_sfx = Sound(0xf0d5f, 'UnknownSfx', sfx=True) + sfx += unknown_sfx.asms + unknown_sfx.labels + + sfx = sort_asms(sfx) + sfx = insert_asm_incbins(sfx) + + # Split up sfx and crystal sfx. + crystal_sfx = None + for i, asm in enumerate(sfx): + address, content, last_address = asm + if i + 1 < len(sfx): + next_address = sfx[i + 1][0] + if next_address > last_address and last_address / 0x4000 != next_address / 0x4000: + crystal_sfx = sfx[i + 1:] + sfx = sfx[:i + 1] + break + if crystal_sfx: + path = os.path.join(conf.path, 'audio', 'sfx_crystal.asm') + with open(path, 'w') as out: + out.write('\n'.join(asm for address, asm, last_address in crystal_sfx)) + + path = os.path.join(conf.path, 'audio', 'sfx.asm') + with open(path, 'w') as out: + out.write('\n'.join(asm for address, asm, last_address in sfx)) + def generate_crystal_sfx_pointers(): - from sfx_names import sfx_names - return '\n'.join('\tdbw BANK({0}), {0}'.format('Sfx_' + label) for label in sfx_names) + """ + Return a pointer table for Pokemon Crystal sound effects. + """ + lines = ['\tdbw BANK({0}), {0}'.format('Sfx_' + label) for label in sfx_names] + first_crystal_sfx = 190 + lines = lines[:first_crystal_sfx] + ['\n; Crystal adds the following SFX:\n'] + lines[first_crystal_sfx:] + return '\n'.join(lines) def dump_crystal_cries(): - from cry_names import cry_names - export_sound_clump(0xe91b0, cry_names, os.path.join(conf.path, 'audio', 'cries.asm'), 'Cry_') + """ + Dump and export Pokemon Crystal cries to audio/cries.asm. + """ + path = os.path.join(conf.path, 'audio', 'cries.asm') + + cries = dump_sound_clump(0xe91b0, cry_names, 'Cry_', sfx=True) + + # Unreferenced cry channel data. + cry_2e_ch8 = Channel(0xf3134, channel=8, sfx=True).output + [generate_label_asm('Cry_2E_Ch8', 0xf3134)] + unknown_cry_ch5 = Channel(0xf35d3, channel=5, sfx=True).output + [generate_label_asm('Unknown_Cry_Ch5', 0xf35d3)] + unknown_cry_ch6 = Channel(0xf35ee, channel=6, sfx=True).output + [generate_label_asm('Unknown_Cry_Ch6', 0xf35ee)] + unknown_cry_ch8 = Channel(0xf3609, channel=8, sfx=True).output + [generate_label_asm('Unknown_Cry_Ch8', 0xf3609)] + + cries += cry_2e_ch8 + unknown_cry_ch5 + unknown_cry_ch6 + unknown_cry_ch8 + cries = sort_asms(cries) + cries = insert_asm_incbins(cries) + + with open(path, 'w') as out: + out.write('\n'.join(asm for address, asm, last_address in cries)) + def generate_crystal_cry_pointers(): - from cry_names import cry_names + """ + Return a pointer table for Pokemon Crystal cries. + """ return '\n'.join('\tdbw BANK({0}), {0}'.format('Cry_' + label) for label in cry_names) if __name__ == '__main__': dump_crystal_music() + dump_crystal_cries() + dump_crystal_sfx() diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py index f663a87..7cfab85 100644 --- a/pokemontools/crystal.py +++ b/pokemontools/crystal.py @@ -2460,7 +2460,7 @@ music_commands = { 0xDC: ["intensity", ["intensity", SingleByteParam]], 0xDD: ["soundinput", ["input", SingleByteParam]], 0xDE: ["unknownmusic0xde", ["unknown", SingleByteParam]], - 0xDF: ["unknownmusic0xdf"], + 0xDF: ["togglesfx"], 0xE0: ["unknownmusic0xe0", ["unknown", SingleByteParam], ["unknown", SingleByteParam]], 0xE1: ["vibrato", ["delay", SingleByteParam], ["extent", SingleByteParam]], 0xE2: ["unknownmusic0xe2", ["unknown", SingleByteParam]], diff --git a/pokemontools/labels.py b/pokemontools/labels.py index 87e9990..72700a5 100644 --- a/pokemontools/labels.py +++ b/pokemontools/labels.py @@ -191,8 +191,6 @@ def line_has_label(line): return False if line[0] == "\"": return False - if "::" in line: - return False return True def get_label_from_line(line): diff --git a/pokemontools/sfx_names.py b/pokemontools/sfx_names.py index f7da967..18bda53 100644 --- a/pokemontools/sfx_names.py +++ b/pokemontools/sfx_names.py @@ -153,8 +153,8 @@ sfx_names = [ 'GetEggFromDaycareMan', 'GetEggFromDaycareLady', 'MoveDeleted', - '2NdPlace', - '1StPlace', + '2ndPlace', + '1stPlace', 'ChooseACard', 'GetTm', 'GetBadge', @@ -204,7 +204,7 @@ sfx_names = [ 'IntroSuicune4', 'GameFreakPresents', 'Tingle', - 'UnknownCb', + 'UnknownCB', 'TwoPcBeeps', '4NoteDitty', 'Twinkle', |