Sequencer disassembler/decoder improvements

This commit is contained in:
Luke Hubmayer-Werner 2018-04-18 19:22:20 +09:30
parent 11380efc1e
commit 06e914d30c
2 changed files with 74 additions and 34 deletions

View File

@ -15,7 +15,7 @@
along with ff5reader. If not, see <http://www.gnu.org/licenses/>. along with ff5reader. If not, see <http://www.gnu.org/licenses/>.
''' '''
HEX_PREFIX = '#' # '$' or '0x' are also nice HEX_PREFIX = '#' # '#' '$' or '0x' are also nice
def divceil(numerator, denominator): def divceil(numerator, denominator):
''' '''

View File

@ -24,7 +24,7 @@ Trailing 8 bytes are 16 4bit nibbles that make up the compressed samples.
''' '''
import sys import sys
from midiutil import MIDIFile from midiutil import MIDIFile
from includes.helpers import indirect from includes.helpers import indirect, hex
def generate_pointer_set(data): def generate_pointer_set(data):
''' '''
@ -80,21 +80,8 @@ def main(filename):
print('0x{:04x}: {:4d}'.format(k, brr_dict[k])) print('0x{:04x}: {:4d}'.format(k, brr_dict[k]))
filename_jp = 'Final Fantasy V (Japan).sfc'
def get_song_data(rom, id):
lookup_offset = 0x043B97 + (id*3)
offset = indirect(rom, lookup_offset, 3)-0xC00000
bank = offset & 0xFF0000
size = indirect(rom, offset)
track_ptrs = [indirect(rom, offset+i)+bank for i in range(2, 22, 2)]
if track_ptrs[0] != track_ptrs[1]:
print('Master is not channel 1, interesting', track_ptrs)
tracks = [rom[i:j] for i, j in zip(track_ptrs[1:-1], track_ptrs[2:])]
#data = rom[offset+2:offset+2+size]
return tracks
class SPCParser: class SPCParser:
slide_resolution = 50
notes = [i for i in range(12)] notes = [i for i in range(12)]
durations = [4, 3, 2, 4/3, 1.5, 1, 2/3, 0.75, 0.5, 1/3, 0.25, 0.5/3, 0.125, 0.25/3, 0.0625] durations = [4, 3, 2, 4/3, 1.5, 1, 2/3, 0.75, 0.5, 1/3, 0.25, 0.5/3, 0.125, 0.25/3, 0.0625]
def __init__(self): def __init__(self):
@ -103,7 +90,7 @@ class SPCParser:
(2, self._slide_volume), # 0xD3 (2, self._slide_volume), # 0xD3
(1, self._set_pan), # 0xD4 (1, self._set_pan), # 0xD4
(2, self._slide_pan), # 0xD5 (2, self._slide_pan), # 0xD5
(1, 'SomeSlide'), # 0xD6 (2, 'SomeSlide'), # 0xD6
(3, 'Vibrato'), # 0xD7 (3, 'Vibrato'), # 0xD7
(0, 'VibratoOff'), # 0xD8 (0, 'VibratoOff'), # 0xD8
(3, 'Tremolo'), # 0xD9 (3, 'Tremolo'), # 0xD9
@ -138,15 +125,20 @@ class SPCParser:
(2, 'SlideEchoVel'), # 0xF6 (2, 'SlideEchoVel'), # 0xF6
(2, 'unk2'), # 0xF7 (2, 'unk2'), # 0xF7
(1, 'SetGlobalVel'), # 0xF8 (1, 'SetGlobalVel'), # 0xF8
(3, 'EndLoop2'), # 0xF9 (3, 'EndLoopJump'), # 0xF9
(0, 'unk'), # 0xFA (2, self._jump), # 0xFA
(0, 'unk'), # 0xFB (0, 'unk'), # 0xFB does not end track
(0, self._end_channel), # 0xFC (0, self._end_channel), # 0xFC
(0, self._end_channel), # 0xFD (0, self._end_channel), # 0xFD
(0, self._end_channel), # 0xFE (0, self._end_channel), # 0xFE
(0, self._end_channel), # 0xFF (0, self._end_channel), # 0xFF
] ]
def _add_slide(self, cc, value, time):
start_value = 0 # TODO: determine the value at this point!!!
t = 0
v = 0
# TODO linearly scale t from 0 to time, v from start_value to value over slide_resolution
self.m.addControllerEvent(self.track, 0, self.time + t, cc, start_value + v)
def _set_instrument(self, instrument): def _set_instrument(self, instrument):
self.m.addProgramChange(self.track, 0, self.time, instrument) self.m.addProgramChange(self.track, 0, self.time, instrument)
def _set_volume(self, volume): def _set_volume(self, volume):
@ -176,31 +168,60 @@ class SPCParser:
def _slide_tempo(self, time, tempo): def _slide_tempo(self, time, tempo):
# TODO slide # TODO slide
self.m.addTempo(self.track, self.time, tempo) self.m.addTempo(self.track, self.time, tempo)
def _start_loop(self, repeats): def _start_loop(self, repeats):
print('Starting loop', repeats) print('\tStarting loop level {} - repeat x{}'.format(len(self.loop_i), repeats))
self.loop_i = self.i self.loop_i.append(self.i)
self.repeats = repeats self.loop_repeats.append(repeats)
def _end_loop(self): def _end_loop(self):
print('Ending loop', self.repeats) print('\tEnding loop level {} - repeating x{}'.format(len(self.loop_i)-1, self.loop_repeats[-1]))
if self.repeats > 0: if self.loop_repeats[-1] > 0:
self.i = self.loop_i self.i = self.loop_i[-1]
self.repeats -= 1 self.loop_repeats[-1] -= 1
else:
self.loop_i.pop()
self.loop_repeats.pop()
def _jump(self, *address):
'''
In gameplay, this would loop infinitely.
Since we're making finite MIDI files, we'll just loop track 0 a number of times,
and loop the other tracks until they match the length of track 0.
'''
offset = (address[0] + 256*address[1]) - self.start_address
if self.track == 0:
self.full_repeats += 1
if self.full_repeats < self.max_full_repeats:
self.i = offset
else:
self._end_channel()
else:
if self.time < self.track_end[0]-4:
self.i = offset
else:
self._end_channel()
def parse(self, tracks): def parse(self, tracks):
print('Parsing') print('Parsing')
self.m = MIDIFile(len(tracks)) self.m = MIDIFile(len(tracks))
self.velocity = 100 self.velocity = 100
for track, t in enumerate(tracks): self.max_full_repeats = 2
print('Creating track', track) self.track_end = []
for track, (t, address) in enumerate(tracks):
print('\nCreating track', track, hex(address, 6))
self.track = track self.track = track
self.start_address = address & 0xFFFF
self.time = 0 self.time = 0
self.octave = 5 self.octave = 5
self.transpose = 0 self.transpose = 0
self.repeats = 0 self.loop_i = []
self.loop_i = 0 self.loop_repeats = []
self.full_repeats = 0
self.i = 0 self.i = 0
while self.i < len(t): while self.i < len(t):
add = 'Track {}: {}\t{:7.3f}\t'.format(self.track, hex(self.i, 4), self.time)
t1 = t[self.i] t1 = t[self.i]
self.i += 1 self.i += 1
if t1 < 0xD2: if t1 < 0xD2:
@ -210,16 +231,33 @@ class SPCParser:
note = self.notes[t2]+(12*self.octave)+self.transpose note = self.notes[t2]+(12*self.octave)+self.transpose
self.m.addNote(track, 0, note, self.time, duration, self.velocity) self.m.addNote(track, 0, note, self.time, duration, self.velocity)
self.time += duration self.time += duration
print(add, hex(t1), t2, duration)
else: else:
n, callback = self.control_codes[t1-0xD2] n, callback = self.control_codes[t1-0xD2]
args = [t[self.i+j] for j in range(n)] args = [t[self.i+j] for j in range(n)]
self.i += n self.i += n
if callable(callback): if callable(callback):
print(add, callback.__name__, [hex(i) for i in [t1]+args]) # Maybe .__doc__ would work better
callback(*args) callback(*args)
else: else:
print(callback, '0x{:02x}'.format(t1)) print(add, callback, [hex(i) for i in [t1]+args])
self.track_end.append(self.time)
return self.m return self.m
filename_jp = 'Final Fantasy V (Japan).sfc'
def get_song_data(rom, id):
lookup_offset = 0x043B97 + (id*3)
offset = indirect(rom, lookup_offset, 3)-0xC00000
bank = offset & 0xFF0000
size = indirect(rom, offset)
track_ptrs = [indirect(rom, offset+i)+bank for i in range(2, 22, 2)]
if track_ptrs[0] != track_ptrs[1]:
print('Master is not channel 1, interesting', track_ptrs)
tracks = [(rom[i:j], i) for i, j in zip(track_ptrs[1:-1], track_ptrs[2:])]
#data = rom[offset+2:offset+2+size]
return tracks
def make_midi_file(tracks, filename='test.mid'): def make_midi_file(tracks, filename='test.mid'):
m = SPCParser().parse(tracks) m = SPCParser().parse(tracks)
with open(filename, 'wb') as file: with open(filename, 'wb') as file:
@ -231,4 +269,6 @@ def read_rom(filename=filename_jp):
return f return f
if __name__ == '__main__': if __name__ == '__main__':
main(sys.argv[1]) #main(sys.argv[1])
tracks = get_song_data(read_rom(), int(sys.argv[1]))
make_midi_file(tracks)