diff options
-rw-r--r-- | pokemontools/audio.py | 278 |
1 files changed, 278 insertions, 0 deletions
diff --git a/pokemontools/audio.py b/pokemontools/audio.py new file mode 100644 index 0000000..92bd3e3 --- /dev/null +++ b/pokemontools/audio.py @@ -0,0 +1,278 @@ +# coding: utf-8 + +import os + +from math import ceil + +from gbz80disasm import get_global_address, get_local_address + +import crystal +from crystal import music_classes as sound_classes +from crystal import Command + +from crystal import load_rom +rom = load_rom() +rom = bytearray(rom) + +import config +conf = config.Config() + + +class NybbleParam: + size = 0.5 + byte_type = 'dn' + which = None + + def __init__(self, address, name): + if self.which == None: + self.which = {0.0: 'lo', 0.5: 'hi'}[address % 1] + self.address = int(address) + self.name = name + self.parse() + + def parse(self): + self.nybble = (rom[self.address] >> {'lo': 0, 'hi': 4}[self.which]) & 0xf + +class HiNybbleParam(NybbleParam): + which = 'hi' + def to_asm(self): + return '%d' % self.nybble + +class LoNybbleParam(NybbleParam): + which = 'lo' + def to_asm(self): + return '%d' % self.nybble + +class PitchParam(HiNybbleParam): + def to_asm(self): + if self.nybble == 0: + pitch = 'Rst' + else: + pitch = 'CDEFGAB'[(self.nybble - 1) / 2] + if not self.nybble & 1: + pitch += '#' + return pitch + + +class Note(Command): + macro_name = "note" + size = 0 + end = False + param_types = { + 0: {"name": "pitch", "class": PitchParam}, + 1: {"name": "duration", "class": LoNybbleParam}, + } + allowed_lengths = [2] + + def parse(self): + self.params = [] + byte = rom[self.address] + current_address = self.address + for (key, param_type) in self.param_types.items(): + name = param_type["name"] + class_ = param_type["class"] + + # by making an instance, obj.parse() is called + obj = class_(address=int(current_address), name=name) + self.params += [obj] + + current_address += obj.size + self.size += obj.size + + self.params = dict(enumerate(self.params)) + + # obj sizes were 0.5, but were working with ints + current_address = int(ceil(current_address)) + self.size = int(ceil(self.size)) + + self.last_address = current_address + return True + + + +class Channel: + """A sound channel data parser.""" + + def __init__(self, address, channel=1, base_label='Sound'): + self.start_address = address + self.address = address + self.channel = channel + self.base_label = base_label + self.output = [] + self.parse() + + def parse(self): + noise = False + done = False + while not done: + cmd = rom[self.address] + + class_ = self.get_sound_class(cmd)(address=self.address) + + # notetype loses the intensity param on channel 4 + if class_.macro_name == 'notetype': + if self.channel in [4, 8]: + class_.size -= 1 + class_.params = dict(class_.params.items()[:-1]) + + # togglenoise only has a param when toggled on + elif class_.macro_name in ['togglenoise', 'sfxtogglenoise']: + if noise: + class_.size -= 1 + class_.params = dict(class_.params.items()[:-1]) + noise = not noise + + asm = class_.to_asm() + + # label any jumps or calls + for key, param in class_.param_types.items(): + if param['class'] == crystal.PointerLabelParam: + label_address = class_.params[key].parsed_address + label = '%s_branch_%x' % ( + self.base_label, + label_address + ) + label_output = ( + label_address, + '%s: ; %x' % (label, label_address) + ) + if label_output not in self.output: + self.output += [label_output] + asm = asm.replace( + '$%x' % (get_local_address(label_address)), + label + ) + + self.output += [(self.address, '\t' + asm)] + self.address += class_.size + + done = class_.end + # infinite loops are enders + if class_.macro_name == 'loopchannel': + 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 in self.output): + if done: + self.output += [(self.address, '; %x' % 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!' + + def to_asm(self): + self.output = sorted( + self.output, + # comment then label then asm + key=lambda (x, y):(x, not y.startswith(';'), ':' not in y) + ) + text = '' + for i, (address, asm) in enumerate(self.output): + if ':' in asm: + # dont print labels for empty chunks + for (x, y) in self.output[i:]: + if ':' not in y: + text += '\n' + asm + '\n' + break + else: + text += asm + '\n' + text += '; %x' % (address + 1) + '\n' + return text + + def get_sound_class(self, i): + for class_ in sound_classes: + if class_.id == i: + return class_ + return Note + + +class Sound: + """Interprets a sound data header.""" + + def __init__(self, address, name=''): + self.start_address = address + self.bank = address / 0x4000 + self.address = address + + self.name = name + self.base_label = 'Sound_%x' % self.start_address + if self.name != '': + self.base_label = self.name + + self.output = [] + self.parse() + + def parse(self): + self.num_channels = (rom[self.address] >> 6) + 1 + self.channels = [] + for ch in xrange(self.num_channels): + current_channel = (rom[self.address] & 0xf) + 1 + self.address += 1 + 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) + self.channels += [(current_channel, channel)] + + def to_asm(self): + asms = {} + + text = '' + text += '%s: ; %x' % (self.base_label, self.start_address) + '\n' + for num, channel in self.channels: + text += '\tchannel %d, %s_Ch%d' % (num, self.base_label, num) + '\n' + text += '; %x' % self.address + '\n' + asms[self.start_address] = text + + text = '' + for ch, (num, channel) in enumerate(self.channels): + text += '%s_Ch%d: ; %x' % ( + self.base_label, + num, + channel.start_address + ) + '\n' + # stack labels at the same address + if ch < len(self.channels) - 1: + next_channel = self.channels[ch + 1][1] + if next_channel.start_address == channel.start_address: + continue + text += channel.to_asm() + asms[channel.start_address] = text + text = '' + + return '\n'.join(asm for address, asm in sorted(asms.items())) + + +def dump_sounds(origin, names, path, base_label='Sound_'): + """Dump sound data from a pointer table.""" + for i, name in enumerate(names): + addr = origin + i * 3 + bank, address = rom[addr], rom[addr+1] + rom[addr+2] * 0x100 + sound_at = get_global_address(address, bank) + + sound = Sound(sound_at, base_label + name) + output = sound.to_asm() + + filename = name.lower() + '.asm' + with open(os.path.join(path, filename), 'w') as out: + out.write(output) + +def dump_crystal_music(): + from song_names import song_names + dump_sounds(0xe906e, song_names, os.path.join(conf.path, 'audio', 'music'), 'Music_') + +def dump_crystal_sfx(): + from sfx_names import sfx_names + dump_sounds(0xe927c, sfx_names, os.path.join(conf.path, 'audio', 'sfx'), 'Sfx_') + + +if __name__ == '__main__': + dump_crystal_music() + dump_crystal_sfx() + |