Implement audio management system with effects and music players; add settings for sound and music volume

This commit is contained in:
Kirill
2026-10-03 13:21:53 +03:00
parent b6c683298b
commit 9e13c8bbe3
14 changed files with 334 additions and 0 deletions
+9
View File
@@ -9,6 +9,9 @@ var save_plaintext_copy: bool = true
var Settings: Dictionary = {
"filename": "save-0001",
"sound_volume": 1.0,
"music_volume": 1.0,
"sound_enabled": true,
"music_enabled": true,
"language": "en",
}
@@ -23,6 +26,9 @@ var Templates: Dictionary = {}
var LifeRules: Dictionary = {}
var CivilizationRules: Dictionary = {}
var Music: Dictionary = {}
var Effects: Dictionary = {}
var batch_count: float = 40.0 # must be even not odd
@@ -70,6 +76,9 @@ func _ready() -> void:
Templates = OwniverseTemplateLoader.get_data()
CivilizationRules = OwniverseCivilizationRuleLoader.get_data()
Music = OwniverseAudioLoader.get_music()
Effects = OwniverseAudioLoader.get_effects()
func _read_folder_list(folder_name: String) -> void:
var dir := DirAccess.open(folder_name)
+40
View File
@@ -0,0 +1,40 @@
class_name OwniverseAudioLoader
const MUSIC: Dictionary = {
"apocalypse_the_end": preload("res://sound/music/apocalypse_the_end.ogg"),
"cosmic_unicorns": preload("res://sound/music/cosmic_unicorns.ogg"),
"funky_eldritch": preload("res://sound/music/funky_eldritch.ogg"),
"meditative_beings": preload("res://sound/music/meditative_beings.ogg"),
"owniverse_main_theme": preload("res://sound/music/owniverse_main_theme.ogg"),
}
const EFFECTS: Dictionary = {
"black_hole_feed": preload("res://sound/effects/black_hole_feed.ogg"),
"blast": preload("res://sound/effects/blast.ogg"),
"civ_birth": preload("res://sound/effects/civ_birth.ogg"),
"civ_death": preload("res://sound/effects/civ_death.ogg"),
"civ_top": preload("res://sound/effects/civ_top.ogg"),
"cloud_decay": preload("res://sound/effects/cloud_decay.ogg"),
"create": preload("res://sound/effects/create.ogg"),
"force": preload("res://sound/effects/force.ogg"),
"gamma_ray": preload("res://sound/effects/gamma_ray.ogg"),
"life_birth": preload("res://sound/effects/life_birth.ogg"),
"life_death": preload("res://sound/effects/life_death.ogg"),
"select": preload("res://sound/effects/select.ogg"),
"star_birth": preload("res://sound/effects/star_birth.ogg"),
"structure_decay": preload("res://sound/effects/structure_decay.ogg"),
"swipe": preload("res://sound/effects/swipe.ogg"),
"time_minus": preload("res://sound/effects/time_minus.ogg"),
"time_plus": preload("res://sound/effects/time_plus.ogg"),
"trend_action": preload("res://sound/effects/trend_action.ogg"),
"window_close": preload("res://sound/effects/window_close.ogg"),
"window_open": preload("res://sound/effects/window_open.ogg"),
}
static func get_music() -> Dictionary:
return MUSIC.duplicate()
static func get_effects() -> Dictionary:
return EFFECTS.duplicate()
+1
View File
@@ -0,0 +1 @@
uid://cb517ydjlk7iq
+3
View File
@@ -64,6 +64,7 @@ const SWIPE_DURATION: float = 0.4
@onready var g_time_minus: TextureButton = $World/TopMenu/TimeMinus
@onready var g_time_plus: TextureButton = $World/TopMenu/TimePlus
@onready var effects_player: OwniverseEffectsPlayer = $"../FXPlayer"
@onready var screens: Dictionary = {
"World": $World,
@@ -263,12 +264,14 @@ func _swipe_to_nearest_page() -> void:
func _input(event: InputEvent) -> void:
if event is InputEventScreenDrag:
#effects_player.play_effect("swipe")
stage_page_scroller.scroll_horizontal = clamp(
stage_page_scroller.scroll_horizontal - PAGE_COUNT * event.screen_relative.x,
0, PAGE_WIDTH * (PAGE_COUNT - 1))
g_counters.swipe.is_active = true
g_counters.swipe.value = 0
elif event is InputEventPanGesture:
#effects_player.play_effect("swipe")
g_counters.swipe.is_active = true
g_counters.swipe.value = 0
elif event is InputEventScreenTouch:
+47
View File
@@ -0,0 +1,47 @@
extends SceneTree
var checks: int = 0
var failures: int = 0
func _initialize() -> void:
call_deferred("run")
func check(condition: bool, message: String) -> void:
checks += 1
if not condition:
failures += 1
push_error(message)
func run() -> void:
var player := OwniverseEffectsPlayer.new()
root.add_child(player)
check(player.play_effect("select"), "Known effect starts")
check(player.stream == player._effects.select and player.playing, "Selected effect is playing")
var selected_stream: AudioStream = player.stream
check(not player.play_effect("select"), "Playing effect is not restarted")
check(player.stream == selected_stream and player.playing, "Rejected duplicate keeps current playback")
player.stop()
check(not player.play_effect("select"), "Named effect is rejected during its two-second cooldown")
check(player.play_effect("blast"), "Different effect starts without waiting for another name")
player.stop()
player._last_played_msec.select -= player.EFFECT_COOLDOWN_MSEC
check(player.play_effect("select"), "Named effect starts after its cooldown")
check(not player.play_effect("missing_effect"), "Unknown effect is rejected")
check(player.stream == player._effects.select, "Unknown effect does not interrupt playback")
player.set_enabled(false)
check(player.stream_paused, "Disabling effects pauses current playback")
check(not player.play_effect("blast") and player.stream == player._effects.select, "Disabled player rejects new effects")
player.set_enabled(true)
check(not player.stream_paused, "Enabling effects resumes current playback")
player.stop()
player.stream = null
player.free()
await process_frame
print("Effects player: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
+1
View File
@@ -0,0 +1 @@
uid://cj2kk1vx5umtc
+54
View File
@@ -0,0 +1,54 @@
extends SceneTree
var checks: int = 0
var failures: int = 0
func _initialize() -> void:
call_deferred("run")
func check(condition: bool, message: String) -> void:
checks += 1
if not condition:
failures += 1
push_error(message)
func run() -> void:
var player := OwniverseMusicPlayer.new()
root.add_child(player)
await process_frame
check(player.last_track == "", "Music player starts with silence")
check(player._pause_timer.time_left >= player.MIN_PAUSE_SECONDS and player._pause_timer.time_left <= player.MAX_PAUSE_SECONDS, "Initial pause is between two and five minutes")
player._pause_timer.stop()
player._play_next()
check(player.last_track != "", "Music player selects an initial track")
check(player.last_track != player.EXCLUDED_TRACK, "Normal playlist excludes apocalypse")
var previous: String = player.last_track
player._play_next()
check(player.last_track != previous, "Consecutive tracks do not repeat")
check(player.last_track != player.EXCLUDED_TRACK, "Subsequent tracks exclude apocalypse")
var last_normal_track: String = player.last_track
player.play_apocalypse()
check(player.stream == player._music[player.EXCLUDED_TRACK] and player.playing, "Apocalypse music starts immediately")
check(player.last_track == last_normal_track, "Apocalypse does not replace the last normal track")
player._schedule_pause()
check(player._pause_timer.time_left >= player.MIN_PAUSE_SECONDS and player._pause_timer.time_left <= player.MAX_PAUSE_SECONDS, "Pause is between two and five minutes")
player.set_enabled(false)
check(player.stream_paused and player._pause_timer.paused, "Disabling music pauses playback and scheduling")
var disabled_stream: AudioStream = player.stream
player.play_apocalypse()
check(player.stream == disabled_stream, "Apocalypse does not start while music is disabled")
player.set_enabled(true)
check(not player.stream_paused and not player._pause_timer.paused, "Enabling music resumes playback and scheduling")
player.stop()
player.stream = null
player.free()
await process_frame
print("Music player: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
+1
View File
@@ -0,0 +1 @@
uid://nue2ifdplt25
+31
View File
@@ -0,0 +1,31 @@
class_name OwniverseEffectsPlayer
extends AudioStreamPlayer
const EFFECT_COOLDOWN_MSEC := 2000
var enabled: bool = true
var _effects: Dictionary = OwniverseAudioLoader.get_effects()
var _last_played_msec: Dictionary = {}
func set_enabled(value: bool) -> void:
enabled = value
stream_paused = not enabled
func play_effect(effect_id: String) -> bool:
if not enabled:
return false
if not _effects.has(effect_id):
push_warning("Unknown sound effect: " + effect_id)
return false
if playing and stream == _effects[effect_id]:
return false
var now: int = Time.get_ticks_msec()
if now - int(_last_played_msec.get(effect_id, -EFFECT_COOLDOWN_MSEC)) < EFFECT_COOLDOWN_MSEC:
return false
stream = _effects[effect_id]
play()
_last_played_msec[effect_id] = now
return true
+1
View File
@@ -0,0 +1 @@
uid://botb0k4qr0hqo
+69
View File
@@ -0,0 +1,69 @@
class_name OwniverseMusicPlayer
extends AudioStreamPlayer
const EXCLUDED_TRACK := "apocalypse_the_end"
const MIN_PAUSE_SECONDS := 120.0
const MAX_PAUSE_SECONDS := 300.0
var enabled: bool = true
var last_track: String = ""
var _pause_timer: Timer
var _rng := RandomNumberGenerator.new()
var _music: Dictionary = OwniverseAudioLoader.get_music()
func _ready() -> void:
_rng.randomize()
_pause_timer = Timer.new()
_pause_timer.one_shot = true
_pause_timer.timeout.connect(_play_next)
add_child(_pause_timer)
finished.connect(_schedule_pause)
if enabled:
_schedule_pause()
func set_enabled(value: bool) -> void:
enabled = value
if not is_node_ready():
return
stream_paused = not enabled
_pause_timer.paused = not enabled
if enabled and not playing and _pause_timer.is_stopped():
_play_next()
func play_apocalypse() -> void:
if not enabled:
return
if not _music.has(EXCLUDED_TRACK):
push_warning("Apocalypse music is unavailable.")
return
_pause_timer.stop()
stream = _music[EXCLUDED_TRACK]
play()
func _play_next() -> void:
if not enabled:
return
var candidates: Array = _music.keys()
candidates.erase(EXCLUDED_TRACK)
candidates.erase(last_track)
if candidates.is_empty():
push_warning("No music tracks available for playback.")
return
var track_id: String = str(candidates[_rng.randi_range(0, candidates.size() - 1)])
last_track = track_id
stream = _music[track_id]
play()
func _schedule_pause() -> void:
if not enabled:
return
_pause_timer.start(_rng.randf_range(MIN_PAUSE_SECONDS, MAX_PAUSE_SECONDS))
+1
View File
@@ -0,0 +1 @@
uid://btsrt6rw3nr04
+66
View File
@@ -1 +1,67 @@
extends VBoxContainer
const MIN_VOLUME_DB := -50.0
const SILENT_VOLUME_DB := -80.0
@onready var sound_button: TextureButton = $Media/Sound
@onready var music_button: TextureButton = $Media/Music
@onready var sound_volume: HSlider = $Volume/SoundVolume
@onready var music_volume: HSlider = $Volume/MusicVolume
@onready var effects_player: OwniverseEffectsPlayer = $"../../FXPlayer"
@onready var music_player: OwniverseMusicPlayer = $"../../MusicPlayer"
func _ready() -> void:
SaveManager.load_settings()
sound_button.set_pressed_no_signal(bool(Global.Settings.sound_enabled))
music_button.set_pressed_no_signal(bool(Global.Settings.music_enabled))
sound_volume.set_value_no_signal(float(Global.Settings.sound_volume) * sound_volume.max_value)
music_volume.set_value_no_signal(float(Global.Settings.music_volume) * music_volume.max_value)
_apply_sound_settings()
_apply_music_settings()
func _on_sound_pressed() -> void:
Global.Settings.sound_enabled = sound_button.button_pressed
_apply_sound_settings()
SaveManager.save_settings()
func _on_music_pressed() -> void:
Global.Settings.music_enabled = music_button.button_pressed
_apply_music_settings()
SaveManager.save_settings()
func _on_sound_volume_value_changed(value: float) -> void:
Global.Settings.sound_volume = value / sound_volume.max_value
_apply_bus_volume("FX", float(Global.Settings.sound_volume))
SaveManager.save_settings()
func _on_music_volume_value_changed(value: float) -> void:
Global.Settings.music_volume = value / music_volume.max_value
_apply_bus_volume("Music", float(Global.Settings.music_volume))
SaveManager.save_settings()
func _apply_sound_settings() -> void:
var bus_index: int = AudioServer.get_bus_index("FX")
AudioServer.set_bus_mute(bus_index, not bool(Global.Settings.sound_enabled))
effects_player.set_enabled(bool(Global.Settings.sound_enabled))
_apply_bus_volume("FX", float(Global.Settings.sound_volume))
func _apply_music_settings() -> void:
music_player.set_enabled(bool(Global.Settings.music_enabled))
_apply_bus_volume("Music", float(Global.Settings.music_volume))
func _apply_bus_volume(bus_name: String, value: float) -> void:
var bus_index: int = AudioServer.get_bus_index(bus_name)
if bus_index < 0:
push_warning("Missing audio bus: " + bus_name)
return
var normalized: float = clampf(value, 0.0, 1.0)
var volume_db: float = SILENT_VOLUME_DB if is_zero_approx(normalized) else lerpf(MIN_VOLUME_DB, 0.0, normalized)
AudioServer.set_bus_volume_db(bus_index, volume_db)
+10
View File
@@ -67,6 +67,8 @@
[ext_resource type="Texture2D" uid="uid://drvvkd724g8hy" path="res://ui/credits/kid_gp_light.png" id="64_rur7c"]
[ext_resource type="Texture2D" uid="uid://b33ji4mpyw4ks" path="res://ui/credits/kid_gp.png" id="65_y1rwn"]
[ext_resource type="Texture2D" uid="uid://dgt52qiyuirso" path="res://ui/indicators/density/warning_border.png" id="66_rur7c"]
[ext_resource type="Script" uid="uid://btsrt6rw3nr04" path="res://ui/audio/owniverse_music_player.gd" id="67_music"]
[ext_resource type="Script" uid="uid://botb0k4qr0hqo" path="res://ui/audio/owniverse_effects_player.gd" id="68_effects"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ifjjt"]
@@ -1116,8 +1118,12 @@ wait_time = 2.0
autostart = true
[node name="FXPlayer" type="AudioStreamPlayer" parent="." unique_id=1306204231]
bus = &"FX"
script = ExtResource("68_effects")
[node name="MusicPlayer" type="AudioStreamPlayer" parent="." unique_id=707734847]
bus = &"Music"
script = ExtResource("67_music")
[connection signal="timeout" from="Background/Timer" to="Background" method="randomize_parallax"]
[connection signal="pressed" from="Stage/World/TopMenu/BurgerButton" to="Stage" method="_on_topmenu_button_pressed" binds= ["MainMenu"]]
@@ -1135,6 +1141,10 @@ autostart = true
[connection signal="pressed" from="Stage/MainMenu/TopMenu/BackButton" to="Stage" method="_on_topmenu_button_pressed" binds= ["World"]]
[connection signal="pressed" from="Stage/MainMenu/CreateOwniverse" to="Stage" method="create_new_universe"]
[connection signal="pressed" from="Stage/MainMenu/Achievements" to="Stage" method="_on_topmenu_button_pressed" binds= ["Achievements"]]
[connection signal="pressed" from="Stage/MainMenu/Media/Sound" to="Stage/MainMenu" method="_on_sound_pressed"]
[connection signal="pressed" from="Stage/MainMenu/Media/Music" to="Stage/MainMenu" method="_on_music_pressed"]
[connection signal="value_changed" from="Stage/MainMenu/Volume/SoundVolume" to="Stage/MainMenu" method="_on_sound_volume_value_changed"]
[connection signal="value_changed" from="Stage/MainMenu/Volume/MusicVolume" to="Stage/MainMenu" method="_on_music_volume_value_changed"]
[connection signal="pressed" from="Stage/MainMenu/Credits" to="Stage" method="_on_topmenu_button_pressed" binds= ["Credits"]]
[connection signal="pressed" from="Stage/Report/TopMenu/BackButton" to="Stage" method="_on_topmenu_button_pressed" binds= ["World"]]
[connection signal="pressed" from="Stage/Achievements/TopMenu/BackButton" to="Stage" method="_on_topmenu_button_pressed" binds= ["World"]]