port: complete python ledger mapping, audio commands bridge and platform stubs
- Added Client/root (74 modules, 3350 funcs) mapped to RUN_AS_IS - Added UserInterface/PythonNetworkStreamModule.cpp.json mapped to PORTED - Implemented AudioCommands queue bridging MilesLib SoundManager to Godot AudioDriver - Implemented AudioDriver node for BGM crossfading, 2D UI SFX and 3D spatial sounds - Implemented core platform stubs in PythonApplication, GrpScreen, and PythonGraphic - Passed all 12 port tests (port_gate macos) and python_game_render_test
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
# 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
|
||||
var _active_3d := {}
|
||||
|
||||
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)
|
||||
|
||||
# 1. 尝试直接路径
|
||||
if FileAccess.file_exists(rel):
|
||||
return rel
|
||||
|
||||
# 2. 补全扩展名候选
|
||||
var cands := PackedStringArray([rel])
|
||||
if rel.get_extension().is_empty():
|
||||
cands = PackedStringArray([rel + ".wav", rel + ".mp3", rel + ".ogg"])
|
||||
|
||||
# 3. 在所有已知根目录中遍历查找
|
||||
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()
|
||||
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
|
||||
|
||||
# --- 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
|
||||
var vol: float = clampf(master_sound * 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(master_sound * volume_scale, 0.0, 1.0)
|
||||
pl.volume_db = linear_to_db(vol) if vol > 0.001 else -80.0
|
||||
add_child(pl)
|
||||
pl.global_position = pos
|
||||
pl.play()
|
||||
if sound_id > 0:
|
||||
_active_3d[sound_id] = pl
|
||||
pl.finished.connect(func(): _active_3d.erase(sound_id); pl.queue_free())
|
||||
else:
|
||||
pl.finished.connect(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()
|
||||
pl.queue_free()
|
||||
_active_3d.erase(sound_id)
|
||||
|
||||
func stop_all_sound_3d() -> void:
|
||||
for id in _active_3d.keys():
|
||||
stop_sound_3d(id)
|
||||
_active_3d.clear()
|
||||
|
||||
# --- BGM 背景音乐播放 -----------------------------------------------------
|
||||
|
||||
func play_music(name: String, volume_scale := 1.0) -> void:
|
||||
if name.is_empty() or name == _bgm_name:
|
||||
return
|
||||
_bgm_name = name
|
||||
|
||||
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
|
||||
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(master_music * 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
|
||||
@@ -45,6 +45,9 @@ func _start_port_route() -> void:
|
||||
var world: Node3D = preload("res://python_3d_surface.gd").new()
|
||||
world.name = "Python3DSurface"
|
||||
add_child(world)
|
||||
var audio: Node = preload("res://audio_driver.gd").new()
|
||||
audio.name = "AudioDriver"
|
||||
add_child(audio)
|
||||
var err: String = surface.run_app()
|
||||
if err != "":
|
||||
printerr("port: " + err)
|
||||
|
||||
Reference in New Issue
Block a user