ref: 2c4de3dc497a89027aed78548436e64c3631499e
dir: /assets/extract_music.py/
import hashlib import array import heapq, sys import yaml import time import util def load_sound_bank(rom, ea, mem_in = None): memory = list(mem_in) if mem_in else [None]*65536 j =0 while True: numbytes = rom.get_word(ea) target = rom.get_word(ea+2) if numbytes==0: # print('Entry point = 0x%x' % target) return memory, target # print('# Copy %d bytes to 0x%x' % (numbytes, target)) ea += 4 for i in range(numbytes): memory[target+i] = rom.get_byte(ea) ea += 1 if (ea & 0xffff) < 0x8000: ea += 0x8000 j += 1 if j > 256: break def get_byte(ea): return memory[ea] def get_word(ea): return get_byte(ea) | get_byte(ea + 1) * 256 # lightworld # Copy 11694 bytes to 0xd000 # Copy 1672 bytes to 0x2b00 # indoor # Copy 11455 bytes to 0xd000 # Copy 1292 bytes to 0x2b00 def to_str(s): if isinstance(s, str): return s if isinstance(s, int): return str(s) return s.name class Song: name = 'Song' def __str__(self): s = '# Song index %d\n' % self.index s += '[Song_0x%x]\n' % (self.ea) s += "".join(x.name + '\n' for x in self.phrases) return s class SongList: name = 'SongList' def __str__(self): s = '[SongList_0x%x]\n' % (self.ea) s += "".join(('None' if x == None else x.name) + '\n' for x in self.songs) return s class Phrase: name = 'Phrase' def __str__(self): s = '[Phrase_0x%x]\n' % (self.ea) s += "".join(('None' if x == None else x.name) + '\n' for x in self.patterns) return s class Pattern: name = 'Pattern' def __str__(self): r = '[Pattern_0x%x]\n' % (self.ea) last_len = None for a in self.lines: s = '' if len(a) == 4: s += a[0] + " " + " ".join(map(to_str, a[1])) else: s += '%s' % (a[0]) if a[-2] != None: s += ' %2d' % a[-2] last_len = a[-2] else: s += ' --'# % last_len if a[-1] != None: s += ' %2x' % a[-1] else: s += ' --' r += s + '\n' return r class PhraseLoop: name = 'PhraseLoop' def __init__(self, loops, jmp): self.loops = loops self.jmp = jmp self.name = 'PhraseLoop %d %d' % (self.loops, self.jmp) def __str__(self): return self.name types_for_ea = {} pqueue_by_ea = [] def reset_queues(): global types_for_ea, pqueue_by_ea types_for_ea = {} pqueue_by_ea = [] def get_type_for_ea(ea, tp): if ea == 0: return None assert(ea >= 256), ea a = types_for_ea.get(ea) if a != None: assert type(a)==tp, (type(a), tp, '0x%x' % ea) return a a = tp() a.ea = ea a.name = '%s_0x%x' % (a.name, ea) types_for_ea[ea] = a if get_byte(ea) != None: heapq.heappush(pqueue_by_ea, (ea, a)) a.is_imported = False else: a.is_imported = True return a kEffectByteLength = [1, 1, 2, 3, 0, 1, 2, 1, 2, 1, 1, 3, 0, 1, 2, 3, 1, 3, 3, 0, 1, 3, 0, 3, 3, 3, 1] kEffectNames = ['Instrument', 'Pan', 'PanFade', 'Vibrato', 'VibratoOff', 'SongVolume', 'SongVolumeFade', 'Tempo', 'TempoFade', 'Transpose', 'ChannelTranpose', 'Tremolo', 'TremoloOff', 'Volume', 'VolumeFade', 'Call', 'VibratoFade', 'PitchEnvelopeTo', 'PitchEnvelopeFrom', 'PitchEnvelopeOff', 'FineTune', 'EchoEnable', 'EchoOff', 'EchoSetup', 'EchoVolumeFade', 'PitchSlide', 'PercussionDefine'] assert(len(kEffectNames) == 27) def note_to_str(note): kKeys = ['C-', 'C#', 'D-', 'D#', 'E-', 'F-', 'F#', 'G-', 'G#', 'A-', 'A#', 'B-'] if note >= 72: if note == 72: return '-+-' # don't write kof elif note == 73: return '---' # want kof else: assert 0 octave = note / 12 key = note % 12 return '%s%d' % (kKeys[key], octave + 1) def get_pattern(ea): if ea == 0: return None pattern = get_type_for_ea(ea, Pattern) return pattern def get_song(ea, index): song = get_type_for_ea(ea, Song) if song: song.index = index return song def get_phrase(ea): phrase = get_type_for_ea(ea, Phrase) return phrase def decode_pattern(pattern, next_ea): ea = pattern.ea pattern.lines = [] start_ea = ea while True: # print('0x%x 0x%x' % (ea, start_ea)) # assert ea != 0x28f0 if ea != start_ea and ea == next_ea: pattern.lines.append(('Fallthrough', (), None, None)) return note_length, volstuff = None, None cmd = get_byte(ea); ea += 1 if cmd == 0: break if not (cmd & 0x80): note_length = cmd cmd = get_byte(ea); ea += 1 if not (cmd & 0x80): volstuff = cmd cmd = get_byte(ea); ea += 1 if cmd == 0xef: addr = get_word(ea) loops = get_byte(ea + 2) ea += 3 pattern.lines.append((kEffectNames[cmd-0xe0], (get_pattern(addr), loops), note_length, volstuff)) elif cmd >= 0xe0: assert note_length == None and volstuff == None, (note_length, volstuff) x = kEffectByteLength[cmd - 0xe0] args = [get_byte(ea+i) for i in range(x)] ea += x pattern.lines.append((kEffectNames[cmd-0xe0], args, note_length, volstuff)) else: assert(cmd & 0x80) pattern.lines.append((note_to_str(cmd & 0x7f), note_length, volstuff)) return pattern def decode_phrase(phrase): phrase.patterns = [get_pattern(get_word(phrase.ea + i * 2)) for i in range(8)] def decode_song(song): ea = song.ea song.phrases = [] ea_org = ea eas_in_phrase = [] while True: eas_in_phrase.append(ea) phrase = get_word(ea) if phrase == 0: break if phrase < 0x100: assert phrase != 0x80 and phrase != 0x81 tgt = get_word(ea + 2) assert tgt in eas_in_phrase song.phrases.append(PhraseLoop(phrase, (tgt - ea) // 2)) ea += 4 else: song.phrases.append(get_phrase(phrase)) ea += 2 return song def decode_any(what, next_ea): if isinstance(what, Song): decode_song(what) elif isinstance(what, SongList): pass # no need elif isinstance(what, Phrase): decode_phrase(what) elif isinstance(what, Pattern): decode_pattern(what, next_ea) else: assert 0 def get_song_list(ea, num): song_list = get_type_for_ea(ea, SongList) song_list.songs = [get_song(get_word(ea + i * 2), i) for i in range(num)] def load_song(ROM, song): global memory, SONGS_IN_BANK reset_queues() if song == 'intro': memory, entry_point = load_sound_bank(ROM, 0x998000) # intro SONGS_IN_BANK = (get_word(0xd000) - 0xd000) // 2 elif song == 'lightworld': memory, entry_point = load_sound_bank(ROM, 0x9a9ef5) # lw SONGS_IN_BANK = (get_word(0xd000) - 0xd000) // 2 elif song == 'indoor': memory, entry_point = load_sound_bank(ROM, 0x9b8000) # indoor SONGS_IN_BANK = (0xd046 - 0xd000) // 2 elif song == 'ending': memory, entry_point = load_sound_bank(ROM, 0x9ad380) # ending SONGS_IN_BANK = (0xd046 - 0xd000) // 2 def print_song(song, f): get_song_list(0xd000, SONGS_IN_BANK) if song in ('intro', 'lightworld'): get_phrase(0xD878) get_phrase(0xD8A8) get_phrase(0xD8B8) get_phrase(0xDf11) get_phrase(0xe37c) if song == 'indoor': get_phrase(0xDc5e) get_phrase(0xDc6e) get_pattern(0xe905) get_phrase(0xe94a) if song == 'ending': get_phrase(0x2a10) while len(pqueue_by_ea): _, item = heapq.heappop(pqueue_by_ea) decode_any(item, pqueue_by_ea[0][0] if len(pqueue_by_ea) else None) for a, b in sorted(types_for_ea.items()): if not b.is_imported: print(b, file = f) def dump_brr_audio(): def decode_brr(snd): start, loop_start = get_word(0x3c00 + snd * 4), get_word(0x3c00 + snd * 4 + 2) r = util.decode_brr(lambda x: get_byte(start+x)) return r, [get_byte(start+x) for x in range(len(r)//16 * 9)], get_byte(start)&0x2 != 0 for audio_idx in range(25): sound_data, brr_data, brr_repeat = decode_brr(audio_idx) open('sound/sound%d.pcm.brr' % audio_idx, 'wb').write(bytes(brr_data)) open('sound/sound%d.pcm' % audio_idx, 'wb').write(sound_data) def dump_music_info(): music_info = {} kDupSamples = {10 : 9, 20 : 19} music_info['samples'] = [] for audio_idx in range(25): start, rep = get_word(0x3c00 + audio_idx * 4), get_word(0x3c00 + audio_idx * 4 + 2) sample_info = { 'file' : 'sound/sound%d.pcm' % kDupSamples.get(audio_idx, audio_idx) } if get_byte(start) & 2: sample_info['repeat'] = (rep - start) // 9 * 16 music_info['samples'].append(sample_info) def add_sustain_decay_etc(ea, info): adsr1, adsr2, gain = get_byte(ea), get_byte(ea + 1), get_byte(ea + 2) info['decay'] = (adsr1 >> 4) & 7 info['attack'] = adsr1 & 0xf info['sustain_level'] = adsr2 >> 5 info['sustain_rate'] = adsr2 & 0x1f info['vxgain'] = gain music_info['instruments'] = [] for i in range(25): ea = 0x3d00 + i * 6 adsr1, adsr2 = get_byte(ea + 1), get_byte(ea + 2) info = { 'sample' : get_byte(ea), } add_sustain_decay_etc(ea + 1, info) info['pitch_base'] = get_byte(ea + 4) << 8 | get_byte(ea + 5) music_info['instruments'].append(info) music_info['note_gate_off'] = [get_byte(i) for i in range(0x3D96, 0x3D96 + 8)] music_info['note_volume'] = [get_byte(i) for i in range(0x3D9E, 0x3D9E + 16)] music_info['sfx_instruments'] = [] for i in range(25): ea = 0x3e00 + i * 9 info = { 'voll' : get_byte(ea), 'volr' : get_byte(ea + 1), 'pitch' : get_word(ea + 2), 'sample' : get_byte(ea + 4) } add_sustain_decay_etc(ea + 5, info) info['pitch_base'] = get_byte(ea + 8) music_info['sfx_instruments'].append(info) s = yaml.dump(music_info, default_flow_style=None, sort_keys=False) open('music_info.yaml', 'w').write(s) def decode_sfx(ea, next_addr): r = [] while True: if ea == next_addr: r.append(('Fallthrough', )) return r b = get_byte(ea); ea += 1 if b == 0: return r note_length = None volume_left, volume_right = None, None if not (b & 0x80): note_length = b b = get_byte(ea); ea += 1 if not (b & 0x80): volume_left, volume_right = b, None b = get_byte(ea); ea += 1 if not b & 0x80: volume_right = b b = get_byte(ea); ea += 1 if b == 0xe0: assert note_length == None and volume_left == None and volume_right == None, ea b = get_byte(ea); ea += 1 r.append(('SetInstrument %d' % b, )) elif b == 0xf9: #assert note_length == None and volume_left == None and volume_right == None, ea b = get_byte(ea); ea += 1 b0, b1, b2 = get_byte(ea), get_byte(ea+1), get_byte(ea+2); ea += 3 r.append(('PitchSlide %d %d %d' % (b0, b1, b2), note_to_str(b & 0x7f), note_length, volume_left, volume_right)) elif b == 0xf1: #assert note_length == None and volume_left == None and volume_right == None, ea b0, b1, b2 = get_byte(ea), get_byte(ea+1), get_byte(ea+2); ea += 3 r.append(('PitchSlide %d %d %d' % (b0, b1, b2), None, note_length, volume_left, volume_right)) elif b == 0xff: assert note_length == None and volume_left == None and volume_right == None, ea r.append(('Restart',)) return r else: r.append((None, note_to_str(b & 0x7f), note_length, volume_left, volume_right)) def print_all_sfx(f): items = set() def add_sfx_top(base, num, name): print('[%s_0x%x]' % (name, base), file = f) next_ea = base + num * 2 echo_ea = next_ea + num for i in range(num): r = [] ea = get_word(base + i * 2) if ea == 0: t = 'None' else: items.add(ea) t = 'Sfx_0x%x' % ea if name == 'SfxPort1': print('%s,%d' % (t, get_byte(next_ea + i)), file = f) else: print('%s,%d,%d' % (t, get_byte(next_ea + i), get_byte(echo_ea + i)), file = f) print(file = f) add_sfx_top(0x17c0, 32, 'SfxPort1') add_sfx_top(0x1820, 63, 'SfxPort2') add_sfx_top(0x191c, 63, 'SfxPort3') items.add(0x1a5b) items.add(0x1d1c) items.add(0x1ee2) items.add(0x1f13) items.add(0x1f1c) items.add(0x252d) items.add(0x2533) items.add(0x26a2) items.add(0x277e) items.add(0x279d) items.add(0x27c9) items.add(0x27f6) items.add(0x2807) items.add(0x2818) items.add(0x2829) items.add(0x2831) items.add(0x284a) items = sorted(list(items)) for i in range(len(items)): print('[Sfx_0x%x]' % items[i], file = f) next_addr = items[i + 1] if i + 1 < len(items) else 0 rs = decode_sfx(items[i], next_addr) for r in rs: if len(r) == 5: aa = '. ' if r[1] == None else r[1] bb = '--' if r[2] == None else '%2d' % r[2] cc = '---' if r[3] == None else '%3d' % r[3] dd = '---' if r[4] == None else '%3d' % r[4] r0 = '' if r[0] == None else ' ' + r[0] print('%s %s %s %s%s' % (aa, bb, cc, dd, r0), file = f) else: print(r[0], file = f) print(file = f) def extract_sound_data(rom): for song in ['intro', 'indoor', 'ending']: load_song(rom, song) open('sound/%s.spc' % song, 'wb').write(bytes((0 if a == None else a) for a in memory)) print_song(song, open('sound_%s.txt' % song, 'w')) if song == 'intro': dump_brr_audio() dump_music_info() print_all_sfx(open('sfx.txt', 'w')) if __name__ == "__main__": if len(sys.argv) < 3: print('extract_music.py [rom-filename] [intro|lightworld|indoor|ending]') sys.exit(0) ROM = util.LoadedRom(None if sys.argv[1] == '' else sys.argv[1]) song = sys.argv[2] load_song(ROM, song) print_song(song, sys.stdout)