Files
mtgodot-poc/project/audio.gd
T
shenleiandClaude Sonnet 5 9732f2799e 40250 classic: increment 50 (W4) — map BGM selection + background volume save/restore
§9.1 map BGM selection (bgm_director.gd):
- server bgm_changed(name, volume): non-empty name plays it as field music;
  empty name falls back to musicInfo.fieldMusic (METIN2THEMA = "M2BG.mp3")
- volume < 0 (server unspecified, GC_MAIN_CHARACTER3_BGM) -> default
  1/5*0.1 = 0.02; otherwise server volume; volume applied before play_bgm()
  fade-in (matches game.py:190 SetMusicVolume/SetSoundVolume then FadeInMusic)
- connect M2Client.world_reset -> re-run selection on map change
- new musicinfo.gd: METIN2THEMA/loginMusic/createMusic/selectMusic/fieldMusic
  constants + save/load_last_play_field_music -> user://BGM/lastplay.inf

§9.2 background/foreground volume save-restore:
- audio.gd: save_volume()/restore_volume() (backup fields echo reference
  m_fBackupMusicVolume / m_fBackupSoundVolume) + set_music_volume()
- app_lifecycle.gd: _enter_background() uses save_volume() + mute instead of
  stop_bgm(); _exit_background() calls restore_volume(); _bgm_name never cleared

§9.3 lifecycle ownership: DOC ONLY — design confirmed, deferred to Phase 2 / W0
(AppFlow sole lifecycle owner; M2Client drops NOTIFICATION_* keeps suspend/resume).
game_scene.gd / app_flow.gd / m2_client.* left untouched per workflow constraints.

Tests: bgm_test.gd (new) PASS; gamescene_test / netplay_test / p9_test green.
Docs: CLIENT-GAP.md increment 50 bullet + W4 progress row; CLIENT-GAP-FIX.md
§9.1 §9.2 状态/改动/测试/验收, §9.3 deferred note, C.5 batch record.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
2026-09-02 15:31:58 +09:00

196 lines
5.9 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 —— BGM 交叉淡入淡出 + UI 音效池 + 3D 定位音效。
# 对标客户端 AudioLibBGM`bgm/*.mp3`)、UI wav`**/sound/ui/`)、
# 3D wav`**/sound/<rel>`)。纯资产接线,无解码器移植(`.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_fBackupSoundVolumeREF/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/ 同级(<repo>/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)