Basic music playback
This commit is contained in:
parent
0ef19b45a7
commit
bd5598f98f
|
@ -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"
|
||||||
|
|
|
@ -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')
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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,
|
||||||
|
}
|
|
@ -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])
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue