[BGM] Refactor playback and rendering to a Manager singleton
This commit is contained in:
parent
9abf1b49c8
commit
603c84cbc1
|
@ -64,7 +64,7 @@ This will be an interpreter, I am not hardcoding the thousands of scripts.
|
||||||
|
|
||||||
### Sound System
|
### Sound System
|
||||||
- [x] Instrument samples
|
- [x] Instrument samples
|
||||||
- [x] Basic Music playing (currently loops correctly, but tuning is a bit off on looped samples, possible Godot bug to investigate)
|
- [x] Basic Music playing (currently loops correctly for most songs)
|
||||||
- [ ] DSP stuff including ADSR envelopes
|
- [ ] DSP stuff including ADSR envelopes
|
||||||
|
|
||||||
# What's different?
|
# What's different?
|
||||||
|
|
|
@ -33,6 +33,7 @@ SaveLoader="*res://scripts/loaders/SaveLoader.gd"
|
||||||
ThemeManager="*res://scripts/managers/ThemeManager.gd"
|
ThemeManager="*res://scripts/managers/ThemeManager.gd"
|
||||||
StringLoader="*res://scripts/loaders/StringLoader.gd"
|
StringLoader="*res://scripts/loaders/StringLoader.gd"
|
||||||
HTML5="*res://scripts/managers/HTML5.gd"
|
HTML5="*res://scripts/managers/HTML5.gd"
|
||||||
|
MusicManager="*res://scripts/managers/MusicManager.gd"
|
||||||
|
|
||||||
[debug]
|
[debug]
|
||||||
|
|
||||||
|
@ -65,7 +66,7 @@ texture={
|
||||||
|
|
||||||
[rendering]
|
[rendering]
|
||||||
|
|
||||||
quality/driver/driver_name="GLES2"
|
quality/driver/fallback_to_gles2=true
|
||||||
2d/snapping/use_gpu_pixel_snap=true
|
2d/snapping/use_gpu_pixel_snap=true
|
||||||
vram_compression/import_s3tc=false
|
vram_compression/import_s3tc=false
|
||||||
vram_compression/import_etc=true
|
vram_compression/import_etc=true
|
||||||
|
|
|
@ -459,12 +459,10 @@ static func render_channels(tracks: Array, inst_map: Array, _debug_name := 'none
|
||||||
midi_events_bytes3.put_32(0)
|
midi_events_bytes3.put_32(0)
|
||||||
midi_events_bytes_adsr.put_32(0)
|
midi_events_bytes_adsr.put_32(0)
|
||||||
data += midi_events_bytes_t_event_start.data_array + midi_events_bytes_t_end.data_array + midi_events_bytes3.data_array + midi_events_bytes_adsr.data_array + midi_events_bytes_t_note_start.data_array
|
data += midi_events_bytes_t_event_start.data_array + midi_events_bytes_t_end.data_array + midi_events_bytes3.data_array + midi_events_bytes_adsr.data_array + midi_events_bytes_t_note_start.data_array
|
||||||
var smp_loop_start = -1
|
var t_loop_endpoints := Vector2(-1, -1)
|
||||||
var smp_loop_end = -1
|
|
||||||
if highest_channel_p_return >= 0:
|
if highest_channel_p_return >= 0:
|
||||||
smp_loop_start = curve_master_tempo.get_integral(highest_channel_p_return + 100) * 32000
|
t_loop_endpoints = Vector2(curve_master_tempo.get_integral(highest_channel_p_return + 100), curve_master_tempo.get_integral(longest_channel_p_end + 100))
|
||||||
smp_loop_end = curve_master_tempo.get_integral(longest_channel_p_end + 100) * 32000
|
return [data, target_time_length, t_loop_endpoints]
|
||||||
return [data, target_time_length, [smp_loop_start, smp_loop_end]]
|
|
||||||
|
|
||||||
|
|
||||||
static func disassemble_channel_events(channel_events: Array, inst_map: Array) -> PoolStringArray:
|
static func disassemble_channel_events(channel_events: Array, inst_map: Array) -> PoolStringArray:
|
||||||
|
|
|
@ -21,6 +21,8 @@ onready var cached_renders: Dictionary = {} # key: [remaining_samples, PoolByte
|
||||||
onready var current_textures: Array = [] # of ImageTextures - Needed to prevent GC before draw
|
onready var current_textures: Array = [] # of ImageTextures - Needed to prevent GC before draw
|
||||||
onready var waiting_for_viewport: Array = []
|
onready var waiting_for_viewport: Array = []
|
||||||
onready var done_first_draw: bool = false
|
onready var done_first_draw: bool = false
|
||||||
|
var initialized_instrument_texture := false
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
self.material = ShaderMaterial.new()
|
self.material = ShaderMaterial.new()
|
||||||
|
@ -65,14 +67,23 @@ func _update_viewport(width: int, height: int) -> void:
|
||||||
self.material.set_shader_param('INT_OUTPUT_WIDTH', OUTPUT_WIDTH)
|
self.material.set_shader_param('INT_OUTPUT_WIDTH', OUTPUT_WIDTH)
|
||||||
|
|
||||||
|
|
||||||
|
func initialize_instrument_texture(reinitialize := false) -> void:
|
||||||
|
if not reinitialize and self.initialized_instrument_texture:
|
||||||
|
return
|
||||||
|
SoundLoader.samples_to_texture()
|
||||||
|
self.material.set_shader_param('instrument_samples', SoundLoader.samples_tex)
|
||||||
|
self.material.set_shader_param('instrument_samples_size', SoundLoader.samples_tex.get_size())
|
||||||
|
self.initialized_instrument_texture = true
|
||||||
|
|
||||||
|
|
||||||
func _get_cached_midi(key: String) -> Array: # [target_samples: int, tex: ImageTexture, tempo_scale_thousandths: int]
|
func _get_cached_midi(key: String) -> Array: # [target_samples: int, tex: ImageTexture, tempo_scale_thousandths: int]
|
||||||
if not('-' in key):
|
if not('-' in key):
|
||||||
return self.cached_midis[key] + [1000]
|
return self.cached_midis[key] + [1000]
|
||||||
var split := key.split('-')
|
var split := key.split('-')
|
||||||
var tempo_scale_thousandths := int(split[1])
|
var tempo_scale_thousandths := int(split[1])
|
||||||
var target_samples_and_tex = self.cached_midis[split[0]]
|
var target_time_and_tex = self.cached_midis[split[0]]
|
||||||
var new_target_samples: int = (target_samples_and_tex[0]*1000)/tempo_scale_thousandths
|
var new_target_samples: int = (target_time_and_tex[0]*32000*1000)/tempo_scale_thousandths
|
||||||
return [new_target_samples, target_samples_and_tex[1], tempo_scale_thousandths]
|
return [new_target_samples, target_time_and_tex[1], tempo_scale_thousandths]
|
||||||
|
|
||||||
|
|
||||||
func _render_midi(key: String, output_rows_drawn_including_this: int, rows_to_draw: int) -> void:
|
func _render_midi(key: String, output_rows_drawn_including_this: int, rows_to_draw: int) -> void:
|
||||||
|
@ -153,26 +164,29 @@ func _render_just_ahead_of_time() -> void: # Optimized for latency
|
||||||
self.waiting_for_viewport.append([rows_to_draw, key]) # Grab the result next draw
|
self.waiting_for_viewport.append([rows_to_draw, key]) # Grab the result next draw
|
||||||
|
|
||||||
|
|
||||||
func push_image(image: Image, target_samples: int, key: String, enqueue: bool = true) -> void:
|
func push_image(image: Image, target_time: int, key: String, enqueue: bool = true) -> void:
|
||||||
|
if not self.initialized_instrument_texture:
|
||||||
|
self.initialize_instrument_texture()
|
||||||
|
|
||||||
var tex := ImageTexture.new()
|
var tex := ImageTexture.new()
|
||||||
tex.create_from_image(image, 0)
|
tex.create_from_image(image, 0)
|
||||||
self.cached_midis[key] = [target_samples, tex]
|
self.cached_midis[key] = [target_time, tex]
|
||||||
self.material.set_shader_param('midi_events_size', tex.get_size()) # Should all be the same size for now, revisit if we need mixed sizes.
|
self.material.set_shader_param('midi_events_size', tex.get_size()) # Should all be the same size for now, revisit if we need mixed sizes.
|
||||||
if enqueue:
|
if enqueue:
|
||||||
self.render_queue.append([key, target_samples])
|
self.render_queue.append([key, target_time])
|
||||||
|
|
||||||
func push_bytes(data: PoolByteArray, target_samples: int, key: String, enqueue: bool = true) -> void:
|
func push_bytes(data: PoolByteArray, target_time: int, key: String, enqueue: bool = true) -> void:
|
||||||
var rows = int(pow(2, ceil(log((len(data)/INPUT_BYTES_PER_TEXEL) / INPUT_TEX_WIDTH)/log(2))))
|
var rows = int(pow(2, ceil(log((len(data)/INPUT_BYTES_PER_TEXEL) / INPUT_TEX_WIDTH)/log(2))))
|
||||||
var target_length = rows * INPUT_BYTES_PER_TEXEL * INPUT_TEX_WIDTH
|
var target_length = rows * INPUT_BYTES_PER_TEXEL * INPUT_TEX_WIDTH
|
||||||
while len(data) < target_length: # This is inefficient, but this function should be called with pre-padded data anyway
|
while len(data) < target_length: # This is inefficient, but this function should be called with pre-padded data anyway
|
||||||
data.append(0)
|
data.append(0)
|
||||||
var image := Image.new()
|
var image := Image.new()
|
||||||
image.create_from_data(INPUT_TEX_WIDTH, rows, false, INPUT_FORMAT, data)
|
image.create_from_data(INPUT_TEX_WIDTH, rows, false, INPUT_FORMAT, data)
|
||||||
self.push_image(image, target_samples, key, enqueue)
|
self.push_image(image, target_time, key, enqueue)
|
||||||
|
|
||||||
func queue_cached_bgm(key: String) -> void:
|
func queue_cached_bgm(key: String) -> void:
|
||||||
var new_target_samples_etc := self._get_cached_midi(key)
|
var new_target_time_etc := self._get_cached_midi(key)
|
||||||
self.render_queue.append([key, new_target_samples_etc[0]])
|
self.render_queue.append([key, new_target_time_etc[0]])
|
||||||
|
|
||||||
|
|
||||||
func get_result() -> void:
|
func get_result() -> void:
|
||||||
|
|
|
@ -105,6 +105,8 @@ func load_snes_rom_from_bytes(bytes: PoolByteArray) -> void:
|
||||||
SpriteLoader.load_battle_bgs(self.snes_data, self.snes_buffer)
|
SpriteLoader.load_battle_bgs(self.snes_data, self.snes_buffer)
|
||||||
yield(_on_loader_loading_stage_updated('Loading map data', 'MapLoader'), 'completed')
|
yield(_on_loader_loading_stage_updated('Loading map data', 'MapLoader'), 'completed')
|
||||||
MapLoader.load_snes_rom(self.snes_data, self.snes_buffer)
|
MapLoader.load_snes_rom(self.snes_data, self.snes_buffer)
|
||||||
|
yield(_on_loader_loading_stage_updated('Parsing music data', 'MusicManager'), 'completed')
|
||||||
|
MusicManager.load_snes_rom(self.snes_buffer)
|
||||||
yield(_on_loader_loading_stage_updated('Finished loading!', 'RomLoader'), 'completed')
|
yield(_on_loader_loading_stage_updated('Finished loading!', 'RomLoader'), 'completed')
|
||||||
emit_signal('rom_loaded')
|
emit_signal('rom_loaded')
|
||||||
var music = load('res://scripts/loaders/snes/music_ff5.gd').new()
|
var music = load('res://scripts/loaders/snes/music_ff5.gd').new()
|
||||||
|
|
|
@ -0,0 +1,212 @@
|
||||||
|
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) -> void:
|
||||||
|
print('@%dms - Playing %s' % [get_ms(), key])
|
||||||
|
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
|
||||||
|
var target_time := 0.0
|
||||||
|
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()
|
||||||
|
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 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 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()
|
|
@ -6,19 +6,12 @@ const MusicRenderer := preload('res://scripts/MusicRenderer.gd')
|
||||||
var MusicLoader := preload('res://scripts/loaders/snes/music_ff5.gd').new()
|
var MusicLoader := preload('res://scripts/loaders/snes/music_ff5.gd').new()
|
||||||
onready var bgm_titles := Common.load_glyph_table('res://data/5/bgm_titles.txt')
|
onready var bgm_titles := Common.load_glyph_table('res://data/5/bgm_titles.txt')
|
||||||
onready var audio_renderer: Node = SoundLoader.audio_renderer
|
onready var audio_renderer: Node = SoundLoader.audio_renderer
|
||||||
onready var audio_player := $audio_player
|
|
||||||
var prerendered_bgms := {}
|
|
||||||
var prerendered_bgm_start_and_end_loops := {}
|
|
||||||
var inst_buttons = []
|
var inst_buttons = []
|
||||||
var sfx_buttons = []
|
var sfx_buttons = []
|
||||||
var initialized_instrument_texture := false
|
|
||||||
var queued_bgm_playback := ''
|
|
||||||
var current_bgm_playback := ''
|
|
||||||
|
|
||||||
const NUM_CHANNELS := 8
|
const NUM_CHANNELS := 8
|
||||||
var music_player = null
|
var music_player = null
|
||||||
var inst_sample_map := {}
|
var inst_sample_map := {}
|
||||||
var bgm_tracksets := {}
|
|
||||||
|
|
||||||
func _create_sfx_buttons():
|
func _create_sfx_buttons():
|
||||||
var disable_btn := !SoundLoader.has_loaded_audio_samples
|
var disable_btn := !SoundLoader.has_loaded_audio_samples
|
||||||
|
@ -69,45 +62,6 @@ 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
|
||||||
|
|
||||||
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
|
|
||||||
bgm_tracksets[id] = MusicLoader.unroll_bgm(buffer.duplicate(), bgm_song_ptr, track_ptrs, end_ptr, '%02d'%id)
|
|
||||||
|
|
||||||
func _play_bgm_jaot(key: String) -> void:
|
|
||||||
self.queued_bgm_playback = ''
|
|
||||||
var target_time := 0.0
|
|
||||||
var bgm_key_and_tempo_thou := key.split('-')
|
|
||||||
var bgm_key := bgm_key_and_tempo_thou[0]
|
|
||||||
var tempo_thou := int(bgm_key_and_tempo_thou[1])
|
|
||||||
if key == self.current_bgm_playback:
|
|
||||||
self.audio_player.play(0)
|
|
||||||
return
|
|
||||||
if audio_player.playing and self.current_bgm_playback.begins_with(bgm_key):
|
|
||||||
var old_tempo_thou := int(self.current_bgm_playback.split('-')[1])
|
|
||||||
var old_playback_pos: float = audio_player.get_playback_position()
|
|
||||||
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])
|
|
||||||
else:
|
|
||||||
print('audioplayer was not playing same bgm')
|
|
||||||
self.audio_player.stream = self.prerendered_bgms[key]
|
|
||||||
self.audio_player.play(target_time)
|
|
||||||
self.prerendered_bgms.erase(self.current_bgm_playback) # purge previous stream from cache
|
|
||||||
self.current_bgm_playback = key
|
|
||||||
|
|
||||||
|
|
||||||
func play_bgm(id: int, live: bool) -> void:
|
func play_bgm(id: int, live: bool) -> void:
|
||||||
self._stop_all()
|
self._stop_all()
|
||||||
|
@ -122,22 +76,14 @@ func play_bgm(id: int, live: bool) -> void:
|
||||||
self.inst_sample_map[i + 0x20] = SoundLoader.instrument_samples_HACK_EXTENDED_LOOPS[inst_idx]
|
self.inst_sample_map[i + 0x20] = SoundLoader.instrument_samples_HACK_EXTENDED_LOOPS[inst_idx]
|
||||||
if self.music_player:
|
if self.music_player:
|
||||||
remove_child(music_player)
|
remove_child(music_player)
|
||||||
self.music_player = MusicPlayer.new(bgm_tracksets[id], self.inst_sample_map)
|
self.music_player = MusicPlayer.new(MusicManager.bgm_tracksets[id], self.inst_sample_map)
|
||||||
add_child(self.music_player)
|
add_child(self.music_player)
|
||||||
self.music_player.is_playing = true
|
self.music_player.is_playing = true
|
||||||
else:
|
else:
|
||||||
# Play prerendered
|
# Play prerendered
|
||||||
var bgm_key = 'BGM%02d'%id
|
|
||||||
var tempo_thou = $slider_tempo.value * 10
|
var tempo_thou = $slider_tempo.value * 10
|
||||||
var new_key = '%s-%04d' % [bgm_key, tempo_thou]
|
var key = 'BGM%02d-%04d' % [id, tempo_thou]
|
||||||
print('@%dms - Playing %s' % [get_ms(), new_key])
|
MusicManager.play_bgm(key)
|
||||||
if not (bgm_key in self.prerendered_bgms):
|
|
||||||
self.queue_prerender_bgm(id)
|
|
||||||
if new_key in self.prerendered_bgms:
|
|
||||||
self._play_bgm_jaot(new_key)
|
|
||||||
else:
|
|
||||||
audio_renderer.queue_cached_bgm(new_key)
|
|
||||||
self.queued_bgm_playback = new_key
|
|
||||||
|
|
||||||
func _play_bgm_live() -> void:
|
func _play_bgm_live() -> void:
|
||||||
self.play_bgm($sb_bgm.value, true)
|
self.play_bgm($sb_bgm.value, true)
|
||||||
|
@ -155,8 +101,6 @@ func _create_bgm_playback() -> void:
|
||||||
$btn_render.connect('pressed', self, 'render_all_bgm')
|
$btn_render.connect('pressed', self, 'render_all_bgm')
|
||||||
for i in SoundLoader.SFX_NUM:
|
for i in SoundLoader.SFX_NUM:
|
||||||
self.inst_sample_map[i] = SoundLoader.sfx_samples[i]
|
self.inst_sample_map[i] = SoundLoader.sfx_samples[i]
|
||||||
for i in SoundLoader.BGM_NUM:
|
|
||||||
evaluate_bgm(i)
|
|
||||||
|
|
||||||
|
|
||||||
func _stop_all() -> void:
|
func _stop_all() -> void:
|
||||||
|
@ -164,7 +108,7 @@ func _stop_all() -> void:
|
||||||
self.music_player.queue_free()
|
self.music_player.queue_free()
|
||||||
self.music_player = null
|
self.music_player = null
|
||||||
SoundLoader.player.stop()
|
SoundLoader.player.stop()
|
||||||
self.audio_player.stop()
|
MusicManager.stop()
|
||||||
|
|
||||||
|
|
||||||
func _update_bgm_label(id = 0) -> void:
|
func _update_bgm_label(id = 0) -> void:
|
||||||
|
@ -177,11 +121,7 @@ func _update_bgm_label(id = 0) -> void:
|
||||||
func _update_tempo(tempo := 100) -> void:
|
func _update_tempo(tempo := 100) -> void:
|
||||||
$lbl_tempo.text = 'Tempo scale: %d' % tempo
|
$lbl_tempo.text = 'Tempo scale: %d' % tempo
|
||||||
var tempo_thou = tempo * 10
|
var tempo_thou = tempo * 10
|
||||||
if audio_player.is_playing():
|
MusicManager.update_tempo(tempo_thou)
|
||||||
var bgm_key = 'BGM%02d-%04d' % [$sb_bgm.value, tempo_thou]
|
|
||||||
# TODO: render a tempo-scaled version
|
|
||||||
audio_renderer.queue_cached_bgm(bgm_key)
|
|
||||||
self.queued_bgm_playback = bgm_key
|
|
||||||
|
|
||||||
func _reset_tempo() -> void:
|
func _reset_tempo() -> void:
|
||||||
self._update_tempo()
|
self._update_tempo()
|
||||||
|
@ -198,11 +138,6 @@ func _ready() -> void:
|
||||||
$slider_tempo.connect('value_changed', self, '_update_tempo')
|
$slider_tempo.connect('value_changed', self, '_update_tempo')
|
||||||
$btn_reset_tempo.connect('pressed', self, '_reset_tempo')
|
$btn_reset_tempo.connect('pressed', self, '_reset_tempo')
|
||||||
$btn_exit.connect('pressed', self, '_exit')
|
$btn_exit.connect('pressed', self, '_exit')
|
||||||
audio_renderer.connect('render_initial_ready', self, '_on_render_initial_ready')
|
|
||||||
audio_renderer.connect('render_complete', self, '_on_render_complete')
|
|
||||||
# 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])
|
|
||||||
|
|
||||||
|
|
||||||
var load_start_tick: int
|
var load_start_tick: int
|
||||||
|
@ -213,19 +148,10 @@ func get_ms(restart: bool = false) -> int:
|
||||||
return Time.get_ticks_msec() - self.load_start_tick
|
return Time.get_ticks_msec() - self.load_start_tick
|
||||||
|
|
||||||
|
|
||||||
func initialize_instrument_texture() -> void:
|
|
||||||
get_ms(true)
|
|
||||||
SoundLoader.samples_to_texture()
|
|
||||||
audio_renderer.material.set_shader_param('instrument_samples', SoundLoader.samples_tex)
|
|
||||||
audio_renderer.material.set_shader_param('instrument_samples_size', SoundLoader.samples_tex.get_size())
|
|
||||||
self.initialized_instrument_texture = true
|
|
||||||
print('@%dms - Initialized instrument samples texture' % get_ms())
|
|
||||||
|
|
||||||
|
|
||||||
func save_bgm_disassembly(bgm_id: int, filename: String = '') -> void:
|
func save_bgm_disassembly(bgm_id: int, filename: String = '') -> void:
|
||||||
if not filename:
|
if not filename:
|
||||||
filename = 'output/BGM%02d disassembly.txt' % bgm_id
|
filename = 'output/BGM%02d disassembly.txt' % bgm_id
|
||||||
var disassembly: PoolStringArray = MusicRenderer.disassemble_bgm(bgm_tracksets[bgm_id], RomLoader.snes_data.bgm_instrument_indices[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()
|
var file := File.new()
|
||||||
file.open(filename, File.WRITE)
|
file.open(filename, File.WRITE)
|
||||||
for line in disassembly:
|
for line in disassembly:
|
||||||
|
@ -233,88 +159,6 @@ func save_bgm_disassembly(bgm_id: int, filename: String = '') -> void:
|
||||||
file.close()
|
file.close()
|
||||||
|
|
||||||
|
|
||||||
func queue_prerender_bgm(bgm_id: int) -> void:
|
|
||||||
if not self.initialized_instrument_texture:
|
|
||||||
self.initialize_instrument_texture()
|
|
||||||
var data_and_target_time_and_loops = MusicRenderer.render_channels(bgm_tracksets[bgm_id], RomLoader.snes_data.bgm_instrument_indices[bgm_id], 'BGM%02d'%bgm_id)
|
|
||||||
var data = data_and_target_time_and_loops[0]
|
|
||||||
var target_time = data_and_target_time_and_loops[1]
|
|
||||||
var target_samples = target_time * 32000
|
|
||||||
var bgm_key := 'BGM%02d'%bgm_id
|
|
||||||
audio_renderer.push_bytes(data, target_samples, bgm_key, false)
|
|
||||||
self.prerendered_bgm_start_and_end_loops[bgm_key] = data_and_target_time_and_loops[2]
|
|
||||||
|
|
||||||
|
|
||||||
func render_all_bgm(bgms_to_render: int = 70) -> void:
|
|
||||||
$btn_render.set_disabled(true)
|
|
||||||
self.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 _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 rendered_audio := AudioStreamSample.new()
|
|
||||||
rendered_audio.data = remaining_samples_and_data[1]
|
|
||||||
rendered_audio.stereo = true
|
|
||||||
rendered_audio.mix_rate = 32000
|
|
||||||
rendered_audio.format = AudioStreamSample.FORMAT_16_BITS
|
|
||||||
var tempo_thou := 1000
|
|
||||||
if '-' in key:
|
|
||||||
var bgm_key_and_tempo := key.split('-')
|
|
||||||
tempo_thou = int(bgm_key_and_tempo[1])
|
|
||||||
var bgm_key = bgm_key_and_tempo[0]
|
|
||||||
if prerendered_bgm_start_and_end_loops[bgm_key][0] >= 0:
|
|
||||||
rendered_audio.loop_begin = int(round(prerendered_bgm_start_and_end_loops[bgm_key][0] * 1000 / tempo_thou))
|
|
||||||
rendered_audio.loop_end = int(round(prerendered_bgm_start_and_end_loops[bgm_key][1] * 1000 / tempo_thou))
|
|
||||||
rendered_audio.loop_mode = AudioStreamSample.LOOP_FORWARD
|
|
||||||
else:
|
|
||||||
if prerendered_bgm_start_and_end_loops[key][0] >= 0:
|
|
||||||
rendered_audio.loop_begin = int(round(prerendered_bgm_start_and_end_loops[key][0]))
|
|
||||||
rendered_audio.loop_end = int(round(prerendered_bgm_start_and_end_loops[key][1]))
|
|
||||||
rendered_audio.loop_mode = AudioStreamSample.LOOP_FORWARD
|
|
||||||
print('Should loop from %.2fs to %.2fs' % [rendered_audio.loop_begin/32000.0, rendered_audio.loop_end/32000.0])
|
|
||||||
self.prerendered_bgms[key] = rendered_audio
|
|
||||||
|
|
||||||
if self.queued_bgm_playback == key:
|
|
||||||
print('@%dms - Rendered initial chunk of %s for immediate playback (%d samples at 32kHz = %.2fs)' % [get_ms(), key, len(remaining_samples_and_data[1])/4, rendered_audio.get_length()])
|
|
||||||
self._play_bgm_jaot(key)
|
|
||||||
|
|
||||||
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)
|
|
||||||
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.prerendered_bgms[key].data = data
|
|
||||||
print('Should loop from %.2fs to %.2fs' % [self.prerendered_bgms[key].loop_begin/32000.0, self.prerendered_bgms[key].loop_end/32000.0])
|
|
||||||
if save_prerendered_audio:
|
|
||||||
var error = self.prerendered_bgms[key].save_to_wav('output/rendered_%s.wav'%key)
|
|
||||||
print('@%dms - Saved render of %s (error code %s)' % [get_ms(), 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, key, self.prerendered_bgms[key].get_length(), len(data)/0x100000]
|
|
||||||
|
|
||||||
|
|
||||||
func get_shader_test_pattern() -> PoolByteArray:
|
func get_shader_test_pattern() -> PoolByteArray:
|
||||||
var midi_events_bytes := StreamPeerBuffer.new()
|
var midi_events_bytes := StreamPeerBuffer.new()
|
||||||
var midi_events_bytes2 := StreamPeerBuffer.new()
|
var midi_events_bytes2 := StreamPeerBuffer.new()
|
||||||
|
@ -332,11 +176,3 @@ func get_shader_test_pattern() -> PoolByteArray:
|
||||||
# midi_events_bytes3.put_u8(i%256) # pan
|
# midi_events_bytes3.put_u8(i%256) # pan
|
||||||
midi_events_bytes4.put_32(0) # ADSR
|
midi_events_bytes4.put_32(0) # ADSR
|
||||||
return midi_events_bytes.data_array + midi_events_bytes2.data_array + midi_events_bytes3.data_array + midi_events_bytes4.data_array
|
return midi_events_bytes.data_array + midi_events_bytes2.data_array + midi_events_bytes3.data_array + midi_events_bytes4.data_array
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta):
|
|
||||||
update()
|
|
||||||
|
|
||||||
func _draw() -> void:
|
|
||||||
if audio_renderer.waiting_for_viewport:
|
|
||||||
self._get_prerendered_audio()
|
|
||||||
|
|
|
@ -62,8 +62,6 @@ margin_right = 84.0
|
||||||
margin_bottom = 137.0
|
margin_bottom = 137.0
|
||||||
text = "BGM Playback"
|
text = "BGM Playback"
|
||||||
|
|
||||||
[node name="audio_player" type="AudioStreamPlayer" parent="."]
|
|
||||||
|
|
||||||
[node name="btn_exit" type="Button" parent="."]
|
[node name="btn_exit" type="Button" parent="."]
|
||||||
margin_left = 245.0
|
margin_left = 245.0
|
||||||
margin_top = 218.0
|
margin_top = 218.0
|
||||||
|
|
Loading…
Reference in New Issue