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 - used to create texture to render var bgm_target_times := {} # dict var bgm_loop_endpoints := {} # dict var bgm_tracksets := {} # dict> 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) 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()