Files
mtgodot-poc/project/audio_driver.gd
T

359 lines
11 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: 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)