From f1fb01501ce3129b7cceb44e4685c6863067f849 Mon Sep 17 00:00:00 2001 From: Luke Hubmayer-Werner Date: Fri, 5 Jul 2024 16:01:08 +0930 Subject: [PATCH] Add loop extension hack for audio samples --- scripts/loaders/SoundLoader.gd | 39 +++++++++++++++++++++++++++---- test/audio_system.gd | 10 +++++++- test/audio_system.tscn | 21 ++++++++++++----- theme/checkbox_filled.png | Bin 0 -> 155 bytes theme/checkbox_filled.png.import | 35 +++++++++++++++++++++++++++ theme/menu_theme.tres | 14 ++++++++++- 6 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 theme/checkbox_filled.png create mode 100644 theme/checkbox_filled.png.import diff --git a/scripts/loaders/SoundLoader.gd b/scripts/loaders/SoundLoader.gd index 874a802..3f243a1 100644 --- a/scripts/loaders/SoundLoader.gd +++ b/scripts/loaders/SoundLoader.gd @@ -17,10 +17,32 @@ const SFX_BRR_SPC_TABLE := 0x041F4F + 2 # (first two bytes are the length of 0x0 const SFX_BRR_START := 0x041E3F + 2 # First two bytes are the length of the block, 0x010E = 270 bytes = 16 BRR packets = 480 samples const SFX_ADSR := 0x041F71 const SFX_SR := 0x041F83 -const PREPEND_MS := 50 # Prepend 20ms of silence to each sample for preplay purposes +const PREPEND_MS := 20 # Prepend 20ms of silence to each sample for preplay purposes const PLAY_START := PREPEND_MS / 1000.0 +const BYTES_PER_SAMPLE := 2 # 16bit samples + +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# !!! Testing a workaround for a Godot 3.x AudioStreamSample playback: !!! +# !!! Copy the looped samples one or more times to avoid the break in interpolation at buffer end. !!! +# !!! Adding a few ms to the loops removes harshness. !!! +const HACK_EXTEND_LOOP_SAMPLE_EXTRA_MS := 2 # !!! +func HACK_EXTEND_LOOP_SAMPLE(audio: AudioStreamSample) -> AudioStreamSample: # !!! + if audio.loop_begin >= audio.loop_end: # !!! + return audio # !!! + var looped_samples = audio.data.subarray(audio.loop_begin * BYTES_PER_SAMPLE, -1) # !!! + var loop_len = len(looped_samples) # !!! + var target_len = (audio.mix_rate * HACK_EXTEND_LOOP_SAMPLE_EXTRA_MS / 1000) * BYTES_PER_SAMPLE # !!! + while loop_len < target_len: # Keep doubling in length until it's long enough !!! + looped_samples += looped_samples # !!! + loop_len = len(looped_samples) # !!! + var output = audio.duplicate(true) # !!! + output.data = audio.data + looped_samples # !!! + return output # !!! +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + var bgm_tracks = [] var instrument_samples = [] +var instrument_samples_HACK_EXTENDED_LOOPS = [] var sfx_samples = [] func read_rom_address(buffer: StreamPeerBuffer) -> int: @@ -119,7 +141,7 @@ func get_inst_sample_data(snes_data: Dictionary, buffer: StreamPeerBuffer, id: i var audio := make_sample(buffer, size, sample_rate) audio.loop_mode = AudioStreamSample.LOOP_FORWARD audio.loop_begin = (loop_start_packet * 16) + silent_samples # Each 9byte packet is 16 samples - audio.loop_end = silent_samples + num_samples - 1 + audio.loop_end = silent_samples + num_samples # print_debug('Loaded instrument #%02X with lookup offset $%06X, BRR data offset $%06X, length $%04X (%f packets, %d samples) and loop point %d samples' % [id, lookup_offset, brr_offset, size, size/9.0, num_samples, audio.loop_begin]) return audio @@ -139,7 +161,7 @@ func load_sfx_samples_data(snes_data: Dictionary, buffer: StreamPeerBuffer): var loop_start_packet: int = brr_spc_loop_addrs[i] - brr_spc_addrs[i] audio.loop_mode = AudioStreamSample.LOOP_FORWARD audio.loop_begin = (loop_start_packet * 16) + silent_samples # Each 9byte packet is 16 samples - audio.loop_end = (len(audio.data)/2) - 1 + audio.loop_end = (len(audio.data)/2) sfx_samples.append(audio) # Use 900 as a limit, it won't be hit, parser stops after End packet anyway emit_signal('audio_sfx_sample_loaded', i) # print('size of %d samples' % sfx_samples[i].data.size()) @@ -148,9 +170,12 @@ func load_sfx_samples_data(snes_data: Dictionary, buffer: StreamPeerBuffer): # Called when the node enters the scene tree for the first time. func load_samples(snes_data: Dictionary, buffer: StreamPeerBuffer): load_sfx_samples_data(snes_data, buffer) - # For some reason, this is a bit slow currently. Might optimize later. + # For some reason, this is a bit slow currently under certain editor conditions. Might optimize later. for i in INST_NUM: instrument_samples.append(get_inst_sample_data(snes_data, buffer, i)) + # Workaround for Godot 3.x quirk where looping samples are interpolated as if they go to nothing instead of looping + instrument_samples_HACK_EXTENDED_LOOPS.append(HACK_EXTEND_LOOP_SAMPLE(instrument_samples[i])) + print('Instrument %02X has mix_rate %d Hz'%[i, instrument_samples[i].mix_rate]) emit_signal('audio_inst_sample_loaded', i) @@ -189,9 +214,13 @@ func load_bgms(buffer: StreamPeerBuffer): var player := AudioStreamPlayer.new() # Make one for each channel, later +var HACK_EXTEND_LOOP_SAMPLE_playback: bool = false func play_sample(id: int): print('Playing inst sample #%02X' % id) - player.stream = instrument_samples[id] + if HACK_EXTEND_LOOP_SAMPLE_playback: + player.stream = instrument_samples_HACK_EXTENDED_LOOPS[id] + else: + player.stream = instrument_samples[id] player.play(PLAY_START) func play_sfx(id: int): diff --git a/test/audio_system.gd b/test/audio_system.gd index f6fd5c9..0a08d05 100644 --- a/test/audio_system.gd +++ b/test/audio_system.gd @@ -84,7 +84,10 @@ func play_bgm(id: int) -> void: if inst_idx < 0: self.inst_sample_map[i + 0x20] = null else: - self.inst_sample_map[i + 0x20] = SoundLoader.instrument_samples[inst_idx] + if SoundLoader.HACK_EXTEND_LOOP_SAMPLE_playback: + self.inst_sample_map[i + 0x20] = SoundLoader.instrument_samples_HACK_EXTENDED_LOOPS[inst_idx] + 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) @@ -109,11 +112,16 @@ func _stop_all() -> void: self.music_player = null SoundLoader.player.stop() +func _update_loop_hack_status(enabled: bool) -> void: + SoundLoader.HACK_EXTEND_LOOP_SAMPLE_playback = enabled + # Called when the node enters the scene tree for the first time. func _ready() -> void: self._create_sfx_buttons() self._create_bgm_playback() $btn_stop.connect('pressed', self, '_stop_all') + $btn_hack_loop_extension.connect('toggled', self, '_update_loop_hack_status') + $btn_hack_loop_extension.text += ' (%dms)'%SoundLoader.HACK_EXTEND_LOOP_SAMPLE_EXTRA_MS 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]) diff --git a/test/audio_system.tscn b/test/audio_system.tscn index 30876b7..c562c74 100644 --- a/test/audio_system.tscn +++ b/test/audio_system.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=2 format=2] +[gd_scene load_steps=3 format=2] [ext_resource path="res://test/audio_system.gd" type="Script" id=1] +[ext_resource path="res://theme/menu_theme.tres" type="Theme" id=2] [node name="audio_system" type="Node2D"] script = ExtResource( 1 ) @@ -16,8 +17,8 @@ margin_bottom = 176.0 [node name="sb_bgm" type="SpinBox" parent="."] margin_top = 192.0 -margin_right = 23.0 -margin_bottom = 214.0 +margin_right = 38.0 +margin_bottom = 216.0 rect_min_size = Vector2( 38, 0 ) align = 2 @@ -25,12 +26,20 @@ align = 2 margin_left = 40.0 margin_top = 192.0 margin_right = 102.0 -margin_bottom = 214.0 +margin_bottom = 216.0 text = "Play BGM" [node name="btn_stop" type="Button" parent="."] -margin_left = 312.0 +margin_left = 320.0 margin_top = 192.0 margin_right = 374.0 -margin_bottom = 214.0 +margin_bottom = 216.0 text = "Stop All" + +[node name="btn_hack_loop_extension" type="CheckBox" parent="."] +margin_left = 105.0 +margin_top = 192.0 +margin_right = 315.0 +margin_bottom = 216.0 +theme = ExtResource( 2 ) +text = "HACK: Loop Extension" diff --git a/theme/checkbox_filled.png b/theme/checkbox_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..b33165630ed2dd6b0f6963a6cdc1c4920dce3cc9 GIT binary patch literal 155 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6l001;Lp08>o#@DSK!Jm${aF3s zeRt$~){6E{FJda+bmXXl)@`oMD;`KkI%IAV+w{2O(#aWT1@Fn+lGwBQ%bo+KQP1)i zYHrPMi2ci<;W|a;2-m+S0`nKJ9SHQ=RR6U?$9nE>w$Do?RlHpiy@0kcc)I$ztaD0e F0su6IJIDY4 literal 0 HcmV?d00001 diff --git a/theme/checkbox_filled.png.import b/theme/checkbox_filled.png.import new file mode 100644 index 0000000..bb4ccd3 --- /dev/null +++ b/theme/checkbox_filled.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/checkbox_filled.png-3781a04311202b1a451f80288a9e4cf3.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://theme/checkbox_filled.png" +dest_files=[ "res://.import/checkbox_filled.png-3781a04311202b1a451f80288a9e4cf3.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=false +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=0 +process/fix_alpha_border=false +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=false +svg/scale=1.0 diff --git a/theme/menu_theme.tres b/theme/menu_theme.tres index ca993c8..dd31387 100644 --- a/theme/menu_theme.tres +++ b/theme/menu_theme.tres @@ -1,4 +1,4 @@ -[gd_resource type="Theme" load_steps=22 format=2] +[gd_resource type="Theme" load_steps=25 format=2] [ext_resource path="res://3rd_party/sysfont/dynamicfont.tres" type="DynamicFont" id=1] [ext_resource path="res://theme/border_stylebox.tres" type="StyleBox" id=2] @@ -9,6 +9,9 @@ [ext_resource path="res://theme/vslider10px_inner_stylebox.tres" type="StyleBox" id=7] [ext_resource path="res://theme/vslider10px_stylebox.tres" type="StyleBox" id=8] [ext_resource path="res://theme/ThemeElements.png" type="Texture" id=9] +[ext_resource path="res://theme/border_imagetexture.tres" type="Texture" id=10] +[ext_resource path="res://theme/checkbox_filled.png" type="Texture" id=11] +[ext_resource path="res://theme/border_imagetexture_black.tres" type="Texture" id=12] [sub_resource type="StyleBoxTexture" id=10] texture = ExtResource( 9 ) @@ -58,6 +61,15 @@ Button/styles/disabled = ExtResource( 2 ) Button/styles/hover = ExtResource( 2 ) Button/styles/normal = ExtResource( 2 ) Button/styles/pressed = ExtResource( 2 ) +CheckBox/icons/checked = ExtResource( 11 ) +CheckBox/icons/checked_disabled = ExtResource( 11 ) +CheckBox/icons/radio_checked = ExtResource( 11 ) +CheckBox/icons/radio_checked_disabled = ExtResource( 11 ) +CheckBox/icons/radio_unchecked = ExtResource( 10 ) +CheckBox/icons/radio_unchecked_disabled = ExtResource( 12 ) +CheckBox/icons/unchecked = ExtResource( 10 ) +CheckBox/icons/unchecked_disabled = ExtResource( 12 ) +CheckBox/styles/hover_pressed = ExtResource( 2 ) HScrollBar/styles/grabber = SubResource( 10 ) HScrollBar/styles/grabber_highlight = SubResource( 10 ) HScrollBar/styles/grabber_pressed = SubResource( 10 )