ref: 9aa38e0a65d48bd8265e497a18b971f3047f0e2b
dir: /tables/compile_music.py/
import array
import sys
import yaml
import re
import util
class Song:
name = 'Song'
def __str__(self):
s = '[Song_0x%x idx=%d]\n' % (self.ea, self.index)
s += "".join(x.name + '\n' for x in self.phrases)
return s
class Phrase:
name = 'Phrase'
def __str__(self):
s = '[%s]' % (self.name)
return s
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
class Pattern:
name = 'Pattern'
class SfxPattern:
name = 'SfxPattern'
class SongList:
name = 'SongList'
class SfxList:
name = 'SfxList'
types_for_name = {}
def get_type_for_name(nam, tp, is_create):
if nam == 'None':
assert not is_create
return None
a = types_for_name.get(nam)
if a != None:
if is_create:
if a.defined:
raise Exception('%s already defined' % nam)
a.defined = True
assert type(a) == tp, (nam, type(a), tp)
return a
a = tp()
a.name = nam
a.defined = is_create
types_for_name[nam] = a
a.ea = None
if '_0x' in nam:
a.ea = int(nam[nam.index('_0x') + 3:], 16)
return a
def process_song(caption, args, lines):
song = get_type_for_name(caption, Song, True)
song.phrases = []
for line in lines:
line_cmd, *line_args = line.split(' ')
if line_cmd == 'PhraseLoop':
song.phrases.append(PhraseLoop(int(line_args[0]), int(line_args[1])))
else:
song.phrases.append(get_type_for_name(line_cmd, Phrase, False))
return song
def process_song_list(caption, args, lines):
song_list = get_type_for_name(caption, SongList, True)
song_list.songs = [get_type_for_name(line, Song, False) for line in lines]
return song_list
def process_phrase(caption, args, lines):
phrase = get_type_for_name(caption, Phrase, True)
assert len(lines)==8
phrase.patterns = [get_type_for_name(lines[i], Pattern, False) for i in range(8)]
return phrase
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']
kEffectNamesDict = {a:i for i, a in enumerate(kEffectNames)}
kKeys = ['C-', 'C#', 'D-', 'D#', 'E-', 'F-', 'F#', 'G-', 'G#', 'A-', 'A#', 'B-']
kKeysDict = {a+str(j+1):i+j*12 for i, a in enumerate(kKeys) for j in range(6)}
kKeysDict['-+-'] = 72
kKeysDict['---'] = 73
def process_pattern(caption, args, lines):
pattern = get_type_for_name(caption, Pattern, True)
pattern.lines = []
pattern.fallthrough = False
for line in lines:
assert not pattern.fallthrough
line_cmd, *line_args = line.split()
if line_cmd == 'Call':
assert len(line_args) == 2
pattern.lines.append(('Call', (get_type_for_name(line_args[0], Pattern, False),int(line_args[1]) ), None, None))
elif line_cmd == 'Fallthrough':
pattern.fallthrough = True
elif line_cmd in kEffectNamesDict:
line_args = [int(a) for a in line_args]
pattern.lines.append((line_cmd, line_args, None, None))
elif line_cmd in kKeysDict:
assert len(line_args) == 2, line_args
note_length = None if line_args[0] == '--' else int(line_args[0])
volstuff = None if line_args[1] == '--' else int(line_args[1], 16)
pattern.lines.append((line_cmd, line_args, note_length, volstuff))
else:
assert 0, repr(line_cmd)
return pattern
def process_sfx_pattern(caption, args, lines):
pattern = get_type_for_name(caption, SfxPattern, True)
pattern.lines = lines
return pattern
def write_sfx_pattern(serializer, o):
# print('# Creating %s (%x)' % ( o.name, serializer.addr))
for i,line in enumerate(o.lines):
# print(line)
line_cmd, *line_args = re.split(r' +', line)
if line_cmd == 'SetInstrument':
assert len(line_args) == 1
serializer.write([0xe0, int(line_args[0])])
elif line_cmd == 'Restart':
serializer.write([0xff])
assert i == len(o.lines) - 1
return
elif line_cmd == 'Fallthrough':
assert i == len(o.lines) - 1
return
elif line_cmd in kKeysDict or line_cmd == '.':
if line_args[0] != '--':
serializer.write((int(line_args[0]),))
if line_args[1] != '---':
serializer.write((int(line_args[1]),))
if line_args[2] != '---':
serializer.write((int(line_args[2]),))
if len(line_args) >= 4:
assert line_args[3] == 'PitchSlide'
if line_cmd == '.':
serializer.write([0xf1, int(line_args[4]), int(line_args[5]), int(line_args[6])])
else:
serializer.write([0xf9, kKeysDict[line_cmd] | 0x80, int(line_args[4]), int(line_args[5]), int(line_args[6])])
else:
serializer.write([kKeysDict[line_cmd] | 0x80])
else:
assert 0, repr(line_cmd)
serializer.write([0])
def process_sfx_list(caption, args, lines):
sfx_list = get_type_for_name(caption, SfxList, True)
sfx_list.patterns = []
sfx_list.next = []
sfx_list.echo = []
for line in lines:
r = line.split(',')
sfx_list.patterns.append(get_type_for_name(r[0], SfxPattern, False))
sfx_list.next.append(int(r[1]))
if len(r) >= 3:
sfx_list.echo.append(int(r[2]))
assert len(sfx_list.echo) in (0, len(lines))
return sfx_list
def write_sfx_list(serializer, o):
for line in o.patterns:
serializer.write_reloc_entry(line)
for next in o.next:
serializer.write([next])
for i in o.echo:
serializer.write([i])
kGapStartAddrs = (0x2b00, 0x2880, 0xd000)
class Serializer:
def __init__(self):
self.memory = [None] * 65536
self.relocs = []
self.addr = None
def write(self, data):
for d in data:
assert self.memory[self.addr] == None, '0x%x' % self.addr
self.memory[self.addr] = d
self.addr += 1
def write_at(self, a, data):
for d in data:
self.memory[a] = d
a += 1
def write_word(self, a, v):
self.memory[a] = v & 0xff
self.memory[a + 1] = v >> 8 & 0xff
def write_reloc_entry(self, r):
self.write([0, 0])
if r:
self.relocs.append((self.addr - 2, r))
def write_song(self, song):
for phrase in song.phrases:
if isinstance(phrase, PhraseLoop):
i = self.addr + phrase.jmp * 2
self.write([phrase.loops, 0])
self.write([i & 0xff, i >> 8])
else:
self.write_reloc_entry(phrase)
self.write([0, 0])
def write_phrase(self, phrase):
for i in range(8):
self.write_reloc_entry(phrase.patterns[i])
def write_song_list(self, song_list):
for song in song_list.songs:
self.write_reloc_entry(song)
def write_pattern(self, patt):
for cmd, args, note_length, volstuff in patt.lines:
#print(cmd, args, note_length, volstuff)
if note_length != None: self.write((note_length, ))
if volstuff != None: self.write((volstuff, ))
if cmd in kKeysDict:
self.write((0x80 | kKeysDict[cmd], ))
elif cmd == 'Call':
self.write([0xef, 0, 0, int(args[1])])
self.relocs.append((self.addr - 3, args[0]))
elif cmd in kEffectNamesDict:
i = kEffectNamesDict[cmd]
assert len(args) == kEffectByteLength[i]
self.write([0xe0 + i])
self.write(args)
if not patt.fallthrough:
self.write((0, ))
def write_obj(self, what):
# print(what.name, self.addr, what.ea)
# print('Writing %s at 0x%x. Curr pos 0x%x' % (what.name, what.ea, 0 if self.addr == None else self.addr))
if what.ea != None:
if self.addr == None or what.ea in kGapStartAddrs:# or True:#what.ea >= self.addr:
self.addr = what.ea
elif what.ea != self.addr:
print('%s: 0x%x != 0x%x' % (what.name, what.ea, self.addr))
assert(0)
what.write_addr = self.addr
assert self.addr != None
if isinstance(what, Phrase):
self.write_phrase(what)
elif isinstance(what, Pattern):
self.write_pattern(what)
elif isinstance(what, Song):
self.write_song(what)
elif isinstance(what, SongList):
self.write_song_list(what)
elif isinstance(what, SfxPattern):
write_sfx_pattern(self, what)
elif isinstance(what, SfxList):
write_sfx_list(self, what)
else:
print(type(what))
assert(0)
def write_zeros(self, a, b):
while a < b:
self.memory[a] = 0
a += 1
def process_relocs(self):
for p, r in self.relocs:
self.memory[p + 0] = r.write_addr & 0xff
self.memory[p + 1] = r.write_addr >> 8
def process_file(file):
sorted_ents = []
def add_collect(heading, collect):
caption, *args = heading.strip('[]').split(' ', 1)
if caption.startswith('Song_'):
r = process_song(caption, args, collect)
elif caption.startswith('Phrase_'):
r = process_phrase(caption, args, collect)
elif caption.startswith('Pattern_'):
r = process_pattern(caption, args, collect)
elif caption.startswith('SongList_'):
r = process_song_list(caption, args, collect)
elif caption.startswith('Sfx_'):
r = process_sfx_pattern(caption, args, collect)
elif caption.startswith('SfxPort'):
r = process_sfx_list(caption, args, collect)
else:
assert 0
sorted_ents.append(r)
collect = None
heading = None
for line in file:
line = line.strip()
if line == '' or line.startswith('#'): continue
if line.startswith('['):
if heading != None:
add_collect(heading, collect)
heading = line
collect = []
#
#print(caption, args)
else:
collect.append(line.strip('\n'))
if heading != None:
add_collect(heading, collect)
return sorted_ents
def serialize_song(serializer, song, sorted_ents):
if song == 'intro':
# serializer.write_zeros(0x2a8b, 0x2b00)
# serializer.write_zeros(0x3188, 0x4000)
# serializer.write_zeros(0xbaa0, 0xd000)
# serializer.write_zeros(0xfdae, 0x10000)
# serializer.write_zeros(0x2850, 0x2880)
serializer.addr = 0x4000
sample_to_addr = {}
music_info = yaml.safe_load(open('music_info.yaml', 'r'))
for i, info in enumerate(music_info['samples']):
if info['file'] not in sample_to_addr:
sample_to_addr[info['file']] = serializer.addr
serializer.write(open('%s.brr' % info['file'], 'rb').read())
addr = sample_to_addr[info['file']]
serializer.write_word(0x3c00 + i * 4, addr)
serializer.write_word(0x3c00 + i * 4 + 2, addr + info['repeat'] // 16 * 9 if 'repeat' in info else serializer.addr)
for i in range(6):
serializer.write_word(0x3c64 + i * 2, 0xffff)
for i, info in enumerate(music_info['instruments']):
ea = 0x3d00 + i * 6
serializer.memory[ea + 0] = info['sample']
serializer.memory[ea + 1] = 0x80 | info['decay'] << 4 | info['attack']
serializer.memory[ea + 2] = info['sustain_level'] << 5 | info['sustain_rate']
serializer.memory[ea + 3] = info['vxgain']
serializer.memory[ea + 4] = info['pitch_base'] >> 8
serializer.memory[ea + 5] = info['pitch_base'] & 0xff
serializer.write_at(0x3D96, music_info['note_gate_off'])
serializer.write_at(0x3D9e, music_info['note_volume'])
for i, info in enumerate(music_info['sfx_instruments']):
ea = 0x3e00 + i * 9
serializer.memory[ea + 0] = info['voll']
serializer.memory[ea + 1] = info['volr']
serializer.write_word(ea + 2, info['pitch'])
serializer.memory[ea + 4] = info['sample']
serializer.memory[ea + 5] = 0x80 | info['decay'] << 4 | info['attack']
serializer.memory[ea + 6] = info['sustain_level'] << 5 | info['sustain_rate']
serializer.memory[ea + 7] = info['vxgain']
serializer.memory[ea + 8] = info['pitch_base']
serializer.addr = None
for e in sorted_ents:
serializer.write_obj(e)
if song == 'indoor':
t = types_for_name['Song_0x2880']
t.defined = True
t.write_addr = 0x2880
for e in types_for_name.values():
if not e.defined:
raise Exception('Symbol %s not defined' % e.name)
serializer.process_relocs()
def compare_with_orig(serializer, song):
ranges=[]
ok = True
spc = open('sound/%s.spc' % song, 'rb').read()
for i in range(65536):
if serializer.memory[i] != None:
if serializer.memory[i]!= spc[i]:
print('0x%x: 0x%x != 0x%x' % (i, serializer.memory[i], spc[i]))
ok = False
else:
# serializer.memory[i] = spc[i]
if len(ranges) and ranges[-1][1] == i:
ranges[-1][1] = i + 1
else:
ranges.append([i, i + 1])
if __name__ == "__main__":
for a, b in ranges:
print('// undefined %x-%x' % (a, b))
return ok
def produce_loadable_seq(serializer):
r = []
# count non zeros
start, i = 0, 0
while start < 0x10000:
while i < 0x10000 and serializer.memory[i] != None:
i += 1
j = i
while i < 0x10000 and serializer.memory[i] == None:
i += 1
if j == start:
start = i
continue
r.extend([(j - start) & 0xff, (j - start) >> 8, start & 0xff, start >> 8])
r.extend(serializer.memory[start:j])
# print('copy 0x%x - 0x%x (%d)' % (start, j, j - start))
start = i
r.extend([0, 0])
return r
def print_song(song):
global types_for_name
types_for_name = {}
serializer = Serializer()
sorted_ents = process_file(open('sound_%s.txt' % song, 'r'))
if song == 'intro':
sorted_ents.extend(process_file(open('sfx.txt' , 'r')))
serialize_song(serializer, song, sorted(sorted_ents, key = lambda x: x.ea))
r = produce_loadable_seq(serializer)
result = 'kSoundBank_%s' % song, r
if not compare_with_orig(serializer, song):
raise Exception('compare failed')
return result
if __name__ == "__main__":
if len(sys.argv) == 1:
for song in ['intro', 'indoor', 'ending']:
print_song(song, sys.stdout)
else:
print_song(sys.argv[1], sys.stdout)