2024-07-26 19:08:08 +09:30
|
|
|
extends Node2D
|
|
|
|
const MusicRenderer := preload('res://scripts/MusicRenderer.gd')
|
|
|
|
var MusicLoader := preload('res://scripts/loaders/snes/music_ff5.gd').new()
|
|
|
|
onready var audio_renderer: Node = SoundLoader.audio_renderer
|
|
|
|
# Have two music players to crossfade between
|
|
|
|
onready var music_player_1 := AudioStreamPlayer.new()
|
|
|
|
onready var music_player_2 := AudioStreamPlayer.new()
|
|
|
|
var current_bgm := ''
|
|
|
|
var queued_awaiting_render_bgm := ''
|
|
|
|
# SFX don't currently overlap, so just one for now
|
|
|
|
onready var sfx_player := AudioStreamPlayer.new()
|
|
|
|
|
|
|
|
var max_cached_bgms := 6
|
|
|
|
var cached_bgms := {}
|
|
|
|
var cached_bgms_lru := [] # For cache eviction purposes
|
|
|
|
var bgm_tex_bytes := {} # dict<bgm_id: int, PoolByteArray> - used to create texture to render
|
|
|
|
var bgm_target_times := {} # dict<bgm_id: int, float>
|
|
|
|
var bgm_loop_endpoints := {} # dict<bgm_id: int, Vector2>
|
|
|
|
var bgm_tracksets := {} # dict<bgm_id: int, Array<unrolled events>>
|
|
|
|
const NUM_CHANNELS := 8
|
|
|
|
|
|
|
|
func _evict_pcm_cache() -> void:
|
|
|
|
# Remove least-recently-used PCM prerenders
|
|
|
|
var entries_to_evict := len(self.cached_bgms_lru) - self.max_cached_bgms
|
|
|
|
if entries_to_evict < 1:
|
|
|
|
return
|
|
|
|
for i in entries_to_evict:
|
|
|
|
var key: String = self.cached_bgms_lru.pop_front()
|
|
|
|
self.cached_bgms.erase(key)
|
|
|
|
|
|
|
|
func _ready() -> void:
|
|
|
|
music_player_1.name = 'music_player_1'
|
|
|
|
music_player_2.name = 'music_player_2'
|
|
|
|
sfx_player.name = 'sfx_player'
|
|
|
|
add_child(music_player_1)
|
|
|
|
add_child(music_player_2)
|
|
|
|
add_child(sfx_player)
|
|
|
|
audio_renderer.connect('render_initial_ready', self, '_on_render_initial_ready')
|
|
|
|
audio_renderer.connect('render_complete', self, '_on_render_complete')
|
|
|
|
|
|
|
|
func stop() -> void:
|
|
|
|
self.music_player_1.stop()
|
|
|
|
self.music_player_2.stop()
|
|
|
|
self.sfx_player.stop()
|
|
|
|
self.queued_awaiting_render_bgm = ''
|
|
|
|
self.current_bgm = ''
|
|
|
|
|
2024-07-26 22:54:05 +09:30
|
|
|
func play_bgm(key: String, target_time: float = -1.0) -> void:
|
2024-07-26 19:08:08 +09:30
|
|
|
print('@%dms - Playing %s' % [get_ms(), key])
|
2024-07-26 22:54:05 +09:30
|
|
|
_evict_pcm_cache()
|
2024-07-26 19:08:08 +09:30
|
|
|
var bgm_id := int(key.substr(3, 2))
|
|
|
|
if not (bgm_id in audio_renderer.cached_midis):
|
|
|
|
self.queue_prerender_bgm(bgm_id)
|
|
|
|
# TODO: crossfade
|
|
|
|
if self.music_player_1.playing and self.current_bgm.substr(0, 5) == key.substr(0, 5):
|
|
|
|
# Try live transition
|
|
|
|
var tempo_thou := int(key.substr(6))
|
|
|
|
var old_tempo_thou := int(self.current_bgm.substr(6))
|
|
|
|
var old_playback_pos: float = self.music_player_1.get_playback_position()
|
2024-07-26 22:54:05 +09:30
|
|
|
if target_time < 0.0: # Negative (default) time means automatic continuation
|
|
|
|
if old_tempo_thou == tempo_thou:
|
|
|
|
return # Don't even need to refresh LRU since it can only be most recent anyway
|
|
|
|
target_time = old_playback_pos * old_tempo_thou / tempo_thou
|
|
|
|
print('Old temposcale %d, New temposcale %d, Old pos %.2f, New pos %.2f' % [old_tempo_thou, tempo_thou, old_playback_pos, target_time])
|
|
|
|
if target_time < 0.0:
|
|
|
|
target_time = 0.0
|
2024-07-26 19:08:08 +09:30
|
|
|
if key in self.cached_bgms:
|
|
|
|
self.music_player_1.stream = self.cached_bgms[key]
|
|
|
|
self.music_player_1.play(target_time)
|
|
|
|
self.cached_bgms_lru.erase(key) # Move to end of LRU
|
|
|
|
self.cached_bgms_lru.append(key)
|
|
|
|
self.queued_awaiting_render_bgm = ''
|
|
|
|
self.current_bgm = key
|
|
|
|
else:
|
|
|
|
self.queued_awaiting_render_bgm = key
|
|
|
|
audio_renderer.queue_cached_bgm(key)
|
|
|
|
|
|
|
|
func update_tempo(new_tempo_thou: int) -> void:
|
|
|
|
if not self.music_player_1.playing:
|
|
|
|
return
|
|
|
|
self.play_bgm('%s-%04d'%[self.current_bgm.substr(0, 5), new_tempo_thou])
|
|
|
|
|
|
|
|
|
|
|
|
var load_start_tick: int
|
|
|
|
func get_ms(restart: bool = false) -> int:
|
|
|
|
if restart or not self.load_start_tick:
|
|
|
|
self.load_start_tick = Time.get_ticks_msec()
|
|
|
|
return 0
|
|
|
|
return Time.get_ticks_msec() - self.load_start_tick
|
|
|
|
|
|
|
|
func prepare_bgm_for_rendering(bgm_id: int) -> void:
|
|
|
|
var data_and_target_time_and_loops = MusicRenderer.render_channels(self.bgm_tracksets[bgm_id], RomLoader.snes_data.bgm_instrument_indices[bgm_id], 'BGM%02d'%bgm_id)
|
|
|
|
self.bgm_tex_bytes[bgm_id] = data_and_target_time_and_loops[0]
|
|
|
|
self.bgm_target_times[bgm_id] = data_and_target_time_and_loops[1]
|
|
|
|
self.bgm_loop_endpoints[bgm_id] = data_and_target_time_and_loops[2]
|
|
|
|
|
|
|
|
func get_bgm_loop_endpoints(bgm_id: int, tempo_thou: int = 1000) -> Vector2:
|
|
|
|
if not (bgm_id in bgm_loop_endpoints):
|
|
|
|
self.prepare_bgm_for_rendering(bgm_id)
|
|
|
|
var endpoints_1000: Vector2 = bgm_loop_endpoints[bgm_id]
|
|
|
|
return endpoints_1000 / tempo_thou
|
|
|
|
|
|
|
|
func queue_prerender_bgm(bgm_id: int) -> void:
|
|
|
|
if not audio_renderer.initialized_instrument_texture:
|
|
|
|
audio_renderer.initialize_instrument_texture()
|
|
|
|
if not (bgm_id in self.bgm_tex_bytes):
|
|
|
|
self.prepare_bgm_for_rendering(bgm_id)
|
|
|
|
var bgm_key := 'BGM%02d'%bgm_id
|
|
|
|
audio_renderer.push_bytes(self.bgm_tex_bytes[bgm_id], self.bgm_target_times[bgm_id], bgm_key, false)
|
|
|
|
|
|
|
|
|
2024-07-27 22:50:29 +09:30
|
|
|
func save_bgm_disassembly(bgm_id: int, filename: String = '') -> void:
|
|
|
|
if not filename:
|
|
|
|
filename = 'output/BGM%02d disassembly.txt' % bgm_id
|
|
|
|
var disassembly: PoolStringArray = MusicRenderer.disassemble_bgm(MusicManager.bgm_tracksets[bgm_id], RomLoader.snes_data.bgm_instrument_indices[bgm_id])
|
|
|
|
var file := File.new()
|
|
|
|
file.open(filename, File.WRITE)
|
|
|
|
for line in disassembly:
|
|
|
|
file.store_line(line)
|
|
|
|
file.close()
|
|
|
|
|
|
|
|
|
2024-07-26 19:08:08 +09:30
|
|
|
func render_all_bgm(bgms_to_render: int = 70) -> void:
|
|
|
|
audio_renderer.initialize_instrument_texture()
|
|
|
|
for bgm_id in bgms_to_render:
|
|
|
|
self.queue_prerender_bgm(bgm_id)
|
|
|
|
# self.save_bgm_disassembly(bgm_id)
|
|
|
|
if bgm_id % 10 == 9 and bgm_id < bgms_to_render-1:
|
|
|
|
print('@%dms - Processed %d/%d bgm tracks' % [get_ms(), bgm_id+1, bgms_to_render])
|
|
|
|
print('@%dms - Processed %d bgm tracks and sent to gpu for rendering' % [get_ms(), bgms_to_render])
|
|
|
|
|
|
|
|
|
|
|
|
const save_prerendered_audio := false
|
|
|
|
var print_batch_results := ''
|
|
|
|
func _get_prerendered_audio():
|
|
|
|
self.print_batch_results = ''
|
|
|
|
audio_renderer.get_result()
|
|
|
|
if self.print_batch_results:
|
|
|
|
print('@%dms - Rendered %s without saving' % [get_ms(), self.print_batch_results.right(2)])
|
|
|
|
|
|
|
|
func _cache_bgm(cache_key: String, data: PoolByteArray, is_complete: bool = false) -> void:
|
|
|
|
var rendered_audio: AudioStreamSample
|
|
|
|
# var cache_key := 'BGM%02d-%04d' % [bgm_id, tempo_thou]
|
|
|
|
var bgm_id: int
|
|
|
|
var tempo_thou: int
|
|
|
|
if cache_key.substr(0, 3) == 'BGM':
|
|
|
|
bgm_id = int(cache_key.substr(3, 2))
|
|
|
|
tempo_thou = int(cache_key.substr(6))
|
|
|
|
if cache_key in self.cached_bgms:
|
|
|
|
rendered_audio = self.cached_bgms[cache_key]
|
|
|
|
if is_complete:
|
|
|
|
rendered_audio.data = data # Don't overwrite if it's incomplete
|
|
|
|
else:
|
|
|
|
rendered_audio = AudioStreamSample.new()
|
|
|
|
rendered_audio.data = data
|
|
|
|
rendered_audio.stereo = true
|
|
|
|
rendered_audio.mix_rate = 32000
|
|
|
|
rendered_audio.format = AudioStreamSample.FORMAT_16_BITS
|
|
|
|
|
|
|
|
var t_endpoints: Vector2 = bgm_loop_endpoints[bgm_id] * 1000 / tempo_thou # Floating point, safe to divide here
|
|
|
|
var smp_endpoints: Vector2 = (t_endpoints * 32000).round()
|
|
|
|
if smp_endpoints.x >= 0:
|
|
|
|
rendered_audio.loop_begin = int(smp_endpoints.x)
|
|
|
|
rendered_audio.loop_end = int(smp_endpoints.y)
|
|
|
|
rendered_audio.loop_mode = AudioStreamSample.LOOP_FORWARD
|
|
|
|
print('Should loop from %.2fs to %.2fs' % [t_endpoints.x, t_endpoints.y])
|
|
|
|
|
|
|
|
self.cached_bgms[cache_key] = rendered_audio
|
|
|
|
self.cached_bgms_lru.erase(cache_key) # Move to end of LRU
|
|
|
|
self.cached_bgms_lru.append(cache_key)
|
|
|
|
|
|
|
|
if self.queued_awaiting_render_bgm == cache_key:
|
|
|
|
print('@%dms - Rendered initial chunk of %s for immediate playback (%d samples at 32kHz = %.2fs)' % [get_ms(), cache_key, len(data)/4, rendered_audio.get_length()])
|
|
|
|
self.play_bgm(cache_key)
|
|
|
|
if save_prerendered_audio:
|
|
|
|
var error = self.cached_bgms[cache_key].save_to_wav('output/rendered_%s.wav'%cache_key)
|
|
|
|
print('@%dms - Saved render of %s (error code %s)' % [get_ms(), cache_key, globals.ERROR_CODE_STRINGS[error]])
|
|
|
|
else:
|
|
|
|
# print('@%dms - Rendered %s without saving' % [get_ms(), key])
|
|
|
|
self.print_batch_results = '%s, %s (%.2fs %.2fMiB)'%[self.print_batch_results, cache_key, self.cached_bgms[cache_key].get_length(), len(data)/0x100000]
|
|
|
|
|
|
|
|
func _on_render_initial_ready(key: String):
|
|
|
|
print('@%dms - _on_render_initial_ready("%s")' % [get_ms(), key])
|
|
|
|
# Used for JAOT playback
|
|
|
|
var remaining_samples_and_data = audio_renderer.cached_renders[key]
|
|
|
|
var data: PoolByteArray = remaining_samples_and_data[1]
|
|
|
|
self._cache_bgm(key, data, false)
|
|
|
|
|
|
|
|
func _on_render_complete(key: String):
|
|
|
|
print('@%dms - _on_render_complete("%s")' % [get_ms(), key])
|
|
|
|
# Used for JAOT playback
|
|
|
|
var remaining_samples_and_data = audio_renderer.cached_renders[key]
|
|
|
|
audio_renderer.cached_renders.erase(key) # Purge from audio_renderer's output cache
|
|
|
|
var data: PoolByteArray = remaining_samples_and_data[1]
|
|
|
|
|
|
|
|
if remaining_samples_and_data[0] != 0: # Should be 0
|
|
|
|
print_debug('render_completed signal for incomplete render! %s has %d remaining samples, should be 0'%[key, remaining_samples_and_data[0]])
|
|
|
|
# Assume _on_render_initial_ready was already called and AudioStreamSample has already been created
|
|
|
|
self._cache_bgm(key, data, true)
|
|
|
|
|
|
|
|
func load_snes_rom(snes_buffer: StreamPeerBuffer) -> void:
|
|
|
|
var buffer: StreamPeerBuffer = snes_buffer.duplicate()
|
|
|
|
for i in SoundLoader.BGM_NUM:
|
|
|
|
self._evaluate_bgm(buffer, i)
|
|
|
|
|
|
|
|
func _evaluate_bgm(buffer: StreamPeerBuffer, id: int):
|
|
|
|
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
|
|
|
|
self.bgm_tracksets[id] = MusicLoader.unroll_bgm(buffer.duplicate(), bgm_song_ptr, track_ptrs, end_ptr, '%02d'%id)
|
|
|
|
|
|
|
|
func _process(_delta):
|
|
|
|
update()
|
|
|
|
|
|
|
|
func _draw() -> void:
|
|
|
|
if audio_renderer.waiting_for_viewport:
|
|
|
|
self._get_prerendered_audio()
|