350 lines
13 KiB
GDScript
350 lines
13 KiB
GDScript
const MAX_TRACKS := 8
|
|
const MAX_LOOP_DEPTH := 8 # Apparently 4, but eh whatever
|
|
const NOTE_IS_TIE := -1
|
|
const NOTE_IS_REST := -2
|
|
|
|
enum EventType {
|
|
ADSR_ATTACK_RATE,
|
|
ADSR_DECAY_RATE,
|
|
ADSR_DEFAULT,
|
|
ADSR_SUSTAIN_RATE,
|
|
ADSR_SUSTAIN_LEVEL,
|
|
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_RATE: [OperandType.U8], # Attack Rate, & 0b00001111
|
|
EventType.ADSR_DECAY_RATE: [OperandType.U8], # Decay Rate, & 0b00000111
|
|
EventType.ADSR_DEFAULT: [],
|
|
EventType.ADSR_SUSTAIN_RATE: [OperandType.U8], # & 0b00001111
|
|
EventType.ADSR_SUSTAIN_LEVEL: [OperandType.U8], # & 0b00000111
|
|
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.S8],
|
|
EventType.TRANSPOSE_REL: [OperandType.S8],
|
|
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
|
|
|
|
# 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)
|
|
|
|
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
|
|
var NOTE_TIE: int
|
|
var NOTE_REST: int
|
|
|
|
const LOGGING_LEVEL_INFO: bool = false
|
|
func print_info(s: String) -> void:
|
|
if LOGGING_LEVEL_INFO:
|
|
print(s)
|
|
|
|
# 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
|
|
if note >= 12: # 12 and 13 are rests and ties
|
|
if note == self.NOTE_REST:
|
|
note = NOTE_IS_REST
|
|
else:
|
|
note = NOTE_IS_TIE
|
|
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 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()
|
|
|
|
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
|
|
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])
|
|
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
|
|
print_info('ended with infinite loop')
|
|
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]:
|
|
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]])
|
|
loop_counts[loop_level] = on_loop
|
|
else:
|
|
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])
|
|
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:
|
|
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])
|
|
events.append([EventType.GOTO, rom_address_to_event_index[target_pos]])
|
|
break
|
|
else: # The GOTO address isn't in our index yet, proceed to it
|
|
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:
|
|
print_info('track %s ended on loop_level %d (0x%06X)' % [bgm_id, loop_level, loop_positions[loop_level]])
|
|
# # 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])
|
|
return events
|
|
|
|
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
|