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
21var music_playlist:AudioStreamPlaylist
22var playlist_active:bool = false
24var generated_sfx_library := {}
25var current_track_index := -1
26var queued_track_index := -1
27var fallback_sfx_enabled := true
28var fallback_music_enabled := true
29var pending_next_track:bool = false
30var music_rng := RandomNumberGenerator.new()
31var data_cache_manager = DataCacheManager.new()
32@onready var SettingsManager = get_node_or_null(
"/root/SettingsManager")
38 music_player = AudioStreamPlayer.new()
39 music_player.bus =
"Music"
40 add_child(music_player)
41 music_player.finished.connect(_on_music_finished)
43 next_music_player = AudioStreamPlayer.new()
44 next_music_player.bus =
"Music"
45 add_child(next_music_player)
46 next_music_player.finished.connect(_on_music_finished)
48 sfx_player = AudioStreamPlayer.new()
49 sfx_player.bus =
"SFX"
54 if has_node(
"/root/SettingsManager"):
55 SettingsManager.settings_changed.connect(_apply_settings)
56 _apply_settings(SettingsManager.get_settings())
60func _process(_delta:float) -> void:
61 if pending_next_track:
62 pending_next_track = false
67func load_audio(music_dir:String = MUSIC_DIR, sfx_dir:String = SFX_DIR) -> void:
68 music_tracks = _load_music_manifest(MUSIC_MANIFEST)
69 if music_tracks.is_empty():
70 music_tracks = _load_audio_streams(music_dir)
71 if music_tracks.is_empty()
and fallback_music_enabled:
72 music_tracks.append(_make_chord_loop())
74 _shuffle_music_tracks()
75 _build_music_playlist()
76 queued_track_index = -1
77 if next_music_player != null:
78 next_music_player.stream = null
80 for path
in _find_audio_files(sfx_dir):
81 var stream = _load_audio_stream(path)
83 sfx_library[path.get_file().get_basename()] = stream
84 _preload_generated_sfx()
88func play_all_music() -> void:
89 if music_tracks.is_empty()
or music_player.playing:
91 if music_playlist != null
and music_tracks.size() > 1:
92 playlist_active = true
93 current_track_index = -1
94 _clear_queued_music_stream()
95 music_player.stream = music_playlist
98 playlist_active = false
99 current_track_index = -1
104func sync_from_settings() -> void:
105 if SettingsManager == null
or not is_instance_valid(SettingsManager):
106 SettingsManager = get_node_or_null(
"/root/SettingsManager")
107 if SettingsManager != null
and SettingsManager.has_method(
"get_settings"):
108 var settings:Dictionary = SettingsManager.call(
"get_settings")
as Dictionary
109 _apply_settings(settings)
113func play_music(track_name:String) -> void:
114 for i
in range(music_tracks.size()):
115 var stream:AudioStream = music_tracks[i]
as AudioStream
118 if stream.resource_path.get_file().get_basename() == track_name:
119 playlist_active = false
120 current_track_index = i
121 _play_stream_from_index(i)
122 _prepare_next_track()
128func prepare_next_music_track() -> void:
129 _prepare_next_track()
133func get_music_cache_state() -> Dictionary:
135 "track_count": music_tracks.size(),
136 "current_track_index": current_track_index,
137 "queued_track_index": queued_track_index,
138 "has_queued_stream": next_music_player != null
and next_music_player.stream != null,
139 "playlist_active": playlist_active,
140 "playlist_tracks": music_playlist.stream_count
if music_playlist != null
else 0,
141 "low_power_mode": _is_low_power_mode()
146func get_cache_stats() -> Dictionary:
148 "music_tracks": music_tracks.size(),
149 "sfx_streams": sfx_library.size(),
150 "generated_sfx": generated_sfx_library.size(),
151 "has_music_player": 1
if music_player != null
and is_instance_valid(music_player)
else 0,
152 "has_next_music_player": 1
if next_music_player != null
and is_instance_valid(next_music_player)
else 0,
153 "has_sfx_player": 1
if sfx_player != null
and is_instance_valid(sfx_player)
else 0,
154 "has_queued_stream": 1
if next_music_player != null
and is_instance_valid(next_music_player)
and next_music_player.stream != null
else 0,
155 "low_power_mode": 1
if _is_low_power_mode()
else 0,
156 "music_playing": 1
if music_player != null
and is_instance_valid(music_player)
and music_player.playing
else 0,
157 "playlist_active": 1
if playlist_active
else 0,
158 "playlist_tracks": music_playlist.stream_count
if music_playlist != null
else 0,
159 "data_cache_entries":
int(data_cache_manager.get_cache_stats().get(
"entries", 0)),
160 "data_cache_hits":
int(data_cache_manager.get_cache_stats().get(
"cache_hits", 0)),
161 "data_cache_misses":
int(data_cache_manager.get_cache_stats().get(
"cache_misses", 0))
165func _play_stream_from_index(index:int) -> void:
166 if index < 0
or index >= music_tracks.size():
168 var stream:AudioStream = music_tracks[index]
as AudioStream
171 if next_music_player != null
and next_music_player.stream == stream:
172 var previous_player:AudioStreamPlayer = music_player
173 music_player = next_music_player
174 next_music_player = previous_player
175 next_music_player.stop()
176 next_music_player.stream = null
178 music_player.stream = stream
182func _prepare_next_track() -> void:
183 if next_music_player == null:
185 if not _should_queue_next_music_track():
186 _clear_queued_music_stream()
188 if music_tracks.size() <= 1
or current_track_index < 0:
189 _clear_queued_music_stream()
191 queued_track_index = (current_track_index + 1) % music_tracks.size()
192 var stream:AudioStream = music_tracks[queued_track_index]
as AudioStream
195 if next_music_player.stream != stream:
196 next_music_player.stream = stream
200func stop_music() -> void:
202 if next_music_player != null:
203 next_music_player.stop()
209func dispose() -> void:
211 if SettingsManager != null
and is_instance_valid(SettingsManager)
and SettingsManager.settings_changed.is_connected(_apply_settings):
212 SettingsManager.settings_changed.disconnect(_apply_settings)
213 for player
in [music_player, next_music_player, sfx_player]:
214 if player == null
or not is_instance_valid(player):
218 if player.get_parent() == self:
220 elif not player.is_inside_tree():
223 music_playlist = null
224 playlist_active = false
226 generated_sfx_library.clear()
228 next_music_player = null
233func play_sfx(sound_name:String) -> void:
234 if not sfx_library.has(sound_name):
235 if fallback_sfx_enabled
and generated_sfx_library.has(sound_name):
236 sfx_player.stream = generated_sfx_library[sound_name]
238 elif fallback_sfx_enabled
and generated_sfx_library.has(
"default"):
239 sfx_player.stream = generated_sfx_library[
"default"]
242 sfx_player.stream = sfx_library[sound_name]
247func play_button_click() -> void:
248 play_sfx(
"button_click")
252func install_button_sounds(root:Node) -> void:
255 for child
in root.get_children():
256 if child
is BaseButton
and not child.pressed.is_connected(play_button_click):
257 child.pressed.connect(play_button_click)
258 install_button_sounds(child)
261func _play_next_track() -> void:
262 if music_tracks.is_empty():
264 if queued_track_index >= 0:
265 current_track_index = queued_track_index
267 current_track_index = (current_track_index + 1) % music_tracks.size()
268 _play_stream_from_index(current_track_index)
269 _prepare_next_track()
272func _on_music_finished() -> void:
275 pending_next_track = true
278func _preload_generated_sfx() -> void:
279 if not generated_sfx_library.is_empty():
281 generated_sfx_library[
"default"] = _make_tone(420.0, 0.055, 0.34)
282 generated_sfx_library[
"button_click"] = _make_tone(520.0, 0.045, 0.28)
283 generated_sfx_library[
"paddle_hit"] = _make_tone(360.0, 0.04, 0.42)
284 generated_sfx_library[
"score"] = _make_tone(660.0, 0.12, 0.4)
285 generated_sfx_library[
"powerup"] = _make_tone(760.0, 0.09, 0.34)
286 generated_sfx_library[
"powerup_use"] = _make_tone(880.0, 0.07, 0.38)
287 generated_sfx_library[
"rocket"] = _make_tone(180.0, 0.13, 0.42)
288 generated_sfx_library[
"stun"] = _make_tone(120.0, 0.16, 0.46)
289 generated_sfx_library[
"match_win"] = _make_tone(980.0, 0.18, 0.38)
290 generated_sfx_library[
"card_purchase"] = _make_tone(720.0, 0.08, 0.32)
291 generated_sfx_library[
"card_sell"] = _make_tone(460.0, 0.07, 0.30)
292 generated_sfx_library[
"card_merge"] = _make_tone(840.0, 0.12, 0.36)
293 generated_sfx_library[
"card_roll"] = _make_tone(610.0, 0.055, 0.30)
294 generated_sfx_library[
"card_damage"] = _make_tone(210.0, 0.09, 0.38)
295 generated_sfx_library[
"card_win"] = _make_tone(1040.0, 0.16, 0.34)
296 generated_sfx_library[
"card_loss"] = _make_tone(160.0, 0.20, 0.36)
297 generated_sfx_library[
"card_death"] = _make_tone(110.0, 0.22, 0.34)
298 generated_sfx_library[
"achievement_unlock"] = _make_tone(1180.0, 0.18, 0.32)
301func _make_tone(frequency:float, duration:float, volume:float) -> AudioStreamWAV:
302 var mix_rate:int = 22050
303 var sample_count:int = max(1,
int(
float(mix_rate) * duration))
305 data.resize(sample_count * 2)
306 for sample_index
in range(sample_count):
308 var fade:float = 1.0 - (
float(sample_index) /
float(sample_count))
309 var sample:float = sin(TAU * frequency * t) * volume * fade
310 data.encode_s16(sample_index * 2,
int(clamp(sample, -1.0, 1.0) * 32767.0))
311 var stream:AudioStreamWAV = AudioStreamWAV.new()
312 stream.format = AudioStreamWAV.FORMAT_16_BITS
313 stream.mix_rate = mix_rate
314 stream.stereo = false
319func _make_chord_loop() -> AudioStreamWAV:
320 var mix_rate:int = 22050
321 var duration:float = 3.5
322 var sample_count:int =
int(
float(mix_rate) * duration)
324 data.resize(sample_count * 2)
325 var frequencies:Array[float] = [196.0, 246.94, 293.66]
326 for sample_index
in range(sample_count):
327 var t:float =
float(sample_index) /
float(mix_rate)
328 var sample:float = 0.0
329 for frequency
in frequencies:
330 sample += sin(TAU * frequency * t) * 0.045
331 var fade_in:float = min(1.0, t / 0.18)
332 var fade_out:float = min(1.0, (duration - t) / 0.35)
333 sample *= min(fade_in, fade_out)
334 data.encode_s16(sample_index * 2,
int(clamp(sample, -1.0, 1.0) * 32767.0))
335 var stream:AudioStreamWAV = AudioStreamWAV.new()
336 stream.format = AudioStreamWAV.FORMAT_16_BITS
337 stream.mix_rate = mix_rate
338 stream.stereo = false
343func _load_audio_streams(dir_path:String) -> Array:
345 for path
in _find_audio_files(dir_path):
346 var stream = _load_audio_stream(path)
348 streams.append(stream)
352func _load_music_manifest(path:String) -> Array:
354 var parsed:Dictionary = data_cache_manager.load_json(path)
355 var rows = parsed.get(
"music", [])
356 if not rows
is Array:
360 if row
is Dictionary:
361 track_path = str(row.get(
"path",
""))
363 track_path = str(row)
364 var stream = _load_audio_stream(track_path)
366 streams.append(stream)
372func _build_music_playlist() -> void:
373 music_playlist = null
374 playlist_active = false
375 if music_tracks.size() <= 1:
377 var playlist := AudioStreamPlaylist.new()
378 playlist.stream_count = mini(music_tracks.size(), AudioStreamPlaylist.MAX_STREAMS)
380 playlist.shuffle = false
381 playlist.fade_time = 0.2
382 for index
in range(playlist.stream_count):
383 playlist.set_list_stream(index, music_tracks[index]
as AudioStream)
384 music_playlist = playlist
387func _load_audio_stream(path:String):
390 if ResourceLoader.exists(path):
391 return ResourceLoader.load(path)
395func _shuffle_music_tracks() -> void:
396 for i
in range(music_tracks.size() - 1, 0, -1):
397 var swap_index:int = music_rng.randi_range(0, i)
400 var current = music_tracks[i]
401 music_tracks[i] = music_tracks[swap_index]
402 music_tracks[swap_index] = current
405func _find_audio_files(dir_path:String) -> Array:
407 var dir = DirAccess.open(dir_path)
411 var file_name = dir.get_next()
412 while file_name !=
"":
413 if not dir.current_is_dir()
and _is_audio_file(file_name):
414 paths.append(dir_path +
"/" + file_name)
415 file_name = dir.get_next()
419func _is_audio_file(file_name:String) -> bool:
420 var lower = file_name.to_lower()
421 for extension
in AUDIO_EXTENSIONS:
422 if lower.ends_with(extension):
427func _ensure_audio_buses() -> void:
432func _ensure_bus(bus_name:String) -> void:
433 if AudioServer.get_bus_index(bus_name) >= 0:
435 AudioServer.add_bus()
436 var index = AudioServer.get_bus_count() - 1
437 AudioServer.set_bus_name(index, bus_name)
440func _apply_settings(settings:Dictionary) -> void:
441 _set_bus(
"Master",
float(settings.get(
"master_volume", 1.0)),
bool(settings.get(
"master_muted", false)))
442 _set_bus(
"Music",
float(settings.get(
"music_volume", 1.0)),
bool(settings.get(
"music_muted", false)))
443 _set_bus(
"SFX",
float(settings.get(
"sfx_volume", 1.0)),
bool(settings.get(
"sfx_muted", false)))
444 if bool(settings.get(
"low_power_mode", false)):
445 _clear_queued_music_stream()
448func _clear_queued_music_stream() -> void:
449 queued_track_index = -1
450 if next_music_player != null:
451 next_music_player.stop()
452 next_music_player.stream = null
455func _is_low_power_mode() -> bool:
456 if SettingsManager == null
or not is_instance_valid(SettingsManager):
458 SettingsManager = get_node_or_null(
"/root/SettingsManager")
459 if SettingsManager != null
and SettingsManager.has_method(
"is_low_power_mode"):
460 return bool(SettingsManager.call(
"is_low_power_mode"))
461 if SettingsManager != null
and SettingsManager.has_method(
"get_settings"):
462 var settings:Dictionary = SettingsManager.call(
"get_settings")
as Dictionary
463 return bool(settings.get(
"low_power_mode", false))
467func _should_queue_next_music_track() -> bool:
468 if not _is_low_power_mode():
470 if SettingsManager != null
and SettingsManager.has_method(
"get_low_power_profile"):
471 var profile:Dictionary = SettingsManager.call(
"get_low_power_profile")
as Dictionary
472 var audio_profile = profile.get(
"audio", {})
473 if audio_profile
is Dictionary:
474 return bool(audio_profile.get(
"queue_next_music_track", false))
478func _set_bus(bus_name:String, linear_volume:float, muted:bool) -> void:
479 var index = AudioServer.get_bus_index(bus_name)
482 AudioServer.set_bus_volume_db(index, linear_to_db(clampf(linear_volume, 0.0, 1.0)))
483 AudioServer.set_bus_mute(index, muted)