feat(m76-d): four listenable mixdowns for the owner audio gate

This commit is contained in:
2026-09-14 21:22:18 +08:00
parent 313a5a46b4
commit 180bbb1e2f
8 changed files with 338 additions and 0 deletions
+1
View File
@@ -12,6 +12,7 @@
| 2026-09-13 | M60 可玩性真相(W1) | `e1cad068`/`dac5de40` | `m60-playthrough` `20260913T145904Z-38281` | runtime_verified | 34 步 0 阻断;视频与逐步截图 |
| 2026-09-13 | M61 设计/密度(W2) | `310d2620` | `m61-five-profile` `20260913T162625Z-2172` | simulation_verified | decisions=330, max_gap=31.3s, 反支配/存档/可达通过 |
| 2026-09-13 | M62 组件统一(W3) | `2e5540ed` | `m62-ink-baselines` `20260913T164335Z-38711` | runtime_verified | 组件库+矩阵+探针接入;基线 Guild 0 / Contract 1 / Readiness+Result 6 / Market 严格 0 |
| 2026-09-14 | M76-D 整机试听带(4 段合成试听) | `feat(m76-d)` | runtime_pending(Owner 听感)| guild/market/risk/result 四段;音乐床 + 真实音效时序 |
| 2026-09-14 | M76-C 音频接线(服务/总线/事件/音乐族)+ 卡内模块点击缺陷 | `feat(m76-c)`/`fix(m76-c)` `b0db3b46` | `m76-audio` `20260914T131826Z-19299` | runtime_verified(待整机听感)| 144 事件、16 总线;全流程 34 步 0 阻断且音频门通过;修复市场卡无法点击 |
| 2026-09-14 | M76-B SFX 全切片(144 事件 / 158 条)+ 音乐 7 床 | `feat(m76-b)` | machine_verified(待接线与听感)| 校验 0 错误;8 条静音失败件重生成通过;花费累计 ≈ $1.7 |
| 2026-09-14 | M76-A 音频试听批(ElevenLabs;查明旧音频为本地合成占位) | `feat(m76-a)` `4efe4d2c`/`7f7ec58c` | runtime_pending(待人工听感)| 音乐 7 床 + SFX 23 候选;机器校验 0 错误;花费 ≈ $1.35(音乐 $0.15/min、音效 $0.12/min)|
Binary file not shown.
Binary file not shown.
+182
View File
@@ -0,0 +1,182 @@
{
"guild_page": {
"title": "公会页(音乐 + 检视/焦点/整队/整队完成)",
"music": "MUS-G01-GUILD-base.mp3",
"length": 26,
"cues": [
[
1.5,
"SFX-UI-005-page_open.v01.mp3"
],
[
3.4,
"SFX-UI-001-focus_move.v01.mp3"
],
[
5.0,
"SFX-UI-007-inspect_open.v01.mp3"
],
[
6.6,
"SFX-UI-008-inspect_close.v01.mp3"
],
[
8.4,
"SFX-RUN-015-save_begin.v01.mp3"
],
[
10.0,
"SFX-RUN-016-save_commit.v01.mp3"
],
[
12.5,
"SFX-UI-001-focus_move.v01.mp3"
],
[
14.0,
"SFX-ORD-006-reorder_commit.v01.mp3"
],
[
16.5,
"SFX-ORD-017-ready_check_pass.v01.mp3"
],
[
19.0,
"SFX-UI-018-confirm_commit.v01.mp3"
]
]
},
"market_transaction": {
"title": "市场成交(音乐 + 选卡/检视/成交/扣币)",
"music": "MUS-G02-SHOP-base.mp3",
"length": 24,
"cues": [
[
1.2,
"SFX-UI-005-page_open.v01.mp3"
],
[
2.6,
"SFX-SHP-002-offer_reveal.v01.mp3"
],
[
4.6,
"SFX-SHP-012-recruit_card_reveal.v01.mp3"
],
[
6.6,
"SFX-SHP-013-recruit_inspect.v01.mp3"
],
[
8.8,
"SFX-SHP-014-recruit_hire_review.v01.mp3"
],
[
11.0,
"SFX-SHP-015-recruit_hire_commit.v01.mp3"
],
[
13.4,
"SFX-SHP-011-currency_spend.v01.mp3"
],
[
16.0,
"SFX-SHP-021-trait_benefit_reveal.v01.mp3"
],
[
18.4,
"SFX-EQP-007-equip_commit.v01.mp3"
],
[
20.6,
"SFX-SHP-024-shop_exit.v01.mp3"
]
]
},
"risk_and_departure": {
"title": "风险与开战(音乐 + 选风险/落章/确认/出发)",
"music": "MUS-G04-RUN-base.mp3",
"length": 24,
"cues": [
[
1.2,
"SFX-UI-005-page_open.v01.mp3"
],
[
3.0,
"SFX-RUN-001-risk_option_focus.v01.mp3"
],
[
5.0,
"SFX-RUN-002-risk_compare.v01.mp3"
],
[
7.2,
"SFX-RUN-003-risk_commit.v01.mp3"
],
[
9.8,
"SFX-UI-017-confirm_arm.v01.mp3"
],
[
12.0,
"SFX-UI-018-confirm_commit.v01.mp3"
],
[
14.6,
"SFX-ORD-019-battle_start_arm.v01.mp3"
],
[
17.0,
"SFX-ORD-020-battle_start_commit.v01.mp3"
],
[
19.4,
"SFX-RUN-004-encounter_enter.v01.mp3"
]
]
},
"result_win": {
"title": "结算·胜(音乐 + 胜利/奖励/升级)",
"music": "MUS-G05-RESULT-WIN-base.mp3",
"length": 24,
"cues": [
[
1.2,
"SFX-UI-005-page_open.v01.mp3"
],
[
2.8,
"SFX-RES-012-battle_victory.v01.mp3"
],
[
5.4,
"SFX-RES-016-reward_reveal.v01.mp3"
],
[
7.8,
"SFX-RES-017-reward_option_focus.v01.mp3"
],
[
10.0,
"SFX-RES-019-reward_accept.v01.mp3"
],
[
12.4,
"SFX-RES-022-currency_reward.v01.mp3"
],
[
14.8,
"SFX-RES-025-experience_gain.v01.mp3"
],
[
17.2,
"SFX-RES-026-level_gain.v01.mp3"
],
[
20.0,
"SFX-UI-018-confirm_commit.v01.mp3"
]
]
}
}
Binary file not shown.
Binary file not shown.
+10
View File
@@ -58,6 +58,16 @@
- **接线暴露并修复真缺陷**:市场招募卡的 `IdentityModule` 等模块根节点默认 `MOUSE_FILTER_STOP`,**吃掉卡片点击 → 新开局无法选卡/招募**(全流程驱动此前在 hire 步骤阻断)。已修并写入共享 UI 规则。
- **BAT 60 条与区域/BOSS 音乐**仍随战斗里程碑(清单里显式列为 deferred)。
## M76-D 门(2026-09-14,待 Owner 听感)
- 机器检查:`validate_audio.py`(158 SFX + 7 音乐)0 错误;严格运行时诊断 0 泄漏(修复了
音乐交叉淡化 Tween 与 Dummy 驱动下的流播放泄漏;playthrough 关掉遗留音调适配器)。
- 运行时证据:`m76-audio` attempt(13 markers 各一次)——音频服务单测、DEV-2 入口音频断言、
34 步全流程 0 阻断 + 音频门(families=10 / page_open=35 / events=55 / switches=10)、八组墨迹探针。
- 整机试听带:`docs/audio/mixdowns/{guild_page,market_transaction,risk_and_departure,result_win}.mp3`
(音乐床 + 该页真实派发的音效时序;合成试听,不等价于设备播放证据)。
- **人工听感 = Owner**:音箱与耳机分别听;结论记录后本里程碑收口。
## Acceptance checks
- 试听批:Owner 听过并认可方向(或给出反例修订)
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""Compose listenable mixdowns of the runtime audio (music bed + page cue timing).
The mixdown approximates what a page sounds like in the running game: the music
family bed under the semantic cues that page actually dispatches (see
`runtime/entry/entry_shell.gd` wiring). It is a listening aid for the Owner gate,
not runtime evidence of device playback.
"""
from __future__ import annotations
import json
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
AUDIO = ROOT / "runtime/assets/audio"
SFX = AUDIO / "sfx"
MUSIC = AUDIO / "music"
OUT = ROOT / "docs/audio/mixdowns"
MUSIC_GAIN_DB = -13.0
SFX_GAIN_DB = -1.0
MIXDOWNS: dict[str, dict] = {
"guild_page": {
"title": "公会页(音乐 + 检视/焦点/整队/整队完成)",
"music": "MUS-G01-GUILD-base.mp3",
"length": 26,
"cues": [
(1.5, "SFX-UI-005-page_open.v01.mp3"),
(3.4, "SFX-UI-001-focus_move.v01.mp3"),
(5.0, "SFX-UI-007-inspect_open.v01.mp3"),
(6.6, "SFX-UI-008-inspect_close.v01.mp3"),
(8.4, "SFX-RUN-015-save_begin.v01.mp3"),
(10.0, "SFX-RUN-016-save_commit.v01.mp3"),
(12.5, "SFX-UI-001-focus_move.v01.mp3"),
(14.0, "SFX-ORD-006-reorder_commit.v01.mp3"),
(16.5, "SFX-ORD-017-ready_check_pass.v01.mp3"),
(19.0, "SFX-UI-018-confirm_commit.v01.mp3"),
],
},
"market_transaction": {
"title": "市场成交(音乐 + 选卡/检视/成交/扣币)",
"music": "MUS-G02-SHOP-base.mp3",
"length": 24,
"cues": [
(1.2, "SFX-UI-005-page_open.v01.mp3"),
(2.6, "SFX-SHP-002-offer_reveal.v01.mp3"),
(4.6, "SFX-SHP-012-recruit_card_reveal.v01.mp3"),
(6.6, "SFX-SHP-013-recruit_inspect.v01.mp3"),
(8.8, "SFX-SHP-014-recruit_hire_review.v01.mp3"),
(11.0, "SFX-SHP-015-recruit_hire_commit.v01.mp3"),
(13.4, "SFX-SHP-011-currency_spend.v01.mp3"),
(16.0, "SFX-SHP-021-trait_benefit_reveal.v01.mp3"),
(18.4, "SFX-EQP-007-equip_commit.v01.mp3"),
(20.6, "SFX-SHP-024-shop_exit.v01.mp3"),
],
},
"risk_and_departure": {
"title": "风险与开战(音乐 + 选风险/落章/确认/出发)",
"music": "MUS-G04-RUN-base.mp3",
"length": 24,
"cues": [
(1.2, "SFX-UI-005-page_open.v01.mp3"),
(3.0, "SFX-RUN-001-risk_option_focus.v01.mp3"),
(5.0, "SFX-RUN-002-risk_compare.v01.mp3"),
(7.2, "SFX-RUN-003-risk_commit.v01.mp3"),
(9.8, "SFX-UI-017-confirm_arm.v01.mp3"),
(12.0, "SFX-UI-018-confirm_commit.v01.mp3"),
(14.6, "SFX-ORD-019-battle_start_arm.v01.mp3"),
(17.0, "SFX-ORD-020-battle_start_commit.v01.mp3"),
(19.4, "SFX-RUN-004-encounter_enter.v01.mp3"),
],
},
"result_win": {
"title": "结算·胜(音乐 + 胜利/奖励/升级)",
"music": "MUS-G05-RESULT-WIN-base.mp3",
"length": 24,
"cues": [
(1.2, "SFX-UI-005-page_open.v01.mp3"),
(2.8, "SFX-RES-012-battle_victory.v01.mp3"),
(5.4, "SFX-RES-016-reward_reveal.v01.mp3"),
(7.8, "SFX-RES-017-reward_option_focus.v01.mp3"),
(10.0, "SFX-RES-019-reward_accept.v01.mp3"),
(12.4, "SFX-RES-022-currency_reward.v01.mp3"),
(14.8, "SFX-RES-025-experience_gain.v01.mp3"),
(17.2, "SFX-RES-026-level_gain.v01.mp3"),
(20.0, "SFX-UI-018-confirm_commit.v01.mp3"),
],
},
}
def find_sfx(prefix: str) -> Path | None:
matches = sorted(SFX.glob(prefix))
return matches[0] if matches else None
def build(slug: str, spec: dict) -> bool:
music = MUSIC / str(spec["music"])
if not music.exists():
print(f"SKIP {slug}: music missing {music.name}")
return False
inputs: list[str] = ["-i", str(music)]
filters: list[str] = [
f"[0:a]volume={MUSIC_GAIN_DB}dB,atrim=0:{spec['length']},afade=t=out:st={max(0, spec['length'] - 2)}:d=2[music]"
]
labels = ["[music]"]
index = 1
for at, filename in spec["cues"]:
path = find_sfx(filename)
if path is None:
print(f" cue missing: {filename}")
continue
inputs += ["-i", str(path)]
delay = int(float(at) * 1000)
filters.append(f"[{index}:a]volume={SFX_GAIN_DB}dB,adelay={delay}:all=1[cue{index}]")
labels.append(f"[cue{index}]")
index += 1
filters.append(
"".join(labels) + f"amix=inputs={len(labels)}:normalize=0:dropout_transition=0,alimiter=limit=0.95[out]"
)
OUT.mkdir(parents=True, exist_ok=True)
target = OUT / f"{slug}.mp3"
subprocess.run(
["ffmpeg", "-y", "-v", "error", *inputs, "-filter_complex", ";".join(filters),
"-map", "[out]", "-c:a", "libmp3lame", "-q:a", "2", str(target)],
check=True,
)
print(f"OK {slug} -> {target.relative_to(ROOT)} ({target.stat().st_size} bytes)")
return True
def main() -> int:
built = 0
for slug, spec in MIXDOWNS.items():
if build(slug, spec):
built += 1
print(f"mixdowns built: {built}/{len(MIXDOWNS)}")
(OUT / "mixdown_manifest.json").write_text(json.dumps(MIXDOWNS, ensure_ascii=False, indent=1))
return 0
if __name__ == "__main__":
raise SystemExit(main())