Files
mtgodot-poc/project/audio_driver.gd
T

296 lines
8.7 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 := [null, null] # 两个 AudioStreamPlayer 用于 BGM 交叉淡入淡出
var _bgm_cur := 0
var _bgm_name := ""
var _bgm_fade_tween: Tween = null
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 := [
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:
# 2 个 BGM 播放器
for i in 2:
var pl := AudioStreamPlayer.new()
pl.bus = "Master"
pl.name = "BgmPlayer%d" % i
add_child(pl)
_bgm[i] = pl
# 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()
if commands.is_empty():
return
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))
"stop_sound_3d":
stop_sound_3d(cmd.get("id", 0))
"stop_all_sound_3d":
stop_all_sound_3d()
"play_music":
play_music(cmd.get("filename", ""), cmd.get("volume", 1.0))
"fade_out_music":
fade_out_music(cmd.get("speed", 0.0))
"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))
# --- 音效路径检索与加载 ---------------------------------------------------
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) -> 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
pl.finished.connect(func():
_all_3d.erase(pl)
if sound_id > 0 and _active_3d.get(sound_id) == pl:
_active_3d.erase(sound_id)
pl.queue_free())
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 play_music(name: String, volume_scale := 1.0) -> void:
if name.is_empty() or name == _bgm_name:
return
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)
var stream := _load_stream(path)
if stream == null:
return
_bgm_name = name
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()
var target_vol: float = clampf(volume_scale, 0.0, 1.0)
var target_db: float = linear_to_db(target_vol) if target_vol > 0.001 else -80.0
if _bgm_fade_tween and _bgm_fade_tween.is_valid():
_bgm_fade_tween.kill()
_bgm_fade_tween = create_tween()
_bgm_fade_tween.parallel().tween_property(_bgm[nxt], "volume_db", target_db, 1.2)
var cur_player: AudioStreamPlayer = _bgm[_bgm_cur]
if cur_player and cur_player.playing:
_bgm_fade_tween.parallel().tween_property(cur_player, "volume_db", -40.0, 1.2)
_bgm_fade_tween.chain().tween_callback(cur_player.stop)
_bgm_cur = nxt
func fade_out_music(speed := 0.0) -> void:
_bgm_name = ""
var dur := 1.5 if speed <= 0.0 else maxf(0.5, 3.0 / speed)
var cur_player: AudioStreamPlayer = _bgm[_bgm_cur]
if cur_player and cur_player.playing:
if _bgm_fade_tween and _bgm_fade_tween.is_valid():
_bgm_fade_tween.kill()
_bgm_fade_tween = create_tween()
_bgm_fade_tween.tween_property(cur_player, "volume_db", -50.0, dur)
_bgm_fade_tween.tween_callback(cur_player.stop)
func fade_out_all_music() -> void:
fade_out_music(0.0)
# --- 音量控制 -------------------------------------------------------------
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)
var pl: AudioStreamPlayer = _bgm[_bgm_cur]
if pl and pl.playing:
pl.volume_db = linear_to_db(master_music) if master_music > 0.001 else -80.0