RapidGameFramework
Reusable Godot managers for data-driven small games
Loading...
Searching...
No Matches
audio_manager.gd
Go to the documentation of this file.
1extends Node
2
3
10
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")
16
17var music_player:AudioStreamPlayer
18var next_music_player:AudioStreamPlayer
19var sfx_player:AudioStreamPlayer
20var music_tracks := []
21var music_playlist:AudioStreamPlaylist
22var playlist_active:bool = false
23var sfx_library := {}
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")
33
34
35func _ready() -> void:
36 set_process(true)
37 music_rng.randomize()
38 music_player = AudioStreamPlayer.new()
39 music_player.bus = "Music"
40 add_child(music_player)
41 music_player.finished.connect(_on_music_finished)
42
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)
47
48 sfx_player = AudioStreamPlayer.new()
49 sfx_player.bus = "SFX"
50 add_child(sfx_player)
51
52 _ensure_audio_buses()
53 load_audio()
54 if has_node("/root/SettingsManager"):
55 SettingsManager.settings_changed.connect(_apply_settings)
56 _apply_settings(SettingsManager.get_settings())
57 play_all_music()
58
59
60func _process(_delta:float) -> void:
61 if pending_next_track:
62 pending_next_track = false
63 _play_next_track()
64
65
66
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())
73 else:
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
79 sfx_library.clear()
80 for path in _find_audio_files(sfx_dir):
81 var stream = _load_audio_stream(path)
82 if stream != null:
83 sfx_library[path.get_file().get_basename()] = stream
84 _preload_generated_sfx()
85
86
87
88func play_all_music() -> void:
89 if music_tracks.is_empty() or music_player.playing:
90 return
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
96 music_player.play()
97 return
98 playlist_active = false
99 current_track_index = -1
100 _play_next_track()
101
102
103
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)
110
111
112
113func play_music(track_name:String) -> void:
114 for i in range(music_tracks.size()):
115 var stream:AudioStream = music_tracks[i] as AudioStream
116 if stream == null:
117 continue
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()
123 return
124
125
126
128func prepare_next_music_track() -> void:
129 _prepare_next_track()
130
131
132
133func get_music_cache_state() -> Dictionary:
134 return {
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()
142 }
143
144
145
146func get_cache_stats() -> Dictionary:
147 return {
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))
162 }
163
164
165func _play_stream_from_index(index:int) -> void:
166 if index < 0 or index >= music_tracks.size():
167 return
168 var stream:AudioStream = music_tracks[index] as AudioStream
169 if stream == null:
170 return
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
177 else:
178 music_player.stream = stream
179 music_player.play()
180
181
182func _prepare_next_track() -> void:
183 if next_music_player == null:
184 return
185 if not _should_queue_next_music_track():
186 _clear_queued_music_stream()
187 return
188 if music_tracks.size() <= 1 or current_track_index < 0:
189 _clear_queued_music_stream()
190 return
191 queued_track_index = (current_track_index + 1) % music_tracks.size()
192 var stream:AudioStream = music_tracks[queued_track_index] as AudioStream
193 if stream == null:
194 return
195 if next_music_player.stream != stream:
196 next_music_player.stream = stream
197
198
199
200func stop_music() -> void:
201 music_player.stop()
202 if next_music_player != null:
203 next_music_player.stop()
204
205
206
209func dispose() -> void:
210 set_process(false)
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):
215 continue
216 player.stop()
217 player.stream = null
218 if player.get_parent() == self:
219 player.queue_free()
220 elif not player.is_inside_tree():
221 player.free()
222 music_tracks.clear()
223 music_playlist = null
224 playlist_active = false
225 sfx_library.clear()
226 generated_sfx_library.clear()
227 music_player = null
228 next_music_player = null
229 sfx_player = null
230
231
232
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]
237 sfx_player.play()
238 elif fallback_sfx_enabled and generated_sfx_library.has("default"):
239 sfx_player.stream = generated_sfx_library["default"]
240 sfx_player.play()
241 return
242 sfx_player.stream = sfx_library[sound_name]
243 sfx_player.play()
244
245
246
247func play_button_click() -> void:
248 play_sfx("button_click")
249
250
251
252func install_button_sounds(root:Node) -> void:
253 if root == null:
254 return
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)
259
260
261func _play_next_track() -> void:
262 if music_tracks.is_empty():
263 return
264 if queued_track_index >= 0:
265 current_track_index = queued_track_index
266 else:
267 current_track_index = (current_track_index + 1) % music_tracks.size()
268 _play_stream_from_index(current_track_index)
269 _prepare_next_track()
270
271
272func _on_music_finished() -> void:
273 if playlist_active:
274 return
275 pending_next_track = true
276
277
278func _preload_generated_sfx() -> void:
279 if not generated_sfx_library.is_empty():
280 return
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)
299
300
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))
304 var data:PackedByteArray = PackedByteArray()
305 data.resize(sample_count * 2)
306 for sample_index in range(sample_count):
307 var t:float = float(sample_index) / float(mix_rate)
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
315 stream.data = data
316 return stream
317
318
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)
323 var data:PackedByteArray = PackedByteArray()
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
339 stream.data = data
340 return stream
341
342
343func _load_audio_streams(dir_path:String) -> Array:
344 var streams = []
345 for path in _find_audio_files(dir_path):
346 var stream = _load_audio_stream(path)
347 if stream != null:
348 streams.append(stream)
349 return streams
350
351
352func _load_music_manifest(path:String) -> Array:
353 var streams = []
354 var parsed:Dictionary = data_cache_manager.load_json(path)
355 var rows = parsed.get("music", [])
356 if not rows is Array:
357 return streams
358 for row in rows:
359 var track_path := ""
360 if row is Dictionary:
361 track_path = str(row.get("path", ""))
362 else:
363 track_path = str(row)
364 var stream = _load_audio_stream(track_path)
365 if stream != null:
366 streams.append(stream)
367 return streams
368
369
370
372func _build_music_playlist() -> void:
373 music_playlist = null
374 playlist_active = false
375 if music_tracks.size() <= 1:
376 return
377 var playlist := AudioStreamPlaylist.new()
378 playlist.stream_count = mini(music_tracks.size(), AudioStreamPlaylist.MAX_STREAMS)
379 playlist.loop = true
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
385
386
387func _load_audio_stream(path:String):
388 if path == "":
389 return null
390 if ResourceLoader.exists(path):
391 return ResourceLoader.load(path)
392 return null
393
394
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)
398 if swap_index == i:
399 continue
400 var current = music_tracks[i]
401 music_tracks[i] = music_tracks[swap_index]
402 music_tracks[swap_index] = current
403
404
405func _find_audio_files(dir_path:String) -> Array:
406 var paths = []
407 var dir = DirAccess.open(dir_path)
408 if dir == null:
409 return paths
410 dir.list_dir_begin()
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()
416 return paths
417
418
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):
423 return true
424 return false
425
426
427func _ensure_audio_buses() -> void:
428 _ensure_bus("Music")
429 _ensure_bus("SFX")
430
431
432func _ensure_bus(bus_name:String) -> void:
433 if AudioServer.get_bus_index(bus_name) >= 0:
434 return
435 AudioServer.add_bus()
436 var index = AudioServer.get_bus_count() - 1
437 AudioServer.set_bus_name(index, bus_name)
438
439
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()
446
447
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
453
454
455func _is_low_power_mode() -> bool:
456 if SettingsManager == null or not is_instance_valid(SettingsManager):
457 if is_inside_tree():
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))
464 return false
465
466
467func _should_queue_next_music_track() -> bool:
468 if not _is_low_power_mode():
469 return true
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))
475 return false
476
477
478func _set_bus(bus_name:String, linear_volume:float, muted:bool) -> void:
479 var index = AudioServer.get_bus_index(bus_name)
480 if index < 0:
481 return
482 AudioServer.set_bus_volume_db(index, linear_to_db(clampf(linear_volume, 0.0, 1.0)))
483 AudioServer.set_bus_mute(index, muted)