mirror of
https://github.com/snesrev/zelda3.git
synced 2025-12-19 18:05:55 -05:00
471 lines
14 KiB
Python
471 lines
14 KiB
Python
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)
|
|
|