ChocolateBird/scripts/managers/MusicManager.gd

229 lines
9.5 KiB
GDScript3
Raw Permalink Normal View History

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 = ''
func play_bgm(key: String, target_time: float = -1.0) -> void:
print('@%dms - Playing %s' % [get_ms(), key])
_evict_pcm_cache()
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()
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
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()
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()