2023-08-23 17:26:16 +09:30
const MAX_TRACKS : = 8
const MAX_LOOP_DEPTH : = 8 # Apparently 4, but eh whatever
2024-07-15 16:35:43 +09:30
const NOTE_IS_TIE : = - 1
const NOTE_IS_REST : = - 2
2023-08-23 17:26:16 +09:30
enum EventType {
2024-07-17 14:10:16 +09:30
ADSR_ATTACK_RATE ,
ADSR_DECAY_RATE ,
2023-08-23 17:26:16 +09:30
ADSR_DEFAULT ,
2024-07-17 14:10:16 +09:30
ADSR_SUSTAIN_RATE ,
ADSR_SUSTAIN_LEVEL ,
2023-08-23 17:26:16 +09:30
CPU_CONTROLLED_JUMP ,
ECHO_FEEDBACK_FIR ,
ECHO_OFF ,
ECHO_ON ,
ECHO_VOLUME_SLIDE ,
ECHO_VOLUME ,
END ,
GOTO ,
LOOP_BREAK ,
LOOP_END ,
LOOP_RESTART ,
LOOP_START ,
MASTER_VOLUME ,
NOISE_FREQ ,
NOISE_OFF ,
NOISE_ON ,
NONE ,
NOTE ,
OCTAVE_DOWN ,
OCTAVE_UP ,
OCTAVE ,
PAN_SLIDE ,
PAN_LFO_OFF ,
PAN_LFO_ON ,
PAN ,
PITCH_SLIDE ,
PITCHMOD_OFF ,
PITCHMOD_ON ,
PROGCHANGE ,
TEMPO_SLIDE ,
TEMPO ,
TRANSPOSE_ABS ,
TRANSPOSE_REL ,
TREMOLO_OFF ,
TREMOLO_ON ,
TUNING ,
VIBRATO_OFF ,
VIBRATO_ON ,
VOLUME_SLIDE ,
VOLUME ,
}
enum OperandType {
U8 ,
U16 ,
S8 ,
}
const OPERAND_MAP : = {
2024-07-17 14:10:16 +09:30
EventType . ADSR_ATTACK_RATE : [ OperandType . U8 ] , # Attack Rate, & 0b00001111
EventType . ADSR_DECAY_RATE : [ OperandType . U8 ] , # Decay Rate, & 0b00000111
2023-08-23 17:26:16 +09:30
EventType . ADSR_DEFAULT : [ ] ,
2024-07-17 14:10:16 +09:30
EventType . ADSR_SUSTAIN_RATE : [ OperandType . U8 ] , # & 0b00001111
EventType . ADSR_SUSTAIN_LEVEL : [ OperandType . U8 ] , # & 0b00000111
2023-08-23 17:26:16 +09:30
EventType . CPU_CONTROLLED_JUMP : [ OperandType . U16 ] ,
EventType . ECHO_FEEDBACK_FIR : [ OperandType . U8 , OperandType . U8 ] ,
EventType . ECHO_OFF : [ ] ,
EventType . ECHO_ON : [ ] ,
EventType . ECHO_VOLUME_SLIDE : [ OperandType . U8 , OperandType . U8 ] ,
EventType . ECHO_VOLUME : [ OperandType . U8 ] ,
EventType . END : [ ] ,
EventType . GOTO : [ OperandType . U16 ] ,
EventType . LOOP_BREAK : [ OperandType . U8 , OperandType . U16 ] ,
EventType . LOOP_END : [ ] ,
EventType . LOOP_START : [ OperandType . U8 ] ,
EventType . MASTER_VOLUME : [ OperandType . U8 ] ,
EventType . NOISE_FREQ : [ OperandType . U8 ] ,
EventType . NOISE_OFF : [ ] ,
EventType . NOISE_ON : [ ] ,
EventType . OCTAVE_DOWN : [ ] ,
EventType . OCTAVE_UP : [ ] ,
EventType . OCTAVE : [ OperandType . U8 ] ,
EventType . PAN_LFO_OFF : [ ] ,
EventType . PAN_LFO_ON : [ OperandType . U8 , OperandType . U8 ] ,
EventType . PAN_SLIDE : [ OperandType . U8 , OperandType . U8 ] ,
EventType . PAN : [ OperandType . U8 ] ,
EventType . PITCH_SLIDE : [ OperandType . U8 , OperandType . S8 ] ,
EventType . PITCHMOD_OFF : [ ] ,
EventType . PITCHMOD_ON : [ ] ,
EventType . PROGCHANGE : [ OperandType . U8 ] ,
EventType . TEMPO_SLIDE : [ OperandType . U8 , OperandType . U8 ] ,
EventType . TEMPO : [ OperandType . U8 ] ,
2024-07-17 14:10:16 +09:30
EventType . TRANSPOSE_ABS : [ OperandType . S8 ] ,
EventType . TRANSPOSE_REL : [ OperandType . S8 ] ,
2023-08-23 17:26:16 +09:30
EventType . TREMOLO_OFF : [ ] ,
EventType . TREMOLO_ON : [ OperandType . U8 , OperandType . U8 , OperandType . U8 ] ,
EventType . TUNING : [ OperandType . U8 ] ,
EventType . VIBRATO_OFF : [ ] ,
EventType . VIBRATO_ON : [ OperandType . U8 , OperandType . U8 , OperandType . U8 ] ,
EventType . VOLUME_SLIDE : [ OperandType . U8 , OperandType . U8 ] ,
EventType . VOLUME : [ OperandType . U8 ] ,
}
const PPQN : = 48 # Pulses Per Quarter Note. All the games this project cares about use 48 (allows fast triplets). Some other SNES games I don't care about use 12/24/32.
# const TIMER0_FREQUENCY := 36 # 0x24
# return 60000000.0 / (125 * PPQN * TIMER0_FREQUENCY)) * (tempo / 256.0);
static func tempo_to_bpm ( tempo_byte : int ) - > float :
if tempo_byte < 1 :
return 1.0
return ( tempo_byte / 255.0 ) * 60000000.0 / 216000.0 # VGMTrans uses /256.0 but I don't trust that
2024-07-10 22:13:58 +09:30
# bpm * ppqn = ppm (pulses per minute)
# ppm / 60 = pps (pulses per second)
# 1/pps = seconds per pulse
static func tempo_to_seconds_per_pulse ( tempo_byte : int ) - > float :
# 125 * TIMER0_FREQUENCY = 4500
return 4500.0 / ( 1000000.0 * tempo_byte / 255.0 )
2023-08-23 17:26:16 +09:30
static func get_int_array ( size : int ) - > PoolIntArray :
var array : = PoolIntArray ( )
array . resize ( size )
array . fill ( 0 )
return array
# In real OOP these would be static or static const, but GDScript doesn't allow that
# These need to be defined in subclasses for methods to work
var EVENT_MAP : Dictionary
var NOTE_DURATIONS : PoolByteArray # This might need to be made untyped if a future addition uses PoolIntArray instead
var REFERENCE_NOTE : int
2024-07-15 16:35:43 +09:30
var NOTE_TIE : int
var NOTE_REST : int
2023-08-23 17:26:16 +09:30
2024-07-10 00:35:29 +09:30
const LOGGING_LEVEL_INFO : bool = false
func print_info ( s : String ) - > void :
if LOGGING_LEVEL_INFO :
print ( s )
2023-08-23 17:26:16 +09:30
# These would be static methods if NOTE_DURATIONS and EVENT_MAP could be static
func translate_instruction ( buffer : StreamPeer ) - > Array :
var instruction : = buffer . get_u8 ( )
if instruction < 0xD2 :
var duration = self . NOTE_DURATIONS [ instruction % 15 ]
var note = instruction / 15
2024-07-15 16:35:43 +09:30
if note > = 12 : # 12 and 13 are rests and ties
if note == self . NOTE_REST :
note = NOTE_IS_REST
else :
note = NOTE_IS_TIE
2023-08-23 17:26:16 +09:30
return [ EventType . NOTE , note , duration ]
else :
# Control codes
var cc = self . EVENT_MAP [ instruction ]
var operand_list = OPERAND_MAP [ cc ]
var operands = [ cc ]
for o in operand_list :
match o :
OperandType . U8 :
operands . append ( buffer . get_u8 ( ) )
OperandType . U16 :
operands . append ( buffer . get_u16 ( ) )
OperandType . S8 :
operands . append ( buffer . get_8 ( ) )
return operands
2024-07-17 23:20:02 +09:30
func disassemble_raw ( buffer : StreamPeerBuffer , labels : Dictionary , address_start : = 0x3000 , address_end : = 0x4800 ) - > PoolStringArray :
var output : = PoolStringArray ( )
var address : = address_start
var address_offset : int = address_start - buffer . get_position ( )
var ins_labels : = EventType . keys ( )
while address < address_end :
if address in labels :
output . append ( labels [ address ] )
var instruction : = translate_instruction ( buffer )
var event_type = instruction [ 0 ]
match event_type :
EventType . NOTE :
var pulses = instruction [ 2 ]
match instruction [ 1 ] :
NOTE_IS_REST :
output . append ( ' $ %04x : NOTE_REST %d pulses ' % [ address , pulses ] )
NOTE_IS_TIE :
output . append ( ' $ %04x : NOTE_TIE %d pulses ' % [ address , pulses ] )
var pitch :
output . append ( ' $ %04x : NOTE %d %d pulses ' % [ address , pitch , pulses ] )
_ :
if len ( instruction ) > 1 :
output . append ( ' $ %04x : %s %s ' % [ address , ins_labels [ event_type ] , instruction . slice ( 1 , - 1 ) ] )
else :
output . append ( ' $ %04x : %s ' % [ address , ins_labels [ event_type ] ] )
address = address_offset + buffer . get_position ( )
return output
func disassemble_sfx ( buffer : StreamPeerBuffer ) :
var labels : = { }
buffer . seek ( 0x041F97 )
for sfx in 178 :
for channel in 2 :
var offset : int = buffer . get_u16 ( )
if offset > = 0x3000 :
if offset in labels :
labels [ offset ] = labels [ offset ] + ' , SFX_0x %02X : %d ' % [ sfx , channel ]
labels [ offset ] = ' SFX_0x %02X : %d ' % [ sfx , channel ]
buffer . seek ( 0x042397 )
var lines : = disassemble_raw ( buffer , labels )
var file : = File . new ( )
file . open ( ' output/sfx_bank_disassembly.txt ' , File . WRITE )
for line in lines :
file . store_line ( line )
file . close ( )
2023-08-23 17:26:16 +09:30
func unroll_track ( buffer : StreamPeerBuffer , bgm_start_pos : int , track_start_pos : int , bgm_end_pos : int , bgm_id = ' N/A ' ) - > Array :
# There are three ways a track's data can end:
# 1) Explicit End instruction
# 2) Instruction pointer out of bounds (one FFV track starts out of bounds to indicate an unused channel)
# 3) Infinite loop
# 3.1) unconditional GOTO to a previous address
# 3.2) LOOP_END with a prior [LOOP_START, 0]
# Detecting 1), 2) and 3.1) is trivial and stateless, but 3.2 requires loop tracking
# We may as well unroll the loops here and convert a final infinite loop to a GOTO
# While it is possible that GOTO may point forwards, we'll just treat it as an end for now
var start_bank : = bgm_start_pos & 0x3F0000
var events : = [ ]
var event_idx : = 0
var rom_address_to_event_index : = { }
var loop_level : int = - 1
var loop_positions : = get_int_array ( MAX_LOOP_DEPTH )
var loop_counts : = get_int_array ( MAX_LOOP_DEPTH )
var loop_counters : = get_int_array ( MAX_LOOP_DEPTH )
var loop_break_positions : = get_int_array ( MAX_LOOP_DEPTH )
var loop_break_targets : = get_int_array ( MAX_LOOP_DEPTH )
buffer . seek ( track_start_pos )
while true :
var pos = buffer . get_position ( )
if pos > = bgm_end_pos : # Outside of the chunk that would be copied to the SPC
break
rom_address_to_event_index [ pos ] = event_idx
var event = translate_instruction ( buffer )
match event [ 0 ] :
EventType . LOOP_START :
loop_level += 1
var count = event [ 1 ]
if count > 0 :
count += 1
loop_counts [ loop_level ] = count
loop_positions [ loop_level ] = pos
EventType . LOOP_END :
# Check if the loop contains a break
var loop_count : = loop_counts [ loop_level ]
if loop_break_positions [ loop_level ] > 0 :
# Copy the appropriate amount of loops, a partial loop up to the break, then seek to the break's target
var break_pos : = loop_break_positions [ loop_level ]
var jump_pos : = loop_break_targets [ loop_level ]
if jump_pos - pos != 1 : # This happens once on track 68:06 FFV SNES BGM
2024-07-10 00:35:29 +09:30
print_debug ( ' LOOP_END at 0x %06X would break instead from 0x %06X to 0x %06X ( %d past end) ' % [ pos , break_pos , jump_pos , jump_pos - pos ] )
2023-08-23 17:26:16 +09:30
buffer . seek ( jump_pos )
var loop_events : = events . slice ( rom_address_to_event_index [ loop_positions [ loop_level ] ] , - 1 )
var partial_loop_events : = events . slice ( rom_address_to_event_index [ loop_positions [ loop_level ] ] , rom_address_to_event_index [ break_pos ] - 1 )
for loop in loop_count - 2 : # The first loop is already present, and the last is partial
events . append_array ( loop_events )
events . append_array ( partial_loop_events )
event_idx += ( len ( loop_events ) * ( loop_count - 2 ) ) + len ( partial_loop_events )
# Clean up loop break vars
loop_break_positions [ loop_level ] = 0
loop_break_targets [ loop_level ] = 0
loop_level -= 1
elif loop_count > 0 :
# No break within the loop, just copy the loop contents the appropriate number of times and proceed
var loop_events : = events . slice ( rom_address_to_event_index [ loop_positions [ loop_level ] ] , - 1 )
for loop in loop_count - 1 : # The first loop is already present
events . append_array ( loop_events )
event_idx += len ( loop_events ) * ( loop_count - 1 )
# Clean up loop vars
loop_level -= 1
else :
# Infinite loop, convert it to a GOTO and return
2024-07-10 00:35:29 +09:30
print_info ( ' ended with infinite loop ' )
2023-08-23 17:26:16 +09:30
events . append ( [ EventType . GOTO , rom_address_to_event_index [ loop_break_targets [ loop_level ] ] ] )
loop_level -= 1
break
EventType . LOOP_BREAK :
# All FFV loop breaks are the same as the loop count
var on_loop : int = event [ 1 ]
# Just treat the rest as a GOTO
var target_pos : int = event [ 2 ] + start_bank
if target_pos < bgm_start_pos :
target_pos += 0x010000
loop_break_positions [ loop_level ] = pos
loop_break_targets [ loop_level ] = target_pos
if on_loop != loop_counts [ loop_level ] :
2024-07-10 00:35:29 +09:30
print_info ( ' LOOP_BREAK found in track %s at 0x %06X :0x %06X :0x %06X : 0x %06X to 0x %06X on loop %d of %d ' % [ bgm_id , bgm_start_pos , track_start_pos , bgm_end_pos , pos , target_pos , on_loop , loop_counts [ loop_level ] ] )
2023-08-23 17:26:16 +09:30
loop_counts [ loop_level ] = on_loop
else :
2024-07-10 00:35:29 +09:30
print_info ( ' LOOP_BREAK found in track %s at 0x %06X :0x %06X :0x %06X : 0x %06X to 0x %06X on last loop ' % [ bgm_id , bgm_start_pos , track_start_pos , bgm_end_pos , pos , target_pos ] )
2023-08-23 17:26:16 +09:30
if target_pos in rom_address_to_event_index :
print_debug ( ' LOOP_BREAK is a past address, this is either malformed or an infinite loop ' )
break
EventType . END :
break
EventType . GOTO :
# GOTO pointers reference the ROM position
# BGM 04 (Pirates Ahoy!) crosses a ROM bank so correct bank wrapping is required
var target_pos : int = event [ 1 ] + start_bank
if target_pos < bgm_start_pos :
target_pos += 0x010000
var event_num = rom_address_to_event_index . get ( target_pos , - 1 )
if event_num > = 0 :
2024-07-10 00:35:29 +09:30
print_info ( ' Infinite GOTO found in track %s at 0x %06X :0x %06X :0x %06X : 0x %06X to 0x %06X (event number %d ) ' % [ bgm_id , bgm_start_pos , track_start_pos , bgm_end_pos , pos , target_pos , event_num ] )
2023-08-23 17:26:16 +09:30
events . append ( [ EventType . GOTO , rom_address_to_event_index [ target_pos ] ] )
break
2024-07-19 21:44:38 +09:30
else : # The GOTO address isn't in our index yet, proceed to it
2023-08-23 17:26:16 +09:30
print_debug ( ' forward GOTO found in track %s at 0x %06X :0x %06X :0x %06X : 0x %06X to 0x %06X ' % [ bgm_id , bgm_start_pos , track_start_pos , bgm_end_pos , pos , target_pos ] )
buffer . seek ( target_pos )
_ :
events . append ( event )
event_idx += 1
# DEBUG: Report if track doesn't end all the contained loops
if loop_level > = 0 :
2024-07-10 00:35:29 +09:30
print_info ( ' track %s ended on loop_level %d (0x %06X ) ' % [ bgm_id , loop_level , loop_positions [ loop_level ] ] )
2024-07-19 21:44:38 +09:30
# # DEBUG: Report total note duration and repeat note duration
# if events:
# print_info('%d events' % len(events))
# var total_note_dur := 0
# var total_notes := 0
# var total_rests := 0
# for e in events:
# if e[0] == EventType.NOTE:
# total_note_dur += e[2]
# if e[1] >= 0:
# total_notes += 1
# else:
# total_rests += 1
# if events[-1][0] == EventType.GOTO:
# var repeat_note_dur := 0
# for e in events.slice(events[-1][1], -1):
# if e[0] == EventType.NOTE:
# repeat_note_dur += e[2]
# print_info('track %s has duration %d and repeat duration %d (intro %d) - %d notes %d rests' % [bgm_id, total_note_dur, repeat_note_dur, total_note_dur-repeat_note_dur, total_notes, total_rests])
# events[-1].append_array([total_note_dur-repeat_note_dur, total_note_dur])
# else:
# print_info('track %s has duration %d - %d notes %d rests' % [bgm_id, total_note_dur, total_notes, total_rests])
2023-08-23 17:26:16 +09:30
return events
2024-07-19 21:44:38 +09:30
func unroll_bgm ( buffer : StreamPeerBuffer , bgm_start_address : int , track_start_addresses : PoolIntArray , bgm_end_address : int , bgm_id = ' N/A ' ) - > Array :
var tracks : = [ ]
for track_start_address in track_start_addresses :
tracks . append ( self . unroll_track ( buffer , bgm_start_address , track_start_address , bgm_end_address , bgm_id ) )
return tracks