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 {
ADSR_ATTACK ,
ADSR_DECAY ,
ADSR_DEFAULT ,
ADSR_RELEASE ,
ADSR_SUSTAIN ,
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 : = {
EventType . ADSR_ATTACK : [ OperandType . U8 ] ,
EventType . ADSR_DECAY : [ OperandType . U8 ] ,
EventType . ADSR_DEFAULT : [ ] ,
EventType . ADSR_RELEASE : [ OperandType . U8 ] ,
EventType . ADSR_SUSTAIN : [ OperandType . U8 ] ,
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 ] ,
EventType . TRANSPOSE_ABS : [ OperandType . U8 ] ,
EventType . TRANSPOSE_REL : [ OperandType . U8 ] ,
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
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
else :
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 ] ] )
2023-08-23 17:26:16 +09:30
# DEBUG: Report total note duration and repeat note duration
if events :
2024-07-10 00:35:29 +09:30
print_info ( ' %d events ' % len ( events ) )
2023-08-23 17:26:16 +09:30
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 ]
2024-07-10 00:35:29 +09:30
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 ] )
2023-08-23 17:26:16 +09:30
else :
2024-07-10 00:35:29 +09:30
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