# 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 := "" # §9.2 前后台音量存/恢复,字段名对齐参考 CSoundManager 的 # m_fBackupMusicVolume / m_fBackupSoundVolume(REF/MilesLib/SoundManager.h:31)。 var _backup_music_volume := -1.0 var _backup_sound_volume := -1.0 var _volume_saved := false 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) # 立即把 master_bgm 应用到当前正在播放的 BGM 声道(无淡入)。 # 对标 CSoundManager::SetMusicVolume:改的是「当前音乐实例」的音量。 func set_music_volume(v: float) -> void: master_bgm = clampf(v, 0.0, 1.0) var pl: AudioStreamPlayer = _bgm[_bgm_cur] if pl and pl.playing: pl.volume_db = linear_to_db(master_bgm) if master_bgm > 0.0 else -80.0 # §9.2:进入后台前保存音量(CSoundManager::SaveVolume)。 func save_volume() -> void: _backup_music_volume = master_bgm _backup_sound_volume = master_sfx _volume_saved = true # §9.2:回到前台后恢复音量(CSoundManager::RestoreVolume)。 # 不碰 _bgm_name / _bgm[],同一首继续播,只把音量拉回来。 func restore_volume() -> void: if not _volume_saved: return master_bgm = _backup_music_volume master_sfx = _backup_sound_volume _volume_saved = false var pl: AudioStreamPlayer = _bgm[_bgm_cur] if pl and pl.playing: pl.volume_db = linear_to_db(master_bgm) if master_bgm > 0.0 else -80.0 # --- 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)