Basic music playback

This commit is contained in:
Luke Hubmayer-Werner 2023-08-23 17:26:16 +09:30
parent 0ef19b45a7
commit bd5598f98f
8 changed files with 670 additions and 15 deletions

View File

@ -34,6 +34,7 @@ StringLoader="*res://scripts/loaders/StringLoader.gd"
[debug] [debug]
settings/fps/force_fps=60
gdscript/warnings/unused_variable=false gdscript/warnings/unused_variable=false
gdscript/warnings/integer_division=false gdscript/warnings/integer_division=false
@ -42,6 +43,7 @@ gdscript/warnings/integer_division=false
window/size/width=384 window/size/width=384
window/size/height=240 window/size/height=240
window/dpi/allow_hidpi=true window/dpi/allow_hidpi=true
window/vsync/use_vsync=false
window/stretch/shrink=2.0 window/stretch/shrink=2.0
window/size/snap_to_integer=true window/size/snap_to_integer=true
@ -61,7 +63,6 @@ texture={
[rendering] [rendering]
quality/driver/driver_name="GLES2"
2d/snapping/use_gpu_pixel_snap=true 2d/snapping/use_gpu_pixel_snap=true
environment/default_clear_color=Color( 0, 0, 0, 1 ) environment/default_clear_color=Color( 0, 0, 0, 1 )
environment/default_environment="res://default_env.tres" environment/default_environment="res://default_env.tres"

235
scripts/MusicPlayer.gd Normal file
View File

@ -0,0 +1,235 @@
#warning-ignore-all:shadowed_variable
extends Node
const music := preload('res://scripts/loaders/snes/music_ff5.gd')
const EventType := music.EventType
var MUSIC := music.new()
var inst_map: Dictionary # int keys, AudioStreamSample values
var tracks: Array
var num_tracks: int
var players: Array
var tempo: float # Store as BPM
var seconds_per_pulse: float
var master_volume := 255
var channel_pointer := PoolIntArray()
var channel_instrument_idx := PoolByteArray()
var channel_next_pulse := PoolIntArray()
var channel_current_note := PoolByteArray()
var channel_velocity := PoolByteArray()
var channel_pan := PoolByteArray() # Reversed from MIDI
var channel_octave := PoolByteArray()
var channel_transpose := PoolByteArray()
var channel_fine_tuning := PoolRealArray()
var channel_adsr_attack := PoolByteArray()
var channel_adsr_decay := PoolByteArray()
var channel_adsr_sustain := PoolByteArray()
var channel_adsr_release := PoolByteArray()
var channel_noise_freq := PoolByteArray()
var channel_noise_on := PoolByteArray()
var channel_pan_lfo_rate := PoolByteArray()
var channel_pan_lfo_depth := PoolByteArray()
var channel_pan_lfo_on := PoolByteArray()
var channel_tremolo_delay := PoolByteArray()
var channel_tremolo_rate := PoolByteArray()
var channel_tremolo_depth := PoolByteArray()
var channel_tremolo_on := PoolByteArray()
var channel_vibrato_delay := PoolByteArray()
var channel_vibrato_rate := PoolByteArray()
var channel_vibrato_depth := PoolByteArray()
var channel_vibrato_on := PoolByteArray()
var channel_pitchmod_on := PoolByteArray()
var channel_echo_on := PoolByteArray()
var channel_echo_volume := PoolByteArray()
func set_tempo(tempo: float):
self.tempo = tempo
self.seconds_per_pulse = 60.0 / (tempo * music.PPQN)
func _init(tracks: Array, instrument_map: Dictionary):
self.tracks = tracks
self.num_tracks = len(self.tracks)
self.inst_map = instrument_map
self.players = []
self.set_tempo(120.0)
for i in num_tracks:
self.players.append(AudioStreamPlayer.new())
add_child(self.players[-1])
self.channel_pointer.append(0)
self.channel_instrument_idx.append(0)
self.channel_next_pulse.append(0)
self.channel_current_note.append(0)
self.channel_velocity.append(100)
self.channel_pan.append(0)
self.channel_octave.append(5)
self.channel_transpose.append(0)
self.channel_fine_tuning.append(1.0)
self.channel_adsr_attack.append(0)
self.channel_adsr_decay.append(0)
self.channel_adsr_sustain.append(0)
self.channel_adsr_release.append(0)
self.channel_noise_freq.append(0)
self.channel_noise_on.append(0)
self.channel_pan_lfo_rate.append(0)
self.channel_pan_lfo_depth.append(0)
self.channel_pan_lfo_on.append(0)
self.channel_tremolo_delay.append(0)
self.channel_tremolo_rate.append(0)
self.channel_tremolo_depth.append(0)
self.channel_tremolo_on.append(0)
self.channel_vibrato_delay.append(0)
self.channel_vibrato_rate.append(0)
self.channel_vibrato_depth.append(0)
self.channel_vibrato_on.append(0)
self.channel_pitchmod_on.append(0)
self.channel_echo_on.append(0)
self.channel_echo_volume.append(0)
func play_channel(channel: int, time_offset: float = 0.0) -> int:
# Executes the track events until it hits a note/rest, in which case it returns the pulse count to the next action, or the end of the events, in which case it returns -1
self.players[channel].stop()
var track: Array = self.tracks[channel]
var l := len(track)
var player: AudioStreamPlayer = self.players[channel]
while true:
var ptr: int = self.channel_pointer[channel]
if ptr >= l:
break
var event = track[ptr]
self.channel_pointer[channel] += 1
match event[0]: # Control codes
EventType.NOTE:
var note = event[1]
var duration = event[2]
if note >= 0: # Don't shift or play rests
note += (12 * self.channel_octave[channel]) + self.channel_transpose[channel]
player.pitch_scale = pow(2.0, (note - MUSIC.REFERENCE_NOTE)/12.0) #* self.channel_fine_tuning[channel]
player.volume_db = linear2db((self.channel_velocity[channel]/255.0) * (self.master_volume/255.0))
player.play(max(SoundLoader.PLAY_START - time_offset, 0.0))
self.channel_current_note[channel] = note
else:
self.channel_current_note[channel] = -1
# TODO: Confirm tempo scaling
return duration # Pulses to next instruction
EventType.VOLUME:
self.channel_velocity[channel] = event[1]
EventType.VOLUME_SLIDE: # TODO: implement slides
var slide_duration: int = event[1]
self.channel_velocity[channel] = event[2]
EventType.PAN: # TODO: implement slides
self.channel_pan[channel] = event[1]
EventType.PAN_SLIDE: # TODO: implement slides
var slide_duration: int = event[1]
self.channel_pan[channel] = event[2]
EventType.PITCH_SLIDE: # TODO: implement slides
var slide_duration: int = event[1]
var target_pitch: int = event[2] # Signed
EventType.VIBRATO_ON:
self.channel_vibrato_delay[channel] = event[1]
self.channel_vibrato_rate[channel] = event[2]
self.channel_vibrato_depth[channel] = event[3]
self.channel_vibrato_on[channel] = 1
EventType.VIBRATO_OFF:
self.channel_vibrato_on[channel] = 0
EventType.TREMOLO_ON:
self.channel_tremolo_delay[channel] = event[1]
self.channel_tremolo_rate[channel] = event[2]
self.channel_tremolo_depth[channel] = event[3]
self.channel_tremolo_on[channel] = 1
EventType.TREMOLO_OFF:
self.channel_tremolo_on[channel] = 0
EventType.PAN_LFO_ON:
self.channel_pan_lfo_depth[channel] = event[1]
self.channel_pan_lfo_rate[channel] = event[2]
self.channel_pan_lfo_on[channel] = 1
EventType.PAN_LFO_OFF:
self.channel_pan_lfo_on[channel] = 0
EventType.NOISE_FREQ:
self.channel_noise_freq[channel] = event[1]
EventType.NOISE_ON:
self.channel_noise_on[channel] = 1
EventType.NOISE_OFF:
self.channel_noise_on[channel] = 0
EventType.PITCHMOD_ON:
self.channel_pitchmod_on[channel] = 1
EventType.PITCHMOD_OFF:
self.channel_pitchmod_on[channel] = 0
EventType.ECHO_ON:
self.channel_echo_on[channel] = 1
EventType.ECHO_OFF:
self.channel_echo_on[channel] = 0
EventType.OCTAVE:
self.channel_octave[channel] = event[1]
EventType.OCTAVE_UP:
self.channel_octave[channel] += 1
EventType.OCTAVE_DOWN:
self.channel_octave[channel] -= 1
EventType.TRANSPOSE_ABS:
self.channel_transpose[channel] = event[1]
EventType.TRANSPOSE_REL:
self.channel_transpose[channel] += event[1]
EventType.TUNING:
var fine_tune: int = event[1]
var scale: float
if fine_tune < 0x80:
scale = 1.0 + fine_tune/255.0
else:
scale = fine_tune/255.0
self.channel_fine_tuning[channel] = scale
EventType.PROGCHANGE:
self.channel_instrument_idx[channel] = event[1]
player.stream = self.inst_map[self.channel_instrument_idx[channel]]
# TODO - grab instrument envelope
EventType.ADSR_ATTACK:
self.channel_adsr_attack[channel] = event[1]
EventType.ADSR_DECAY:
self.channel_adsr_decay[channel] = event[1]
EventType.ADSR_SUSTAIN:
self.channel_adsr_sustain[channel] = event[1]
EventType.ADSR_RELEASE:
self.channel_adsr_release[channel] = event[1]
EventType.ADSR_DEFAULT: # TODO - grab instrument envelope
pass
EventType.TEMPO:
self.set_tempo(music.tempo_to_bpm(event[1]))
EventType.TEMPO_SLIDE:
self.set_tempo(music.tempo_to_bpm(event[2]))
var slide_duration: int = event[1]
EventType.ECHO_VOLUME:
self.channel_echo_volume[channel] = event[1]
EventType.ECHO_VOLUME_SLIDE: # TODO: implement slides
self.channel_echo_volume[channel] = event[2]
var slide_duration: int = event[1]
EventType.ECHO_FEEDBACK_FIR: # TODO
var feedback: int = event[1]
var filterIndex: int = event[2]
EventType.MASTER_VOLUME:
self.master_volume = event[1]
EventType.GOTO:
self.channel_pointer[channel] = event[1]
EventType.END:
break
_:
break
return -1 # End of track
func play_pulse(time_offset := 0.0) -> bool: # Return true if any channel played
var active_channels := 0
for channel in self.num_tracks:
if self.channel_next_pulse[channel] < 0:
continue # Channel not playing
active_channels += 1
if self.channel_next_pulse[channel] == 0:
self.channel_next_pulse[channel] = self.play_channel(channel, time_offset)
self.channel_next_pulse[channel] -= 1
return active_channels > 0
var is_playing := false
var bgm_timestamp := 0.0 # Note this will be behind by the maximum delay
func _process(delta: float) -> void:
if self.is_playing:
bgm_timestamp += delta
while bgm_timestamp > seconds_per_pulse:
bgm_timestamp -= seconds_per_pulse
self.is_playing = play_pulse(bgm_timestamp)
if not self.is_playing:
print('BGM finished playing')

View File

@ -18,6 +18,7 @@ var GBA_filename := '2564 - Final Fantasy V Advance (U)(Independent).gba'
var rom_snes := File.new() var rom_snes := File.new()
var snes_data := {} var snes_data := {}
var snes_bytes: PoolByteArray var snes_bytes: PoolByteArray
var snes_buffer: StreamPeerBuffer
var thread := Thread.new() var thread := Thread.new()
func load_snes_structs(buffer: StreamPeerBuffer) -> Dictionary: func load_snes_structs(buffer: StreamPeerBuffer) -> Dictionary:
@ -71,19 +72,19 @@ func load_snes_rom(filename: String):
var rom_size := rom_snes.get_len() var rom_size := rom_snes.get_len()
var bytes := rom_snes.get_buffer(rom_size) var bytes := rom_snes.get_buffer(rom_size)
self.snes_bytes = bytes self.snes_bytes = bytes
var buffer = StreamPeerBuffer.new() self.snes_buffer = StreamPeerBuffer.new()
buffer.data_array = bytes self.snes_buffer.data_array = bytes
self.snes_data = load_snes_structs(buffer) self.snes_data = load_snes_structs(self.snes_buffer)
#print(snes_data.job_levels) #print(snes_data.job_levels)
# Give this its own buffer, avoid file pointer conflicts # Give this its own buffer if threaded, avoid file pointer conflicts
self.load_snes_audio_thread([self.snes_data, buffer.duplicate()]) self.load_snes_audio_thread([self.snes_data, self.snes_buffer])
# var _thread_error = thread.start(self, 'load_snes_audio_thread', [self.snes_data, buffer.duplicate()]) # var _thread_error = thread.start(self, 'load_snes_audio_thread', [self.snes_data, self.snes_buffer.duplicate()])
StringLoader.load_snes_rom(buffer, true) StringLoader.load_snes_rom(self.snes_buffer, true)
SpriteLoader.load_from_structs(self.snes_data) SpriteLoader.load_from_structs(self.snes_data)
SpriteLoader.load_enemy_battle_sprites(self.snes_data, buffer) SpriteLoader.load_enemy_battle_sprites(self.snes_data, self.snes_buffer)
SpriteLoader.load_battle_bgs(self.snes_data, buffer) SpriteLoader.load_battle_bgs(self.snes_data, self.snes_buffer)
MapLoader.load_snes_rom(buffer) MapLoader.load_snes_rom(self.snes_buffer)
func load_psx_folder(_dirname: String): func load_psx_folder(_dirname: String):
pass pass

View File

@ -0,0 +1,278 @@
const MAX_TRACKS := 8
const MAX_LOOP_DEPTH := 8 # Apparently 4, but eh whatever
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
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
# 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
note = -1
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
print('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('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('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('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('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:
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('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('%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('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])
else:
print('track %s has duration %d - %d notes %d rests' % [bgm_id, total_note_dur, total_notes, total_rests])
return events

View File

@ -0,0 +1,56 @@
extends 'res://scripts/loaders/snes/music.gd'
# In real OOP these would be static or static const, but GDScript doesn't allow that
func _init() -> void:
# Durations are in pulses, 48 = 1 quarter note (crotchet)
self.NOTE_DURATIONS = PoolByteArray([192, 144, 96, 64, 72, 48, 32, 36, 24, 16, 12, 8, 6, 4, 3]) # See ROM 0x041D7E to 0x041D8C
self.REFERENCE_NOTE = 71
self.EVENT_MAP = {
0xD2: EventType.VOLUME,
0xD3: EventType.VOLUME_SLIDE,
0xD4: EventType.PAN,
0xD5: EventType.PAN_SLIDE,
0xD6: EventType.PITCH_SLIDE,
0xD7: EventType.VIBRATO_ON,
0xD8: EventType.VIBRATO_OFF,
0xD9: EventType.TREMOLO_ON,
0xDA: EventType.TREMOLO_OFF,
0xDB: EventType.PAN_LFO_ON,
0xDC: EventType.PAN_LFO_OFF,
0xDD: EventType.NOISE_FREQ,
0xDE: EventType.NOISE_ON,
0xDF: EventType.NOISE_OFF,
0xE0: EventType.PITCHMOD_ON,
0xE1: EventType.PITCHMOD_OFF,
0xE2: EventType.ECHO_ON,
0xE3: EventType.ECHO_OFF,
0xE4: EventType.OCTAVE,
0xE5: EventType.OCTAVE_UP,
0xE6: EventType.OCTAVE_DOWN,
0xE7: EventType.TRANSPOSE_ABS,
0xE8: EventType.TRANSPOSE_REL,
0xE9: EventType.TUNING,
0xEA: EventType.PROGCHANGE,
0xEB: EventType.ADSR_ATTACK,
0xEC: EventType.ADSR_DECAY,
0xED: EventType.ADSR_SUSTAIN,
0xEE: EventType.ADSR_RELEASE,
0xEF: EventType.ADSR_DEFAULT,
0xF0: EventType.LOOP_START,
0xF1: EventType.LOOP_END,
0xF2: EventType.END,
0xF3: EventType.TEMPO,
0xF4: EventType.TEMPO_SLIDE,
0xF5: EventType.ECHO_VOLUME,
0xF6: EventType.ECHO_VOLUME_SLIDE,
0xF7: EventType.ECHO_FEEDBACK_FIR,
0xF8: EventType.MASTER_VOLUME,
0xF9: EventType.LOOP_BREAK,
0xFA: EventType.GOTO,
0xFB: EventType.CPU_CONTROLLED_JUMP,
0xFC: EventType.END,
0xFD: EventType.END,
0xFE: EventType.END,
0xFF: EventType.END,
}

View File

@ -1,5 +1,6 @@
extends Node2D extends Node2D
const MusicPlayer := preload('res://scripts/MusicPlayer.gd')
var MusicLoader := preload('res://scripts/loaders/snes/music_ff5.gd').new()
var inst_buttons = [] var inst_buttons = []
var sfx_buttons = [] var sfx_buttons = []
@ -19,7 +20,7 @@ func _create_sfx_buttons():
var btn = Button.new() var btn = Button.new()
btn.text = 'SFX #%02X' % i btn.text = 'SFX #%02X' % i
btn.align = Button.ALIGN_CENTER btn.align = Button.ALIGN_CENTER
btn.set_position(Vector2((i%4)*50, 156 + (i/4)*24)) btn.set_position(Vector2((i%4)*50, 130 + (i/4)*24))
btn.rect_min_size.x = 48 btn.rect_min_size.x = 48
add_child(btn) add_child(btn)
btn.connect('pressed', SoundLoader, 'play_sfx', [i]) btn.connect('pressed', SoundLoader, 'play_sfx', [i])
@ -46,6 +47,66 @@ func _enable_inst_button(id: int):
for i in id+1: for i in id+1:
inst_buttons[i].disabled = false inst_buttons[i].disabled = false
const NUM_CHANNELS := 8
var music_player = null
var inst_sample_map := {}
var bgm_tracksets := {}
func evaluate_bgm(id: int):
var buffer: StreamPeerBuffer = RomLoader.snes_buffer.duplicate()
var bgm_song_ptr: int = RomLoader.snes_data.bgm_song_pointers[id] & 0x3FFFFF
var bank_offset: int = bgm_song_ptr & 0x3F0000
buffer.seek(bgm_song_ptr)
var length := buffer.get_u16()
var rom_address_base := buffer.get_u16()
var track_ptrs := PoolIntArray()
for i in NUM_CHANNELS:
var track_ptr := buffer.get_u16() + bank_offset
if track_ptr < bgm_song_ptr:
track_ptr += 0x010000 # next bank
track_ptrs.append(track_ptr)
var end_ptr := buffer.get_u16() + bank_offset
if end_ptr < bgm_song_ptr:
end_ptr += 0x010000 # next bank
var tracks := []
for i in NUM_CHANNELS:
var track_ptr := track_ptrs[i]
# var channel: MusicChannel = self.channels[i]
# print('Unrolling BGM track %02d:%02d at 0x%06X:0x%06X:0x%06X' % [id, i, bgm_song_ptr, track_ptr, end_ptr])
tracks.append(MusicLoader.unroll_track(buffer.duplicate(), bgm_song_ptr, track_ptr, end_ptr, '%02d:%02d'%[id, i]))
bgm_tracksets[id] = tracks
func play_bgm(id: int) -> void:
var inst_indices = RomLoader.snes_data.bgm_instrument_indices[id]
for i in 16:
var inst_idx: int = inst_indices[i]-1
if inst_idx < 0:
self.inst_sample_map[i + 0x20] = null
else:
self.inst_sample_map[i + 0x20] = SoundLoader.instrument_samples[inst_idx]
if self.music_player:
remove_child(music_player)
self.music_player = MusicPlayer.new(bgm_tracksets[id], self.inst_sample_map)
add_child(self.music_player)
self.music_player.is_playing = true
print('Playing BGM%02d' % id)
func _play_bgm() -> void:
self.play_bgm($sb_bgm.value)
func _create_bgm_playback() -> void:
$sb_bgm.max_value = SoundLoader.BGM_NUM
$btn_bgm.connect('pressed', self, '_play_bgm')
for i in SoundLoader.SFX_NUM:
self.inst_sample_map[i] = SoundLoader.sfx_samples[i]
for i in SoundLoader.BGM_NUM:
evaluate_bgm(i)
# Called when the node enters the scene tree for the first time. # Called when the node enters the scene tree for the first time.
func _ready() -> void: func _ready() -> void:
_create_sfx_buttons() self._create_sfx_buttons()
self._create_bgm_playback()
for i in len(RomLoader.snes_data.bgm_song_pointers):
var pointer = RomLoader.snes_data.bgm_song_pointers[i]
print('BGM 0x%02X (%02d) at 0x%06X' % [i, i, pointer])

View File

@ -4,3 +4,26 @@
[node name="audio_system" type="Node2D"] [node name="audio_system" type="Node2D"]
script = ExtResource( 1 ) script = ExtResource( 1 )
[node name="inst_buttons" type="ReferenceRect" parent="."]
margin_right = 348.0
margin_bottom = 118.0
[node name="sfx_buttons" type="ReferenceRect" parent="."]
margin_top = 130.0
margin_right = 198.0
margin_bottom = 176.0
[node name="sb_bgm" type="SpinBox" parent="."]
margin_top = 192.0
margin_right = 23.0
margin_bottom = 214.0
rect_min_size = Vector2( 38, 0 )
align = 2
[node name="btn_bgm" type="Button" parent="."]
margin_left = 40.0
margin_top = 192.0
margin_right = 102.0
margin_bottom = 214.0
text = "Play BGM"

View File

@ -15,7 +15,6 @@ theme = ExtResource( 6 )
script = ExtResource( 3 ) script = ExtResource( 3 )
[node name="audio_system" parent="." instance=ExtResource( 5 )] [node name="audio_system" parent="." instance=ExtResource( 5 )]
visible = false
position = Vector2( 0, 160 ) position = Vector2( 0, 160 )
[node name="worldmap_system" parent="." instance=ExtResource( 2 )] [node name="worldmap_system" parent="." instance=ExtResource( 2 )]
@ -30,3 +29,4 @@ visible = false
margin_right = 320.0 margin_right = 320.0
[node name="BattleScene" parent="." instance=ExtResource( 7 )] [node name="BattleScene" parent="." instance=ExtResource( 7 )]
visible = false