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 sfx_library := {}
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")
31
32
33func _ready() -> void:
34 set_process(true)
35 music_rng.randomize()
36 music_player = AudioStreamPlayer.new()
37 music_player.bus = "Music"
38 add_child(music_player)
39 music_player.finished.connect(_on_music_finished)
40
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)
45
46 sfx_player = AudioStreamPlayer.new()
47 sfx_player.bus = "SFX"
48 add_child(sfx_player)
49
50 _ensure_audio_buses()
51 load_audio()
52 if has_node("/root/SettingsManager"):
53 SettingsManager.settings_changed.connect(_apply_settings)
54 _apply_settings(SettingsManager.get_settings())
55 play_all_music()
56
57
58func _process(_delta:float) -> void:
59 if pending_next_track:
60 pending_next_track = false
61 _play_next_track()
62
63
64
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())
71 else:
72 _shuffle_music_tracks()
73 queued_track_index = -1
74 if next_music_player != null:
75 next_music_player.stream = null
76 sfx_library.clear()
77 for path in _find_audio_files(sfx_dir):
78 sfx_library[path.get_file().get_basename()] = load(path)
79 _preload_generated_sfx()
80
81
82
83func play_all_music() -> void:
84 if music_tracks.is_empty() or music_player.playing:
85 return
86 current_track_index = -1
87 _play_next_track()
88
89
90
91func play_music(track_name:String) -> void:
92 for i in range(music_tracks.size()):
93 var stream:AudioStream = music_tracks[i] as AudioStream
94 if stream == null:
95 continue
96 if stream.resource_path.get_file().get_basename() == track_name:
97 current_track_index = i
98 _play_stream_from_index(i)
99 _prepare_next_track()
100 return
101
102
103
105func prepare_next_music_track() -> void:
106 _prepare_next_track()
107
108
109
110func get_music_cache_state() -> Dictionary:
111 return {
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()
117 }
118
119
120
121func get_cache_stats() -> Dictionary:
122 return {
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))
135 }
136
137
138func _play_stream_from_index(index:int) -> void:
139 if index < 0 or index >= music_tracks.size():
140 return
141 var stream:AudioStream = music_tracks[index] as AudioStream
142 if stream == null:
143 return
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
150 else:
151 music_player.stream = stream
152 music_player.play()
153
154
155func _prepare_next_track() -> void:
156 if next_music_player == null:
157 return
158 if not _should_queue_next_music_track():
159 _clear_queued_music_stream()
160 return
161 if music_tracks.size() <= 1 or current_track_index < 0:
162 _clear_queued_music_stream()
163 return
164 queued_track_index = (current_track_index + 1) % music_tracks.size()
165 var stream:AudioStream = music_tracks[queued_track_index] as AudioStream
166 if stream == null:
167 return
168 if next_music_player.stream != stream:
169 next_music_player.stream = stream
170
171
172
173func stop_music() -> void:
174 music_player.stop()
175 if next_music_player != null:
176 next_music_player.stop()
177
178
179
182func dispose() -> void:
183 set_process(false)
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):
188 continue
189 player.stop()
190 player.stream = null
191 if player.get_parent() == self:
192 player.queue_free()
193 elif not player.is_inside_tree():
194 player.free()
195 music_tracks.clear()
196 sfx_library.clear()
197 generated_sfx_library.clear()
198 music_player = null
199 next_music_player = null
200 sfx_player = null
201
202
203
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]
208 sfx_player.play()
209 elif fallback_sfx_enabled and generated_sfx_library.has("default"):
210 sfx_player.stream = generated_sfx_library["default"]
211 sfx_player.play()
212 return
213 sfx_player.stream = sfx_library[sound_name]
214 sfx_player.play()
215
216
217
218func play_button_click() -> void:
219 play_sfx("button_click")
220
221
222
223func install_button_sounds(root:Node) -> void:
224 if root == null:
225 return
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)
230
231
232func _play_next_track() -> void:
233 if music_tracks.is_empty():
234 return
235 if queued_track_index >= 0:
236 current_track_index = queued_track_index
237 else:
238 current_track_index = (current_track_index + 1) % music_tracks.size()
239 _play_stream_from_index(current_track_index)
240 _prepare_next_track()
241
242
243func _on_music_finished() -> void:
244 pending_next_track = true
245
246
247func _preload_generated_sfx() -> void:
248 if not generated_sfx_library.is_empty():
249 return
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)
259
260
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))
264 var data:PackedByteArray = PackedByteArray()
265 data.resize(sample_count * 2)
266 for sample_index in range(sample_count):
267 var t:float = float(sample_index) / float(mix_rate)
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
275 stream.data = data
276 return stream
277
278
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)
283 var data:PackedByteArray = PackedByteArray()
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
299 stream.data = data
300 return stream
301
302
303func _load_audio_streams(dir_path:String) -> Array:
304 var streams = []
305 for path in _find_audio_files(dir_path):
306 var stream = _load_audio_stream(path)
307 if stream != null:
308 streams.append(stream)
309 return streams
310
311
312func _load_music_manifest(path:String) -> Array:
313 var streams = []
314 var parsed:Dictionary = data_cache_manager.load_json(path)
315 var rows = parsed.get("music", [])
316 if not rows is Array:
317 return streams
318 for row in rows:
319 var track_path := ""
320 if row is Dictionary:
321 track_path = str(row.get("path", ""))
322 else:
323 track_path = str(row)
324 var stream = _load_audio_stream(track_path)
325 if stream != null:
326 streams.append(stream)
327 return streams
328
329
330func _load_audio_stream(path:String):
331 if path == "":
332 return null
333 if ResourceLoader.exists(path):
334 return ResourceLoader.load(path)
335 if FileAccess.file_exists(path):
336 return load(path)
337 return null
338
339
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)
343 if swap_index == i:
344 continue
345 var current = music_tracks[i]
346 music_tracks[i] = music_tracks[swap_index]
347 music_tracks[swap_index] = current
348
349
350func _find_audio_files(dir_path:String) -> Array:
351 var paths = []
352 var dir = DirAccess.open(dir_path)
353 if dir == null:
354 return paths
355 dir.list_dir_begin()
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()
361 return paths
362
363
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):
368 return true
369 return false
370
371
372func _ensure_audio_buses() -> void:
373 _ensure_bus("Music")
374 _ensure_bus("SFX")
375
376
377func _ensure_bus(bus_name:String) -> void:
378 if AudioServer.get_bus_index(bus_name) >= 0:
379 return
380 AudioServer.add_bus()
381 var index = AudioServer.get_bus_count() - 1
382 AudioServer.set_bus_name(index, bus_name)
383
384
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()
391
392
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
398
399
400func _is_low_power_mode() -> bool:
401 if SettingsManager == null or not is_instance_valid(SettingsManager):
402 if is_inside_tree():
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))
409 return false
410
411
412func _should_queue_next_music_track() -> bool:
413 if not _is_low_power_mode():
414 return true
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))
420 return false
421
422
423func _set_bus(bus_name:String, linear_volume:float, muted:bool) -> void:
424 var index = AudioServer.get_bus_index(bus_name)
425 if index < 0:
426 return
427 AudioServer.set_bus_volume_db(index, linear_to_db(clampf(linear_volume, 0.0, 1.0)))
428 AudioServer.set_bus_mute(index, muted)