初始提交:3 款纯音频游戏 + 共享引擎 + 完整文档与资产管线

- 共享引擎(app/src/main):GestureEngine 6 手势识别、AudioEngine 三层音频
  (ExoPlayer BGM 交叉淡入 / 对白 ducking / SoundPool 低延迟)、Game/GameEngine
  状态机接口、MainActivity 全屏封面 + TalkBack
- 《声命线》storyline:末世互动广播剧,3 幕 8 抉择点 4 结局
- 《声东击西》reflex:听声辨位反应,12 波战斗 + BGM 三档 BPM 变速
- 《暗室》escape:3x3 网格密室逃脱,钥匙→铁柜→保险丝→电箱→大门谜题链
- 文档:setup(从零搭建)/ architecture(架构)/ engine-api(引擎 API)/
  asset-pipeline(资产流水线)/ games/*(三款游戏设计文档)
- 资产管线:Qwen3-TTS 对白 + 程序合成音效/BGM + Mureka BGM 的 gen 脚本与台账,
  全部 ogg 资产入库可复现
- env.d 环境脚本:android/tts 入库;sfx/mureka 含密钥不入库,仅提交 .example.sh 模板
This commit is contained in:
SoundGame Dev
2026-08-29 13:00:34 +08:00
commit f0f0a9b50e
446 changed files with 2627836 additions and 0 deletions
@@ -0,0 +1,200 @@
#!/usr/bin/env python3
"""Benchmark — wall time + peak RAM for SA3 across model sizes and clip lengths.
Runs each (DiT, decoder, seconds) cell as a fresh ./sa3 subprocess so peak
RAM is isolated per run. Weights are pre-warmed via ensure_local() before
timing starts, so HF download time is NOT charged against the benchmark.
Usage:
uv run --no-project python scripts/benchmark.py
# or
./sa3 --help # see the regular CLI; this file is separate
"""
from __future__ import annotations
import os
import platform
import re
import subprocess
import sys
import tempfile
import time
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
PROJECT_DIR = SCRIPT_DIR.parent
sys.path.insert(0, str(SCRIPT_DIR))
from weights import ensure_local # noqa: E402
# ── what to benchmark ──────────────────────────────────────────────────────
CONFIGS = [
("sm-music", "same-s"),
("medium", "same-l"),
]
SECONDS = [5, 30, 120, 380]
PROMPT = "Impending tribal, epic orchestral buildup"
SEED = 42
# Weights we need on disk before benchmarks start. T5Gemma is shared.
PREFLIGHT_WEIGHTS = [
"models/mlx/t5gemma_f16.npz",
"models/mlx/dit_sm-music_f16.npz",
"models/mlx/dit_medium_f16.npz",
"models/mlx/same_s_decoder_f32.npz",
"models/mlx/same_l_decoder_f32.npz",
]
# ── hardware info ──────────────────────────────────────────────────────────
def hwinfo() -> str:
try:
chip = subprocess.check_output(
["sysctl", "-n", "machdep.cpu.brand_string"], text=True
).strip()
except Exception:
chip = platform.processor() or platform.machine() or "unknown"
try:
mem_bytes = int(subprocess.check_output(
["sysctl", "-n", "hw.memsize"], text=True).strip())
mem_gb = f"{mem_bytes / (1024**3):.0f} GB"
except Exception:
mem_gb = "?"
return f"{chip} · {mem_gb} RAM"
# ── output parsing ─────────────────────────────────────────────────────────
_ANSI = re.compile(r"\x1b\[[0-9;]*m")
_DONE = re.compile(
r"done\s+([\d.]+)s\s+wall\s+→\s+([\d.]+)s\s+audio\s+→\s+"
r"([\d.]+)×\s+realtime\s+peak\s+RAM\s+([\d.]+)\s+(MB|GB)"
)
def parse_done(stdout: str) -> dict | None:
plain = _ANSI.sub("", stdout)
m = _DONE.search(plain)
if not m:
return None
return {
"wall_s": float(m.group(1)),
"audio_s": float(m.group(2)),
"realtime": float(m.group(3)),
"peak_ram": f"{m.group(4)} {m.group(5)}",
}
# ── single benchmark cell ──────────────────────────────────────────────────
def run_one(dit: str, decoder: str, seconds: int) -> dict | None:
out = Path(tempfile.gettempdir()) / f"bench_{dit}_{decoder}_{seconds}s.wav"
cmd = [
str(PROJECT_DIR / "sa3"),
"--prompt", PROMPT,
"--dit", dit,
"--decoder", decoder,
"--seconds", str(seconds),
"--seed", str(SEED),
"--out", str(out),
]
label = f" {dit:9s} {decoder:7s} {seconds:>4}s"
print(f"{label} → running …", flush=True)
t0 = time.time()
try:
proc = subprocess.run(
cmd, capture_output=True, text=True, cwd=str(PROJECT_DIR),
timeout=20 * 60, # 20-minute hard cap per cell
)
except subprocess.TimeoutExpired:
print(f"{label} → TIMEOUT after {time.time()-t0:.0f}s")
return None
elapsed = time.time() - t0
out.unlink(missing_ok=True)
if proc.returncode != 0:
last_err = (proc.stderr.strip().split("\n") or ["(no stderr)"])[-1][:120]
print(f"{label} → FAILED exit={proc.returncode} {last_err}")
return None
parsed = parse_done(proc.stdout)
if not parsed:
print(f"{label} → FAILED (could not parse 'done' line after {elapsed:.0f}s)")
return None
print(
f"{label} → {parsed['wall_s']:7.2f}s wall "
f"{parsed['realtime']:5.2f}× RT peak {parsed['peak_ram']}"
)
return {"dit": dit, "decoder": decoder, "seconds": seconds, **parsed}
# ── ASCII table ────────────────────────────────────────────────────────────
def render_table(rows: list[dict]) -> str:
headers = ["model", "decoder", "seconds", "wall (s)", "×realtime", "peak RAM"]
widths = [9, 7, 7, 9, 9, 10]
def hline(l, m, r):
return l + m.join("─" * (w + 2) for w in widths) + r
def row(cells):
parts = []
for i, c in enumerate(cells):
# right-align numeric columns (idx 2-5)
parts.append(c.rjust(widths[i]) if i >= 2 else c.ljust(widths[i]))
return "│ " + " │ ".join(parts) + " │"
lines = [hline("┌", "┬", "┐"), row(headers), hline("├", "┼", "┤")]
prev_dit = None
for r in rows:
# Visual separator when the model changes
if prev_dit is not None and r["dit"] != prev_dit:
lines.append(hline("├", "┼", "┤"))
prev_dit = r["dit"]
lines.append(row([
r["dit"], r["decoder"], str(r["seconds"]),
f"{r['wall_s']:.2f}",
f"{r['realtime']:.2f}×",
r["peak_ram"],
]))
lines.append(hline("└", "┴", "┘"))
return "\n".join(lines)
# ── main ───────────────────────────────────────────────────────────────────
def main() -> int:
print()
print("━" * 72)
print(" Benchmark — Stable Audio 3 (MLX) on Apple Silicon")
print("━" * 72)
print(f" Hardware: {hwinfo()}")
print(f" Prompt: {PROMPT!r}")
print(f" Seed: {SEED}")
print(f" Cells: {len(CONFIGS)} configs × {len(SECONDS)} durations = "
f"{len(CONFIGS) * len(SECONDS)} runs")
print()
print("→ Preflight: ensuring all weights are local (downloads NOT timed)")
for rel in PREFLIGHT_WEIGHTS:
ensure_local(rel)
print(" done.\n")
print("→ Running benchmarks (each subprocess is isolated for clean peak-RAM)")
rows: list[dict] = []
for dit, decoder in CONFIGS:
for s in SECONDS:
r = run_one(dit, decoder, s)
if r:
rows.append(r)
print()
if not rows:
print("No successful runs — nothing to tabulate.")
return 1
print("━" * 72)
print(f" Results ({len(rows)}/{len(CONFIGS)*len(SECONDS)} cells succeeded)")
print(f" Hardware: {hwinfo()}")
print("━" * 72)
print()
print(render_table(rows))
print()
return 0
if __name__ == "__main__":
sys.exit(main())