# Audio —— BGM 交叉淡入淡出 + UI 音效池 + 3D 定位音效。 # 对标客户端 AudioLib:BGM(`bgm/*.mp3`)、UI wav(`**/sound/ui/`)、 # 3D wav(`**/sound/`)。纯资产接线,无解码器移植(`.mss` 不用)。 # 用 preload("res://audio.gd").new() 造一个,add_child,再 setup()。 extends Node var _assets: String var _bgm_root: String var _sound_roots: PackedStringArray = [] var _cache := {} # 绝对路径 -> AudioStream var _bgm := [null, null] # 两个 AudioStreamPlayer 交叉淡 var _bgm_cur := 0 var _bgm_name := "" var _ui_pool: Array[AudioStreamPlayer] = [] var _ui_next := 0 var master_bgm := 0.6 var master_sfx := 0.9 var _queued_bgm := "" func _ready() -> void: if _queued_bgm != "": var n := _queued_bgm _queued_bgm = "" _bgm_name = "" play_bgm(n) func setup(assets_root: String) -> void: _assets = assets_root # bgm/ 与 assets/ 同级(/bgm,见 AssetRoot) _bgm_root = _assets.get_base_dir().path_join("bgm") # 已知声音根 + 各 patch 的 sound/ for r in ["Sound/sound", "sound2/sound", "sound_m/sound"]: var p := _assets.path_join(r) if DirAccess.dir_exists_absolute(p): _sound_roots.append(p) var da := DirAccess.open(_assets) if da: for sub in da.get_directories(): var p := _assets.path_join(sub).path_join("sound") if DirAccess.dir_exists_absolute(p): _sound_roots.append(p) for i in 2: var pl := AudioStreamPlayer.new() pl.bus = "Master" add_child(pl) _bgm[i] = pl for i in 8: var pl := AudioStreamPlayer.new() add_child(pl) _ui_pool.append(pl) # --- resolution ----------------------------------------------------------- func _load_stream(path: String) -> AudioStream: if _cache.has(path): return _cache[path] var s: AudioStream = null var ext := path.get_extension().to_lower() if ext == "mp3": var m := AudioStreamMP3.new() m.data = FileAccess.get_file_as_bytes(path) s = m if m.data.size() > 0 else null elif ext == "ogg": s = AudioStreamOggVorbis.load_from_file(path) elif ext == "wav": s = AudioStreamWAV.load_from_file(path) _cache[path] = s return s func _find_sound(rel: String) -> String: rel = rel.replace("\\", "/") var cands := PackedStringArray([rel]) if rel.get_extension().is_empty(): cands = PackedStringArray([rel + ".wav", rel + ".ogg"]) for root in _sound_roots: for c in cands: var p := root.path_join(c) if FileAccess.file_exists(p): return p # also try under sound/ui/ and sound/common/ for bare names if not rel.contains("/"): for d in ["ui", "common"]: for c in cands: var p := root.path_join(d).path_join(c) if FileAccess.file_exists(p): return p return "" # --- BGM --------------------------------------------------------------- func play_bgm(name: String, fade := 1.2) -> void: if name == _bgm_name: return if not is_node_ready() or not is_inside_tree(): _queued_bgm = name # flushed in _ready() return _bgm_name = name var path := name if path.get_extension().is_empty(): path = _bgm_root.path_join(name + ".mp3") elif not path.is_absolute_path(): path = _bgm_root.path_join(name) var stream := _load_stream(path) if stream == null: push_warning("[audio] bgm not found: " + path) return if stream is AudioStreamMP3: stream.loop = true var nxt := 1 - _bgm_cur _bgm[nxt].stream = stream _bgm[nxt].volume_db = -40.0 _bgm[nxt].play() _fade(_bgm[nxt], linear_to_db(master_bgm), fade) _fade(_bgm[_bgm_cur], -40.0, fade, true) _bgm_cur = nxt func stop_bgm(fade := 1.0) -> void: _bgm_name = "" _fade(_bgm[_bgm_cur], -40.0, fade, true) func _fade(pl: AudioStreamPlayer, to_db: float, dur: float, stop_after := false) -> void: var tw := create_tween() tw.tween_property(pl, "volume_db", to_db, dur) if stop_after: tw.tween_callback(pl.stop) # --- SFX ------------------------------------------------------------------- func play_ui(name: String) -> void: var path := _find_sound(name if name.contains("/") else "ui/" + name) if path.is_empty(): path = _find_sound(name) if path.is_empty(): push_warning("[audio] ui sfx not found: " + name) return var s := _load_stream(path) if s == null: return var pl := _ui_pool[_ui_next] _ui_next = (_ui_next + 1) % _ui_pool.size() pl.stream = s pl.volume_db = linear_to_db(master_sfx) pl.play() func play_at(rel: String, world_pos: Vector3, parent: Node = null) -> void: var path := _find_sound(rel) if path.is_empty(): return var s := _load_stream(path) if s == null: return var pl := AudioStreamPlayer3D.new() pl.stream = s pl.unit_size = 6.0 pl.max_distance = 60.0 pl.volume_db = linear_to_db(master_sfx) (parent if parent else self).add_child(pl) pl.global_position = world_pos pl.play() pl.finished.connect(pl.queue_free)