feat: add selectable animalese voice sets
This commit is contained in:
parent
364e7dc282
commit
ce733818e2
247 changed files with 2675 additions and 302 deletions
|
|
@ -1,8 +1,7 @@
|
|||
class_name AnimaleseVoice
|
||||
extends Node
|
||||
|
||||
const SAMPLE_DIRECTORY := "res://sound/dialogue/animalese/placeholder"
|
||||
const SUPPORTED_CHARACTERS := "abcdefghijklmnopqrstuvwxyz"
|
||||
const SUPPORTED_CHARACTERS := "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
const DEFAULT_CHARACTERS_PER_SECOND: float = 28.0
|
||||
const POLYPHONY: int = 8
|
||||
const VoiceProfilesType = preload(
|
||||
|
|
@ -13,7 +12,7 @@ const TypewriterRevealType = preload("res://ui/typewriter_reveal.gd")
|
|||
var base_pitch: float = 1.0
|
||||
var volume_db: float = -13.0
|
||||
|
||||
var _samples: Dictionary[String, AudioStream] = {}
|
||||
var _sample_sets: Dictionary[String, Dictionary] = {}
|
||||
var _player: AudioStreamPlayer
|
||||
var _playback: AudioStreamPlaybackPolyphonic
|
||||
|
||||
|
|
@ -33,12 +32,20 @@ func _ready() -> void:
|
|||
)
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if _player != null:
|
||||
_player.stop()
|
||||
_player.stream = null
|
||||
_playback = null
|
||||
|
||||
|
||||
func speak_text(
|
||||
tween_owner: Node,
|
||||
text: String,
|
||||
voice_key: String,
|
||||
voice_profile_id: String = VoiceProfilesType.DEFAULT_ID,
|
||||
characters_per_second: float = -1.0,
|
||||
sample_set_id: String = VoiceProfilesType.DEFAULT_SAMPLE_SET_ID,
|
||||
) -> Tween:
|
||||
var speech_tween := tween_owner.create_tween()
|
||||
var resolved_characters_per_second := (
|
||||
|
|
@ -53,6 +60,9 @@ func speak_text(
|
|||
var voice_pitch := (
|
||||
base_pitch * VoiceProfilesType.pitch_for(voice_profile_id)
|
||||
)
|
||||
var resolved_sample_set_id := (
|
||||
VoiceProfilesType.sanitized_sample_set_id(sample_set_id)
|
||||
)
|
||||
for character_index: int in range(text.length()):
|
||||
speech_tween.tween_interval(character_seconds)
|
||||
speech_tween.tween_callback(
|
||||
|
|
@ -62,23 +72,33 @@ func speak_text(
|
|||
text,
|
||||
voice_key,
|
||||
voice_pitch,
|
||||
resolved_sample_set_id,
|
||||
)
|
||||
)
|
||||
return speech_tween
|
||||
|
||||
|
||||
func _load_samples() -> void:
|
||||
for character_index: int in range(SUPPORTED_CHARACTERS.length()):
|
||||
var character := SUPPORTED_CHARACTERS.substr(character_index, 1)
|
||||
_load_sample(character)
|
||||
_load_sample("fallback")
|
||||
for option: Dictionary in VoiceProfilesType.SAMPLE_SET_OPTIONS:
|
||||
var sample_set_id := str(option.get("id", ""))
|
||||
var sample_directory := str(option.get("directory", ""))
|
||||
var samples: Dictionary[String, AudioStream] = {}
|
||||
for character_index: int in range(SUPPORTED_CHARACTERS.length()):
|
||||
var character := SUPPORTED_CHARACTERS.substr(character_index, 1)
|
||||
var sample := _load_sample(sample_directory, character)
|
||||
if sample != null:
|
||||
samples[character] = sample
|
||||
var fallback := _load_sample(sample_directory, "fallback")
|
||||
if fallback != null:
|
||||
samples["fallback"] = fallback
|
||||
_sample_sets[sample_set_id] = samples
|
||||
|
||||
|
||||
func _load_sample(sample_name: String) -> void:
|
||||
var sample_path := "%s/%s.wav" % [SAMPLE_DIRECTORY, sample_name]
|
||||
var sample := load(sample_path) as AudioStream
|
||||
if sample != null:
|
||||
_samples[sample_name] = sample
|
||||
func _load_sample(sample_directory: String, sample_name: String) -> AudioStream:
|
||||
var sample_path := "%s/%s.wav" % [sample_directory, sample_name]
|
||||
if not ResourceLoader.exists(sample_path, "AudioStreamWAV"):
|
||||
return null
|
||||
return load(sample_path) as AudioStream
|
||||
|
||||
|
||||
func _play_character(
|
||||
|
|
@ -87,16 +107,17 @@ func _play_character(
|
|||
full_text: String,
|
||||
voice_key: String,
|
||||
voice_pitch: float,
|
||||
sample_set_id: String,
|
||||
) -> void:
|
||||
if _playback == null or character.strip_edges().is_empty():
|
||||
return
|
||||
var normalized := character.to_lower()
|
||||
if normalized in [".", ",", "!", "?", ":", ";", "-", "_"]:
|
||||
return
|
||||
var sample_name := (
|
||||
normalized if SUPPORTED_CHARACTERS.contains(normalized) else "fallback"
|
||||
)
|
||||
var sample: AudioStream = _samples.get(sample_name)
|
||||
var samples: Dictionary = _sample_sets.get(sample_set_id, {})
|
||||
var sample := samples.get(normalized) as AudioStream
|
||||
if sample == null:
|
||||
sample = samples.get("fallback") as AudioStream
|
||||
if sample == null:
|
||||
return
|
||||
var speaker_variation := (
|
||||
|
|
|
|||
|
|
@ -836,7 +836,11 @@ func _send() -> void:
|
|||
_send_pending = true
|
||||
_pending_send_body = NetworkChatProtocol.sanitize_body(body)
|
||||
_entry.editable = false
|
||||
if not _service.send_local_message(body):
|
||||
if not _service.send_local_message(
|
||||
body,
|
||||
_player.get_animalese_voice_id(),
|
||||
_player.get_animalese_sample_set_id(),
|
||||
):
|
||||
_send_pending = false
|
||||
_pending_send_body = ""
|
||||
_entry.editable = true
|
||||
|
|
@ -939,6 +943,12 @@ func _on_message(message: Dictionary) -> void:
|
|||
peer_id,
|
||||
str(message["body"]),
|
||||
str(message.get("sender_fingerprint", "")),
|
||||
VoiceProfilesType.sanitized_id(str(message.get(
|
||||
"voice_id", VoiceProfilesType.DEFAULT_ID
|
||||
))),
|
||||
VoiceProfilesType.sanitized_sample_set_id(str(message.get(
|
||||
"sample_set_id", VoiceProfilesType.DEFAULT_SAMPLE_SET_ID
|
||||
))),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -952,6 +962,8 @@ func _show_speech_bubble(
|
|||
peer_id: int,
|
||||
body: String,
|
||||
fingerprint: String,
|
||||
requested_voice_profile_id: String = "",
|
||||
requested_sample_set_id: String = "",
|
||||
) -> void:
|
||||
_on_peer_removed(peer_id)
|
||||
var bubble := PanelContainer.new()
|
||||
|
|
@ -978,16 +990,31 @@ func _show_speech_bubble(
|
|||
_speech_layer.add_child(bubble)
|
||||
var reveal_seconds := TypewriterRevealType.start(label)
|
||||
var voice_profile_id: String = VoiceProfilesType.DEFAULT_ID
|
||||
var sample_set_id: String = VoiceProfilesType.DEFAULT_SAMPLE_SET_ID
|
||||
var speaker_avatar := _spawn.get_avatar(peer_id)
|
||||
if speaker_avatar != null:
|
||||
if not requested_voice_profile_id.is_empty():
|
||||
voice_profile_id = VoiceProfilesType.sanitized_id(
|
||||
requested_voice_profile_id
|
||||
)
|
||||
elif speaker_avatar != null:
|
||||
voice_profile_id = VoiceProfilesType.sanitized_id(
|
||||
speaker_avatar.get_animalese_voice_id()
|
||||
)
|
||||
if not requested_sample_set_id.is_empty():
|
||||
sample_set_id = VoiceProfilesType.sanitized_sample_set_id(
|
||||
requested_sample_set_id
|
||||
)
|
||||
elif speaker_avatar != null:
|
||||
sample_set_id = VoiceProfilesType.sanitized_sample_set_id(
|
||||
speaker_avatar.get_animalese_sample_set_id()
|
||||
)
|
||||
_animalese_voice.speak_text(
|
||||
label,
|
||||
label.text,
|
||||
str(peer_id),
|
||||
voice_profile_id,
|
||||
-1.0,
|
||||
sample_set_id,
|
||||
)
|
||||
_speech[peer_id] = {
|
||||
"bubble": bubble,
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ var _draft_speech_speed_id: String = VoiceProfilesType.DEFAULT_SPEED_ID
|
|||
var _persisted_speech_speed_id: String = VoiceProfilesType.DEFAULT_SPEED_ID
|
||||
var _draft_call_id: String = VoiceProfilesType.DEFAULT_CALL_ID
|
||||
var _persisted_call_id: String = VoiceProfilesType.DEFAULT_CALL_ID
|
||||
var _draft_sample_set_id: String = VoiceProfilesType.DEFAULT_SAMPLE_SET_ID
|
||||
var _persisted_sample_set_id: String = VoiceProfilesType.DEFAULT_SAMPLE_SET_ID
|
||||
var _category_id: String = "species"
|
||||
var _dirty: bool = false
|
||||
var _allow_duplicate: bool = false
|
||||
|
|
@ -848,7 +850,10 @@ func _category_label(category_id: String) -> String:
|
|||
|
||||
func _build_voice_options() -> void:
|
||||
var description := Label.new()
|
||||
description.text = "Choose how your character sounds in chat."
|
||||
description.text = (
|
||||
"Choose how your character sounds. "
|
||||
+ "Playback speed affects this device only."
|
||||
)
|
||||
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
description.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
description.add_theme_font_size_override("font_size", 12)
|
||||
|
|
@ -856,48 +861,97 @@ func _build_voice_options() -> void:
|
|||
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
||||
)
|
||||
_option_list.add_child(description)
|
||||
var settings_grid := GridContainer.new()
|
||||
settings_grid.name = "VoiceSettingsGrid"
|
||||
settings_grid.columns = 2
|
||||
settings_grid.add_theme_constant_override("h_separation", 10)
|
||||
settings_grid.add_theme_constant_override("v_separation", 8)
|
||||
settings_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_option_list.add_child(settings_grid)
|
||||
var sample_set_title := Label.new()
|
||||
sample_set_title.text = "voice set"
|
||||
sample_set_title.custom_minimum_size.x = 92.0
|
||||
sample_set_title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
sample_set_title.add_theme_font_size_override("font_size", 13)
|
||||
sample_set_title.add_theme_color_override(
|
||||
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
||||
)
|
||||
settings_grid.add_child(sample_set_title)
|
||||
var sample_set_grid := GridContainer.new()
|
||||
sample_set_grid.name = "VoiceSampleSetGrid"
|
||||
sample_set_grid.columns = mini(
|
||||
3,
|
||||
VoiceProfilesType.SAMPLE_SET_OPTIONS.size(),
|
||||
)
|
||||
sample_set_grid.add_theme_constant_override("h_separation", 8)
|
||||
sample_set_grid.add_theme_constant_override("v_separation", 4)
|
||||
sample_set_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
settings_grid.add_child(sample_set_grid)
|
||||
var sample_set_group := ButtonGroup.new()
|
||||
sample_set_group.allow_unpress = false
|
||||
for option: Dictionary in VoiceProfilesType.SAMPLE_SET_OPTIONS:
|
||||
var sample_set_id := str(option.get("id", ""))
|
||||
var sample_set_button := Button.new()
|
||||
sample_set_button.name = "VoiceSet_%s" % sample_set_id
|
||||
sample_set_button.text = str(option.get("label", sample_set_id))
|
||||
sample_set_button.toggle_mode = true
|
||||
sample_set_button.button_group = sample_set_group
|
||||
sample_set_button.custom_minimum_size = Vector2(108.0, 32.0)
|
||||
sample_set_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
sample_set_button.button_pressed = (
|
||||
_draft_sample_set_id == sample_set_id
|
||||
)
|
||||
sample_set_button.pressed.connect(
|
||||
_select_sample_set_option.bind(sample_set_id)
|
||||
)
|
||||
UtilityPageStyle.apply_compact_ocean_button(sample_set_button)
|
||||
sample_set_grid.add_child(sample_set_button)
|
||||
var pitch_title := Label.new()
|
||||
pitch_title.text = "pitch"
|
||||
pitch_title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
pitch_title.add_theme_font_size_override("font_size", 13)
|
||||
pitch_title.add_theme_color_override(
|
||||
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
||||
)
|
||||
settings_grid.add_child(pitch_title)
|
||||
var grid := GridContainer.new()
|
||||
grid.name = "VoicePitchGrid"
|
||||
grid.columns = 3
|
||||
grid.add_theme_constant_override("h_separation", 6)
|
||||
grid.add_theme_constant_override("v_separation", 4)
|
||||
grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_option_list.add_child(grid)
|
||||
settings_grid.add_child(grid)
|
||||
var pitch_group := ButtonGroup.new()
|
||||
pitch_group.allow_unpress = false
|
||||
for option: Dictionary in VoiceProfilesType.OPTIONS:
|
||||
var option_id := str(option.get("id", ""))
|
||||
var button := Button.new()
|
||||
button.text = str(option.get("label", option_id))
|
||||
button.toggle_mode = true
|
||||
button.button_group = pitch_group
|
||||
button.custom_minimum_size = Vector2(108.0, 32.0)
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button.button_pressed = _draft_voice_id == option_id
|
||||
button.pressed.connect(_select_voice_option.bind(option_id))
|
||||
UtilityPageStyle.apply_compact_ocean_button(button)
|
||||
grid.add_child(button)
|
||||
var divider := HSeparator.new()
|
||||
divider.custom_minimum_size.y = 2.0
|
||||
_option_list.add_child(divider)
|
||||
var speed_title := Label.new()
|
||||
speed_title.text = "speech speed"
|
||||
speed_title.text = "playback speed"
|
||||
speed_title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
speed_title.add_theme_font_size_override("font_size", 13)
|
||||
speed_title.add_theme_color_override(
|
||||
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
||||
)
|
||||
_option_list.add_child(speed_title)
|
||||
var speed_description := Label.new()
|
||||
speed_description.text = "Controls chat speech on this device only."
|
||||
speed_description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
speed_description.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
speed_description.add_theme_font_size_override("font_size", 12)
|
||||
speed_description.add_theme_color_override(
|
||||
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
||||
)
|
||||
_option_list.add_child(speed_description)
|
||||
settings_grid.add_child(speed_title)
|
||||
var speed_grid := GridContainer.new()
|
||||
speed_grid.name = "VoiceSpeedGrid"
|
||||
speed_grid.columns = 3
|
||||
speed_grid.add_theme_constant_override("h_separation", 6)
|
||||
speed_grid.add_theme_constant_override("v_separation", 4)
|
||||
speed_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_option_list.add_child(speed_grid)
|
||||
settings_grid.add_child(speed_grid)
|
||||
var speed_group := ButtonGroup.new()
|
||||
speed_group.allow_unpress = false
|
||||
for option: Dictionary in VoiceProfilesType.SPEED_OPTIONS:
|
||||
var speed_id := str(option.get("id", ""))
|
||||
var speed_button := Button.new()
|
||||
|
|
@ -906,6 +960,7 @@ func _build_voice_options() -> void:
|
|||
float(option.get("characters_per_second", 28.0))
|
||||
)
|
||||
speed_button.toggle_mode = true
|
||||
speed_button.button_group = speed_group
|
||||
speed_button.custom_minimum_size = Vector2(108.0, 32.0)
|
||||
speed_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
speed_button.button_pressed = _draft_speech_speed_id == speed_id
|
||||
|
|
@ -914,35 +969,29 @@ func _build_voice_options() -> void:
|
|||
)
|
||||
UtilityPageStyle.apply_compact_ocean_button(speed_button)
|
||||
speed_grid.add_child(speed_button)
|
||||
var call_divider := HSeparator.new()
|
||||
call_divider.custom_minimum_size.y = 2.0
|
||||
_option_list.add_child(call_divider)
|
||||
var call_title := Label.new()
|
||||
call_title.text = "call"
|
||||
call_title.text = "call (G)"
|
||||
call_title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
call_title.add_theme_font_size_override("font_size", 13)
|
||||
call_title.add_theme_color_override(
|
||||
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
||||
)
|
||||
_option_list.add_child(call_title)
|
||||
var call_description := Label.new()
|
||||
call_description.text = "Press G to make this sound."
|
||||
call_description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
call_description.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
call_description.add_theme_font_size_override("font_size", 12)
|
||||
call_description.add_theme_color_override(
|
||||
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
||||
)
|
||||
_option_list.add_child(call_description)
|
||||
settings_grid.add_child(call_title)
|
||||
var call_grid := GridContainer.new()
|
||||
call_grid.name = "VoiceCallGrid"
|
||||
call_grid.columns = 2
|
||||
call_grid.add_theme_constant_override("h_separation", 8)
|
||||
call_grid.add_theme_constant_override("v_separation", 8)
|
||||
_option_list.add_child(call_grid)
|
||||
call_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
settings_grid.add_child(call_grid)
|
||||
var call_group := ButtonGroup.new()
|
||||
call_group.allow_unpress = false
|
||||
for option: Dictionary in VoiceProfilesType.CALL_OPTIONS:
|
||||
var call_id := str(option.get("id", ""))
|
||||
var call_button := Button.new()
|
||||
call_button.text = str(option.get("label", call_id))
|
||||
call_button.toggle_mode = true
|
||||
call_button.button_group = call_group
|
||||
call_button.custom_minimum_size = Vector2(108.0, 32.0)
|
||||
call_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
call_button.button_pressed = _draft_call_id == call_id
|
||||
|
|
@ -951,12 +1000,20 @@ func _build_voice_options() -> void:
|
|||
call_grid.add_child(call_button)
|
||||
|
||||
|
||||
func _select_sample_set_option(sample_set_id: String) -> void:
|
||||
if not VoiceProfilesType.is_valid_sample_set(sample_set_id):
|
||||
return
|
||||
_draft_sample_set_id = sample_set_id
|
||||
_dirty = _draft_differs()
|
||||
_refresh_actions()
|
||||
_play_voice_preview()
|
||||
|
||||
|
||||
func _select_voice_option(voice_id: String) -> void:
|
||||
if not VoiceProfilesType.is_valid(voice_id):
|
||||
return
|
||||
_draft_voice_id = voice_id
|
||||
_dirty = _draft_differs()
|
||||
_refresh_options()
|
||||
_refresh_actions()
|
||||
_play_voice_preview()
|
||||
|
||||
|
|
@ -966,7 +1023,6 @@ func _select_speech_speed_option(speed_id: String) -> void:
|
|||
return
|
||||
_draft_speech_speed_id = speed_id
|
||||
_dirty = _draft_differs()
|
||||
_refresh_options()
|
||||
_refresh_actions()
|
||||
_play_voice_preview()
|
||||
|
||||
|
|
@ -976,7 +1032,6 @@ func _select_call_option(call_id: String) -> void:
|
|||
return
|
||||
_draft_call_id = call_id
|
||||
_dirty = _draft_differs()
|
||||
_refresh_options()
|
||||
_refresh_actions()
|
||||
|
||||
|
||||
|
|
@ -993,6 +1048,7 @@ func _play_voice_preview() -> void:
|
|||
"profile-preview",
|
||||
_draft_voice_id,
|
||||
VoiceProfilesType.speed_for(_draft_speech_speed_id),
|
||||
_draft_sample_set_id,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -2025,6 +2081,7 @@ func _apply() -> void:
|
|||
_draft_voice_id,
|
||||
_draft_speech_speed_id,
|
||||
_draft_call_id,
|
||||
_draft_sample_set_id,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -2045,11 +2102,13 @@ func _load_persisted() -> void:
|
|||
_service.get_persisted_speech_speed_id()
|
||||
)
|
||||
_persisted_call_id = _service.get_persisted_call_id()
|
||||
_persisted_sample_set_id = _service.get_persisted_sample_set_id()
|
||||
_draft_name = _persisted_name
|
||||
_draft_appearance = _persisted_appearance.duplicate(true)
|
||||
_draft_voice_id = _persisted_voice_id
|
||||
_draft_speech_speed_id = _persisted_speech_speed_id
|
||||
_draft_call_id = _persisted_call_id
|
||||
_draft_sample_set_id = _persisted_sample_set_id
|
||||
TypewriterRevealType.set_characters_per_second(
|
||||
VoiceProfilesType.speed_for(_persisted_speech_speed_id)
|
||||
)
|
||||
|
|
@ -2101,6 +2160,7 @@ func _confirm_pending_action() -> void:
|
|||
_draft_voice_id = VoiceProfilesType.DEFAULT_ID
|
||||
_draft_speech_speed_id = VoiceProfilesType.DEFAULT_SPEED_ID
|
||||
_draft_call_id = VoiceProfilesType.DEFAULT_CALL_ID
|
||||
_draft_sample_set_id = VoiceProfilesType.DEFAULT_SAMPLE_SET_ID
|
||||
_preview.apply_appearance_profile(_draft_appearance)
|
||||
_queue_draft_appearance_preview()
|
||||
_dirty = _draft_differs()
|
||||
|
|
@ -2135,6 +2195,7 @@ func _draft_differs() -> bool:
|
|||
or _draft_voice_id != _persisted_voice_id
|
||||
or _draft_speech_speed_id != _persisted_speech_speed_id
|
||||
or _draft_call_id != _persisted_call_id
|
||||
or _draft_sample_set_id != _persisted_sample_set_id
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ text = "additional audio"
|
|||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.624, 0.812, 0.824, 1)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
text = "tosha73 • fishing reel sounds\nkkenny101 • ocean ambience\nqubodup • water impact\nAyton • rain ambience\nivolipa • dog bark\nChristyboy100 • cat meow"
|
||||
text = "kat • animalese voice sample\nkim • animalese voice sample\ntosha73 • fishing reel sounds\nkkenny101 • ocean ambience\nqubodup • water impact\nAyton • rain ambience\nivolipa • dog bark\nChristyboy100 • cat meow"
|
||||
|
||||
[node name="TechnologySpacer" type="Control" parent="Paper/Margin/Layout/Columns/AdditionalCredits"]
|
||||
custom_minimum_size = Vector2(0, 9)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue