11const MUSIC_DIR :=
"res://assets/music"
12const SFX_DIR :=
"res://assets/sfx"
13const MUSIC_MANIFEST :=
"res://data/audio/music_tracks.json"
14const AUDIO_EXTENSIONS := [
".ogg",
".wav",
".mp3"]
15const DataCacheManager = preload(
"res://scripts/systems/dataCacheManager/data_cache_manager.gd")
17var music_player:AudioStreamPlayer
18var next_music_player:AudioStreamPlayer
19var sfx_player:AudioStreamPlayer
22var generated_sfx_library := {}
23var current_track_index := -1
24var queued_track_index := -1
25var fallback_sfx_enabled := true
26var fallback_music_enabled := true
27var pending_next_track:bool = false
28var music_rng := RandomNumberGenerator.new()
29var data_cache_manager = DataCacheManager.new()
30@onready var SettingsManager = get_node_or_null(
"/root/SettingsManager")
36 music_player = AudioStreamPlayer.new()
37 music_player.bus =
"Music"
38 add_child(music_player)
39 music_player.finished.connect(_on_music_finished)
41 next_music_player = AudioStreamPlayer.new()
42 next_music_player.bus =
"Music"
43 add_child(next_music_player)
44 next_music_player.finished.connect(_on_music_finished)
46 sfx_player = AudioStreamPlayer.new()
47 sfx_player.bus =
"SFX"
52 if has_node(
"/root/SettingsManager"):
53 SettingsManager.settings_changed.connect(_apply_settings)
54 _apply_settings(SettingsManager.get_settings())
58func _process(_delta:float) -> void:
59 if pending_next_track:
60 pending_next_track = false
65func load_audio(music_dir:String = MUSIC_DIR, sfx_dir:String = SFX_DIR) -> void:
66 music_tracks = _load_music_manifest(MUSIC_MANIFEST)
67 if music_tracks.is_empty():
68 music_tracks = _load_audio_streams(music_dir)
69 if music_tracks.is_empty()
and fallback_music_enabled:
70 music_tracks.append(_make_chord_loop())
72 _shuffle_music_tracks()
73 queued_track_index = -1
74 if next_music_player != null:
75 next_music_player.stream = null
77 for path
in _find_audio_files(sfx_dir):
78 sfx_library[path.get_file().get_basename()] = load(path)
79 _preload_generated_sfx()
83func play_all_music() -> void:
84 if music_tracks.is_empty()
or music_player.playing:
86 current_track_index = -1
91func play_music(track_name:String) -> void:
92 for i
in range(music_tracks.size()):
93 var stream:AudioStream = music_tracks[i]
as AudioStream
96 if stream.resource_path.get_file().get_basename() == track_name:
97 current_track_index = i
98 _play_stream_from_index(i)
105func prepare_next_music_track() -> void:
106 _prepare_next_track()
110func get_music_cache_state() -> Dictionary:
112 "track_count": music_tracks.size(),
113 "current_track_index": current_track_index,
114 "queued_track_index": queued_track_index,
115 "has_queued_stream": next_music_player != null
and next_music_player.stream != null,
116 "low_power_mode": _is_low_power_mode()
121func get_cache_stats() -> Dictionary:
123 "music_tracks": music_tracks.size(),
124 "sfx_streams": sfx_library.size(),
125 "generated_sfx": generated_sfx_library.size(),
126 "has_music_player": 1
if music_player != null
and is_instance_valid(music_player)
else 0,
127 "has_next_music_player": 1
if next_music_player != null
and is_instance_valid(next_music_player)
else 0,
128 "has_sfx_player": 1
if sfx_player != null
and is_instance_valid(sfx_player)
else 0,
129 "has_queued_stream": 1
if next_music_player != null
and is_instance_valid(next_music_player)
and next_music_player.stream != null
else 0,
130 "low_power_mode": 1
if _is_low_power_mode()
else 0,
131 "music_playing": 1
if music_player != null
and is_instance_valid(music_player)
and music_player.playing
else 0,
132 "data_cache_entries":
int(data_cache_manager.get_cache_stats().get(
"entries", 0)),
133 "data_cache_hits":
int(data_cache_manager.get_cache_stats().get(
"cache_hits", 0)),
134 "data_cache_misses":
int(data_cache_manager.get_cache_stats().get(
"cache_misses", 0))
138func _play_stream_from_index(index:int) -> void:
139 if index < 0
or index >= music_tracks.size():
141 var stream:AudioStream = music_tracks[index]
as AudioStream
144 if next_music_player != null
and next_music_player.stream == stream:
145 var previous_player:AudioStreamPlayer = music_player
146 music_player = next_music_player
147 next_music_player = previous_player
148 next_music_player.stop()
149 next_music_player.stream = null
151 music_player.stream = stream
155func _prepare_next_track() -> void:
156 if next_music_player == null:
158 if not _should_queue_next_music_track():
159 _clear_queued_music_stream()
161 if music_tracks.size() <= 1
or current_track_index < 0:
162 _clear_queued_music_stream()
164 queued_track_index = (current_track_index + 1) % music_tracks.size()
165 var stream:AudioStream = music_tracks[queued_track_index]
as AudioStream
168 if next_music_player.stream != stream:
169 next_music_player.stream = stream
173func stop_music() -> void:
175 if next_music_player != null:
176 next_music_player.stop()
182func dispose() -> void:
184 if SettingsManager != null
and is_instance_valid(SettingsManager)
and SettingsManager.settings_changed.is_connected(_apply_settings):
185 SettingsManager.settings_changed.disconnect(_apply_settings)
186 for player
in [music_player, next_music_player, sfx_player]:
187 if player == null
or not is_instance_valid(player):
191 if player.get_parent() == self:
193 elif not player.is_inside_tree():
197 generated_sfx_library.clear()
199 next_music_player = null
204func play_sfx(sound_name:String) -> void:
205 if not sfx_library.has(sound_name):
206 if fallback_sfx_enabled
and generated_sfx_library.has(sound_name):
207 sfx_player.stream = generated_sfx_library[sound_name]
209 elif fallback_sfx_enabled
and generated_sfx_library.has(
"default"):
210 sfx_player.stream = generated_sfx_library[
"default"]
213 sfx_player.stream = sfx_library[sound_name]
218func play_button_click() -> void:
219 play_sfx(
"button_click")
223func install_button_sounds(root:Node) -> void:
226 for child
in root.get_children():
227 if child
is BaseButton
and not child.pressed.is_connected(play_button_click):
228 child.pressed.connect(play_button_click)
229 install_button_sounds(child)
232func _play_next_track() -> void:
233 if music_tracks.is_empty():
235 if queued_track_index >= 0:
236 current_track_index = queued_track_index
238 current_track_index = (current_track_index + 1) % music_tracks.size()
239 _play_stream_from_index(current_track_index)
240 _prepare_next_track()
243func _on_music_finished() -> void:
244 pending_next_track = true
247func _preload_generated_sfx() -> void:
248 if not generated_sfx_library.is_empty():
250 generated_sfx_library[
"default"] = _make_tone(420.0, 0.055, 0.34)
251 generated_sfx_library[
"button_click"] = _make_tone(520.0, 0.045, 0.28)
252 generated_sfx_library[
"paddle_hit"] = _make_tone(360.0, 0.04, 0.42)
253 generated_sfx_library[
"score"] = _make_tone(660.0, 0.12, 0.4)
254 generated_sfx_library[
"powerup"] = _make_tone(760.0, 0.09, 0.34)
255 generated_sfx_library[
"powerup_use"] = _make_tone(880.0, 0.07, 0.38)
256 generated_sfx_library[
"rocket"] = _make_tone(180.0, 0.13, 0.42)
257 generated_sfx_library[
"stun"] = _make_tone(120.0, 0.16, 0.46)
258 generated_sfx_library[
"match_win"] = _make_tone(980.0, 0.18, 0.38)
261func _make_tone(frequency:float, duration:float, volume:float) -> AudioStreamWAV:
262 var mix_rate:int = 22050
263 var sample_count:int = max(1,
int(
float(mix_rate) * duration))
265 data.resize(sample_count * 2)
266 for sample_index
in range(sample_count):
268 var fade:float = 1.0 - (
float(sample_index) /
float(sample_count))
269 var sample:float = sin(TAU * frequency * t) * volume * fade
270 data.encode_s16(sample_index * 2,
int(clamp(sample, -1.0, 1.0) * 32767.0))
271 var stream:AudioStreamWAV = AudioStreamWAV.new()
272 stream.format = AudioStreamWAV.FORMAT_16_BITS
273 stream.mix_rate = mix_rate
274 stream.stereo = false
279func _make_chord_loop() -> AudioStreamWAV:
280 var mix_rate:int = 22050
281 var duration:float = 3.5
282 var sample_count:int =
int(
float(mix_rate) * duration)
284 data.resize(sample_count * 2)
285 var frequencies:Array[float] = [196.0, 246.94, 293.66]
286 for sample_index
in range(sample_count):
287 var t:float =
float(sample_index) /
float(mix_rate)
288 var sample:float = 0.0
289 for frequency
in frequencies:
290 sample += sin(TAU * frequency * t) * 0.045
291 var fade_in:float = min(1.0, t / 0.18)
292 var fade_out:float = min(1.0, (duration - t) / 0.35)
293 sample *= min(fade_in, fade_out)
294 data.encode_s16(sample_index * 2,
int(clamp(sample, -1.0, 1.0) * 32767.0))
295 var stream:AudioStreamWAV = AudioStreamWAV.new()
296 stream.format = AudioStreamWAV.FORMAT_16_BITS
297 stream.mix_rate = mix_rate
298 stream.stereo = false
303func _load_audio_streams(dir_path:String) -> Array:
305 for path
in _find_audio_files(dir_path):
306 var stream = _load_audio_stream(path)
308 streams.append(stream)
312func _load_music_manifest(path:String) -> Array:
314 var parsed:Dictionary = data_cache_manager.load_json(path)
315 var rows = parsed.get(
"music", [])
316 if not rows
is Array:
320 if row
is Dictionary:
321 track_path = str(row.get(
"path",
""))
323 track_path = str(row)
324 var stream = _load_audio_stream(track_path)
326 streams.append(stream)
330func _load_audio_stream(path:String):
333 if ResourceLoader.exists(path):
334 return ResourceLoader.load(path)
335 if FileAccess.file_exists(path):
340func _shuffle_music_tracks() -> void:
341 for i
in range(music_tracks.size() - 1, 0, -1):
342 var swap_index:int = music_rng.randi_range(0, i)
345 var current = music_tracks[i]
346 music_tracks[i] = music_tracks[swap_index]
347 music_tracks[swap_index] = current
350func _find_audio_files(dir_path:String) -> Array:
352 var dir = DirAccess.open(dir_path)
356 var file_name = dir.get_next()
357 while file_name !=
"":
358 if not dir.current_is_dir()
and _is_audio_file(file_name):
359 paths.append(dir_path +
"/" + file_name)
360 file_name = dir.get_next()
364func _is_audio_file(file_name:String) -> bool:
365 var lower = file_name.to_lower()
366 for extension
in AUDIO_EXTENSIONS:
367 if lower.ends_with(extension):
372func _ensure_audio_buses() -> void:
377func _ensure_bus(bus_name:String) -> void:
378 if AudioServer.get_bus_index(bus_name) >= 0:
380 AudioServer.add_bus()
381 var index = AudioServer.get_bus_count() - 1
382 AudioServer.set_bus_name(index, bus_name)
385func _apply_settings(settings:Dictionary) -> void:
386 _set_bus(
"Master",
float(settings.get(
"master_volume", 1.0)),
bool(settings.get(
"master_muted", false)))
387 _set_bus(
"Music",
float(settings.get(
"music_volume", 1.0)),
bool(settings.get(
"music_muted", false)))
388 _set_bus(
"SFX",
float(settings.get(
"sfx_volume", 1.0)),
bool(settings.get(
"sfx_muted", false)))
389 if bool(settings.get(
"low_power_mode", false)):
390 _clear_queued_music_stream()
393func _clear_queued_music_stream() -> void:
394 queued_track_index = -1
395 if next_music_player != null:
396 next_music_player.stop()
397 next_music_player.stream = null
400func _is_low_power_mode() -> bool:
401 if SettingsManager == null
or not is_instance_valid(SettingsManager):
403 SettingsManager = get_node_or_null(
"/root/SettingsManager")
404 if SettingsManager != null
and SettingsManager.has_method(
"is_low_power_mode"):
405 return bool(SettingsManager.call(
"is_low_power_mode"))
406 if SettingsManager != null
and SettingsManager.has_method(
"get_settings"):
407 var settings:Dictionary = SettingsManager.call(
"get_settings")
as Dictionary
408 return bool(settings.get(
"low_power_mode", false))
412func _should_queue_next_music_track() -> bool:
413 if not _is_low_power_mode():
415 if SettingsManager != null
and SettingsManager.has_method(
"get_low_power_profile"):
416 var profile:Dictionary = SettingsManager.call(
"get_low_power_profile")
as Dictionary
417 var audio_profile = profile.get(
"audio", {})
418 if audio_profile
is Dictionary:
419 return bool(audio_profile.get(
"queue_next_music_track", false))
423func _set_bus(bus_name:String, linear_volume:float, muted:bool) -> void:
424 var index = AudioServer.get_bus_index(bus_name)
427 AudioServer.set_bus_volume_db(index, linear_to_db(clampf(linear_volume, 0.0, 1.0)))
428 AudioServer.set_bus_mute(index, muted)