# audio_driver.gd —— 桥接 40250 Python/MilesLib 音频调用至 Godot 引擎。 # 每帧通过 Metin2PythonHost.audio_commands() 拉取原生端产生的音频播放/音量指令并执行。 class_name AudioDriver extends Node const AssetRoot = preload("res://asset_root.gd") var _cache := {} # 绝对路径 -> AudioStream var _bgm_root: String var _sound_roots: PackedStringArray = [] var _bgm: Array[AudioStreamPlayer] = [] # 40250 CSoundManagerStream has three music slots. var _bgm_names: Array[String] = [] var _bgm_volumes: Array[float] = [] var _bgm_fades := {} # slot -> {target, speed, mode} var _ui_pool: Array[AudioStreamPlayer] = [] var _ui_next := 0 var master_sound := 1.0 var master_music := 1.0 # 3D 循环音效实例:id -> AudioStreamPlayer3D;所有 3D 播放器用于 StopAll。 var _active_3d := {} var _all_3d: Array[AudioStreamPlayer3D] = [] func _ready() -> void: _init_roots() _init_players() func _init_roots() -> void: var client := AssetRoot.client_path() var assets := AssetRoot.path() # BGM 根目录:优先 Client/BGM,次选 assets/BGM if DirAccess.dir_exists_absolute(client.path_join("BGM")): _bgm_root = client.path_join("BGM") elif DirAccess.dir_exists_absolute(assets.path_join("BGM")): _bgm_root = assets.path_join("BGM") else: _bgm_root = client.path_join("BGM") # 音效查找根目录 var candidate_roots := [ client.path_join("sound"), client.path_join("Sound/sound"), assets.path_join("Sound/sound"), assets.path_join("sound2/sound"), assets.path_join("sound_m/sound"), assets.path_join("metin2_patch_w20_sound/sound"), assets.path_join("Sound"), assets, client, ] for r in candidate_roots: if DirAccess.dir_exists_absolute(r) and not _sound_roots.has(r): _sound_roots.append(r) func _init_players() -> void: for i in 3: var pl := AudioStreamPlayer.new() pl.bus = "Master" pl.name = "BgmPlayer%d" % i add_child(pl) _bgm.append(pl) _bgm_names.append("") _bgm_volumes.append(0.0) # 16 个 2D 音效池播放器 for i in 16: var pl := AudioStreamPlayer.new() pl.bus = "Master" pl.name = "UiPlayer%d" % i add_child(pl) _ui_pool.append(pl) func _process(_delta: float) -> void: if not ClassDB.class_exists("Metin2PythonHost") or not Metin2PythonHost.is_running(): return var commands: Array = Metin2PythonHost.audio_commands() for cmd in commands: var type: String = cmd.get("type", "") match type: "play_sound_2d": play_sound_2d(cmd.get("filename", ""), cmd.get("volume", 1.0)) "play_sound_3d": play_sound_3d(cmd.get("filename", ""), Vector3(cmd.get("x", 0.0) / 100.0, cmd.get("z", 0.0) / 100.0, -cmd.get("y", 0.0) / 100.0), cmd.get("volume", 1.0), cmd.get("id", 0), cmd.get("play_count", 1)) "stop_sound_3d": stop_sound_3d(cmd.get("id", 0)) "stop_all_sound_3d": stop_all_sound_3d() "set_sound_volume_3d": set_sound_volume_3d(cmd.get("id", 0), cmd.get("volume", 1.0)) "play_music": play_music(cmd.get("filename", ""), cmd.get("volume", 1.0)) "fade_in_music": fade_in_music(cmd.get("filename", ""), cmd.get("volume", 1.0), cmd.get("speed", 0.016)) "fade_out_music": fade_out_music(cmd.get("filename", ""), cmd.get("speed", 0.016)) "fade_limit_out_music": fade_limit_out_music(cmd.get("filename", ""), cmd.get("volume", 0.0), cmd.get("speed", 0.016)) "fade_out_all_music": fade_out_all_music() "set_sound_volume": set_sound_volume(cmd.get("volume", 1.0)) "set_music_volume": set_music_volume(cmd.get("volume", 1.0)) _update_music_fades() # --- 音效路径检索与加载 --------------------------------------------------- func _find_sound(rel: String) -> String: if rel.is_empty(): return "" rel = rel.replace("\\", "/").strip_edges() if rel.begins_with("d:/ymir work/") or rel.begins_with("D:/ymir work/"): rel = rel.substr(13) elif rel.begins_with("d:/") or rel.begins_with("D:/"): rel = rel.substr(3) # 补全扩展名候选 var cands := PackedStringArray([rel]) if rel.get_extension().is_empty(): cands = PackedStringArray([rel + ".wav", rel + ".mp3", rel + ".ogg"]) # 40250's CEterPackManager is the authority for sounds, as it is for models and UI images. if ClassDB.class_exists("Metin2Pack") and Metin2Pack.is_ready(): for c in cands: if Metin2Pack.exists(c): return "pack://" + c if not c.begins_with("sound/") and Metin2Pack.exists("sound/" + c): return "pack://sound/" + c if FileAccess.file_exists(rel): return rel # 开发期散文件兜底。 for root in _sound_roots: for c in cands: var p := root.path_join(c) if FileAccess.file_exists(p): return p # 若路径带 sound/,剥掉后再试一次(防止 root 自身已带 sound/) if c.begins_with("sound/"): var p_sub := root.path_join(c.substr(6)) if FileAccess.file_exists(p_sub): return p_sub return "" func _load_stream(path: String) -> AudioStream: if _cache.has(path): return _cache[path] var s: AudioStream = null var ext := path.get_extension().to_lower() var bytes: PackedByteArray if path.begins_with("pack://"): bytes = Metin2Pack.get_bytes(path.substr(7)) else: bytes = FileAccess.get_file_as_bytes(path) if bytes.is_empty(): return null if ext == "mp3": var m := AudioStreamMP3.new() m.data = bytes s = m if m.data.size() > 0 else null elif ext == "ogg": s = AudioStreamOggVorbis.load_from_buffer(bytes) elif ext == "wav": s = AudioStreamWAV.load_from_buffer(bytes) _cache[path] = s return s # --- 2D 音效播放 ----------------------------------------------------------- func play_sound_2d(rel: String, volume_scale := 1.0) -> void: var path := _find_sound(rel) if path.is_empty(): return var stream := _load_stream(path) if stream == null: return if _ui_pool.is_empty(): return var pl := _ui_pool[_ui_next] _ui_next = (_ui_next + 1) % _ui_pool.size() pl.stream = stream # SoundManager already applied the current master volume to this command. var vol: float = clampf(volume_scale, 0.0, 1.0) pl.volume_db = linear_to_db(vol) if vol > 0.001 else -80.0 pl.play() # --- 3D 空间音效播放 ------------------------------------------------------- func play_sound_3d(rel: String, pos: Vector3, volume_scale := 1.0, sound_id := 0, play_count := 1) -> void: var path := _find_sound(rel) if path.is_empty(): return var stream := _load_stream(path) if stream == null: return var pl := AudioStreamPlayer3D.new() pl.stream = stream pl.unit_size = 8.0 pl.max_distance = 80.0 var vol: float = clampf(volume_scale, 0.0, 1.0) pl.volume_db = linear_to_db(vol) if vol > 0.001 else -80.0 add_child(pl) _all_3d.append(pl) pl.global_position = pos pl.play() if sound_id > 0: _active_3d[sound_id] = pl var state := {"remaining": play_count} pl.finished.connect(func(): if state.remaining == 0 or state.remaining > 1: if state.remaining > 1: state.remaining -= 1 pl.play() return _all_3d.erase(pl) if sound_id > 0 and _active_3d.get(sound_id) == pl: _active_3d.erase(sound_id) pl.queue_free()) func set_sound_volume_3d(sound_id: int, volume: float) -> void: var pl: AudioStreamPlayer3D = _active_3d.get(sound_id) if pl != null and is_instance_valid(pl): var level := clampf(volume, 0.0, 1.0) pl.volume_db = linear_to_db(level) if level > 0.001 else -80.0 func stop_sound_3d(sound_id: int) -> void: if _active_3d.has(sound_id): var pl: AudioStreamPlayer3D = _active_3d[sound_id] if is_instance_valid(pl): pl.stop() _all_3d.erase(pl) pl.queue_free() _active_3d.erase(sound_id) func stop_all_sound_3d() -> void: for pl in _all_3d: if is_instance_valid(pl): pl.stop() pl.queue_free() _all_3d.clear() _active_3d.clear() # --- BGM 背景音乐播放 ----------------------------------------------------- func _music_stream(name: String) -> AudioStream: var path := name.replace("\\", "/") if not path.is_absolute_path(): var direct := _bgm_root.path_join(name) if FileAccess.file_exists(direct): path = direct elif FileAccess.file_exists(direct + ".mp3"): path = direct + ".mp3" else: path = _find_sound(name) return _load_stream(path) func _music_slot(name: String) -> int: var normalized := name.replace("\\", "/").to_lower() for i in _bgm_names.size(): if _bgm_names[i].replace("\\", "/").to_lower() == normalized and _bgm[i].playing: return i return -1 func _set_music_volume(slot: int, volume: float) -> void: _bgm_volumes[slot] = clampf(volume, 0.0, 1.0) _bgm[slot].volume_db = linear_to_db(_bgm_volumes[slot]) if _bgm_volumes[slot] > 0.001 else -80.0 func _start_music(slot: int, name: String, volume: float) -> bool: var stream := _music_stream(name) if stream == null: return false if stream is AudioStreamMP3: stream.loop = true _bgm[slot].stop() _bgm[slot].stream = stream _bgm_names[slot] = name _set_music_volume(slot, volume) _bgm[slot].play() return true func play_music(name: String, volume_scale := 1.0) -> void: if name.is_empty(): return # 40250 PlayMusic(name) always replaces stream slot 0. _bgm_fades.erase(0) _start_music(0, name, volume_scale) func fade_in_music(name: String, target_volume := 1.0, speed := 0.016) -> void: if name.is_empty(): return var slot := _music_slot(name) if slot < 0: fade_out_all_music() for i in _bgm.size(): if not _bgm[i].playing: slot = i break if slot < 0 or not _start_music(slot, name, 0.0): return _bgm_fades[slot] = {"target": clampf(target_volume, 0.0, 1.0), "speed": speed, "mode": "in"} func fade_out_music(name: String, speed := 0.016) -> void: var slot := _music_slot(name) if slot >= 0: _bgm_fades[slot] = {"target": 0.0, "speed": speed, "mode": "out"} func fade_limit_out_music(name: String, limit: float, speed := 0.016) -> void: var slot := _music_slot(name) if slot >= 0: _bgm_fades[slot] = {"target": clampf(limit, 0.0, 1.0), "speed": speed, "mode": "limit"} func fade_out_all_music() -> void: for i in _bgm.size(): if _bgm[i].playing: _bgm_fades[i] = {"target": 0.0, "speed": 0.01, "mode": "out"} func _update_music_fades() -> void: # 40250 advances fVolume by fVolumeSpeed once per SoundManager::Update frame. for slot in _bgm_fades.keys(): if not _bgm[slot].playing: _bgm_fades.erase(slot) _bgm_names[slot] = "" continue var fade: Dictionary = _bgm_fades[slot] var target: float = fade.target var step: float = fade.speed var volume: float = _bgm_volumes[slot] match fade.mode: "in": volume = minf(target, volume + step) "limit": volume = maxf(target, volume - step) "out": volume = maxf(0.0, volume - step) _set_music_volume(slot, volume) if (fade.mode == "in" and volume >= target) or (fade.mode != "in" and volume <= target): if fade.mode == "out": _bgm[slot].stop() _bgm_names[slot] = "" _bgm_fades.erase(slot) # --- 音量控制 ------------------------------------------------------------- func set_sound_volume(v: float) -> void: master_sound = clampf(v, 0.0, 1.0) func set_music_volume(v: float) -> void: master_music = clampf(v, 0.0, 1.0) for i in _bgm.size(): if _bgm[i].playing and not (_bgm_fades.has(i) and _bgm_fades[i].mode == "out"): _set_music_volume(i, master_music)