初始提交: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,194 @@
#!/usr/bin/env python3
"""Render bench_autoencoders.py output into one self-contained HTML page.
python make_ae_charts.py bench.json charts.html
Six charts -- VRAM, latency, accuracy, for the encoder and the decoder -- eight lines
each. No external assets: the SVG is inline, so the page works offline.
"""
import json, sys, math, html
if len(sys.argv) < 3:
raise SystemExit("usage: make_ae_charts.py <bench.json> <out.html>")
rows = json.load(open(sys.argv[1]))
OUT = sys.argv[2]
SERIES = [ # (model, prec, chunked) -> label, colour, dash
("same-l", "16", True ), ("same-l", "16", False),
("same-l", "fp8", True ), ("same-l", "fp8", False),
("same-s", "16", True ), ("same-s", "16", False),
("same-s", "fp8", True ), ("same-s", "fp8", False),
]
COLOR = {("same-l","16"):"#e07b39", ("same-l","fp8"):"#a8341b",
("same-s","16"):"#3b9ad9", ("same-s","fp8"):"#1d5b86"}
def label(m,p,c):
pn = {"16":"16-bit","fp8":"fp8"}[p]
return f"{m.upper()} · {pn} · {'chunked' if c else 'single-shot'}"
def series_points(m,p,c,key):
out=[]
for r in rows:
if (r["model"],r["prec"],r["chunked"])==(m,p,c) and key in r:
v=r[key]
if isinstance(v,(int,float)) and v==v and v>0:
out.append((r["L"], v))
return sorted(out)
W,H = 1180, 560
PAD = dict(l=92, r=28, t=26, b=64)
def chart(key, title, ylab, logy, unit, note=""):
data = {s: series_points(*s, key) for s in SERIES}
pts = [v for s in data.values() for _,v in s]
xs = sorted({x for s in data.values() for x,_ in s})
if not pts: return f"<p>no data for {key}</p>"
lo, hi = min(pts), max(pts)
if logy:
lo = max(lo, hi/1e4); ylo, yhi = math.log10(lo*0.85), math.log10(hi*1.18)
ytr = lambda v: math.log10(max(v,lo*0.85))
else:
span=hi-lo or 1; ylo, yhi = lo-span*0.10, hi+span*0.12; ytr = lambda v: v
xlo, xhi = math.log10(min(xs)*0.85), math.log10(max(xs)*1.18)
iw, ih = W-PAD['l']-PAD['r'], H-PAD['t']-PAD['b']
X = lambda L: PAD['l'] + (math.log10(L)-xlo)/(xhi-xlo)*iw
Y = lambda v: PAD['t'] + ih - (ytr(v)-ylo)/(yhi-ylo)*ih
g=[f'<svg viewBox="0 0 {W} {H}" class="chart" role="img" aria-label="{html.escape(title)}">']
# y grid
if logy:
ticks=[]; e=math.floor(ylo)
while e<=math.ceil(yhi):
for mlt in (1,2,5):
v=mlt*10**e
if lo*0.85<=v<=hi*1.18: ticks.append(v)
e+=1
else:
ticks=[ylo+(yhi-ylo)*i/6 for i in range(7)]
for v in ticks:
y=Y(v)
g.append(f'<line x1="{PAD["l"]}" x2="{W-PAD["r"]}" y1="{y:.1f}" y2="{y:.1f}" class="grid"/>')
t=(f"{v:,.0f}" if v>=100 or (not logy and abs(v)>=10) else f"{v:,.2f}".rstrip("0").rstrip("."))
g.append(f'<text x="{PAD["l"]-10}" y="{y+4:.1f}" class="ytick">{t}</text>')
for L in [1,4,16,64,256,1024,4096,8192]:
if not (min(xs)*0.85<=L<=max(xs)*1.18): continue
x=X(L)
g.append(f'<line y1="{PAD["t"]}" y2="{H-PAD["b"]}" x1="{x:.1f}" x2="{x:.1f}" class="grid"/>')
g.append(f'<text x="{x:.1f}" y="{H-PAD["b"]+20}" class="xtick">{L}</text>')
g.append(f'<text x="{W/2}" y="{H-PAD["b"]+46}" class="axlab">latent length L '
f'&nbsp;(L×4096 samples ≈ L×0.093 s)</text>')
g.append(f'<text transform="translate(22,{PAD["t"]+ih/2}) rotate(-90)" class="axlab">'
f'{html.escape(ylab)}</text>')
for s in SERIES:
p=data[s]
if not p: continue
d=" ".join(("M" if i==0 else "L")+f"{X(L):.1f},{Y(v):.1f}" for i,(L,v) in enumerate(p))
col=COLOR[(s[0],s[1])]; dash='6 4' if s[2] else 'none'
g.append(f'<path d="{d}" fill="none" stroke="{col}" stroke-width="2.4" '
f'stroke-dasharray="{dash}" stroke-linejoin="round"/>')
for L,v in p:
g.append(f'<circle cx="{X(L):.1f}" cy="{Y(v):.1f}" r="2.6" fill="{col}">'
f'<title>{label(*s)} — L={L}: {v:,.2f} {unit}</title></circle>')
g.append("</svg>")
leg="".join(
f'<span class="key"><svg width="30" height="10"><line x1="1" y1="5" x2="29" y2="5" '
f'stroke="{COLOR[(s[0],s[1])]}" stroke-width="2.6" '
f'stroke-dasharray="{"6 4" if s[2] else "none"}"/></svg>{html.escape(label(*s))}</span>'
for s in SERIES)
return (f'<section><h2>{html.escape(title)}</h2>'
+ (f'<p class="note">{note}</p>' if note else "")
+ "".join(g) + f'<div class="legend">{leg}</div></section>')
CHARTS = [
("enc", "enc_scratch_mb", "Encoder — VRAM scratch reserved", "scratch (MB, log)", True, "MB",
"TensorRT commits a context's scratch from its PROFILE CEILING, not from the shape you bind — "
"so these are flat in L. That is the whole point of the low band. SAME-L's two precisions "
"reserve identical scratch, so those lines overlap exactly."),
("enc", "enc_ms", "Encoder — latency", "milliseconds (log)", True, "ms",
"Best of three, after warm-up. Single-shot lines stop at L=4096: that is the wide band's "
"ceiling. Chunked keeps going because it windows."),
("enc", "enc_fix_db", "Encoder — accuracy vs the original music, content held fixed",
"reconstruction SNR (dB)", False, "dB",
"The SAME 190 s of real music at every point — reassembled from L-sized blocks, so only the "
"processing length changes. This is the apples-to-apples question the TFLite rung table asks, "
"and the answer is the same: flat. From L=16 up the whole curve moves 0.002 dB on SAME-L and "
"0.020 dB on SAME-S. The fall below L≈16 is real but is not a seam: 0.09 s blocks encoded "
"independently lose context at every boundary."),
("dec", "dec_scratch_mb", "Decoder — VRAM scratch reserved", "scratch (MB, log)", True, "MB", ""),
("dec", "dec_ms", "Decoder — latency", "milliseconds (log)", True, "ms", ""),
("dec", "dec_fix_db", "Decoder — accuracy vs the original music, content held fixed",
"reconstruction SNR (dB)", False, "dB",
"Same construction, decoder varying. Chunked and single-shot land on the same value to three "
"decimals at every length, which is the direct evidence that the windowed overlap is exact."),
]
body = "".join(chart(k, t, y, ly, u, n) for _, k, t, y, ly, u, n in CHARTS)
n_ok = sum(1 for r in rows if "enc_ms" in r)
n_len = len({r['L'] for r in rows})
CSS = """
:root{--bg:#faf8f5;--fg:#1c1a17;--mut:#6b645c;--line:#e0dbd3;--card:#fff;--accent:#a8341b}
:root:not([data-theme="light"]){}
@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){
--bg:#14120f;--fg:#ece7df;--mut:#9a9086;--line:#2e2a25;--card:#1b1815;--accent:#e07b39}}
:root[data-theme="dark"]{--bg:#14120f;--fg:#ece7df;--mut:#9a9086;--line:#2e2a25;
--card:#1b1815;--accent:#e07b39}
*{box-sizing:border-box}
body{margin:0;padding:38px 22px 90px;background:var(--bg);color:var(--fg);
font:15px/1.6 ui-sans-serif,-apple-system,"Segoe UI",Roboto,sans-serif}
main{max-width:1240px;margin:0 auto}
h1{font-size:1.75rem;margin:0 0 6px;letter-spacing:-.02em;text-wrap:balance}
.sub{color:var(--mut);margin:0 0 30px;max-width:78ch}
section{background:var(--card);border:1px solid var(--line);border-radius:10px;
padding:20px 22px 16px;margin:0 0 26px;overflow-x:auto}
h2{font-size:1.06rem;margin:0 0 4px;letter-spacing:-.01em}
.note{color:var(--mut);font-size:.86rem;margin:0 0 12px;max-width:92ch}
.chart{width:100%;height:auto;min-width:820px;display:block}
.grid{stroke:var(--line);stroke-width:1}
.ytick,.xtick{fill:var(--mut);font-size:12px;font-variant-numeric:tabular-nums}
.ytick{text-anchor:end}.xtick{text-anchor:middle}
.axlab{fill:var(--mut);font-size:12.5px;text-anchor:middle}
.legend{display:flex;flex-wrap:wrap;gap:6px 18px;margin-top:12px;
border-top:1px solid var(--line);padding-top:12px}
.key{display:flex;align-items:center;gap:7px;font-size:.83rem;color:var(--mut)}
footer{color:var(--mut);font-size:.85rem;max-width:88ch;margin-top:34px}
code{background:rgba(128,128,128,.14);padding:1px 5px;border-radius:4px;font-size:.9em}
"""
open(OUT, "w").write(f"""<meta charset="utf-8">
<title>SAME Autoencoder Sweep</title>
<style>{CSS}</style>
<main>
<h1>SAME autoencoder: VRAM, latency, accuracy</h1>
<p class="sub">Eight configurations — {{SAME-S, SAME-L}} × {{16-bit, fp8}} × {{chunked,
single-shot}} — measured across {n_len} latent lengths from
L=1 to L=8192 ({n_ok} points). Accuracy is a full round trip against <em>real music</em> — seven
mastered tracks from the 806k-track corpus (rock, jazz, classical, electronic, hip hop, piano,
folk) concatenated to 762&nbsp;s, because L=8192 is longer than any single song and tiling one
track would be artificially self-similar. Never noise: the autoencoder is content-dependent and
noise fabricates numbers that do not hold on music. 16-bit means fp16 on SAME-L and bf16 on
SAME-S. Hover any point for its value.</p>
{body}
<footer>
<p><strong>How accuracy is attributed.</strong> A round trip is a property of the encoder and
decoder together, so to read one stage the other is pinned to that model's 16-bit chunked
engine. The encoder chart varies the encoder; the decoder chart varies the decoder.</p>
<p><strong>Read these as comparisons, not verdicts.</strong> Compare lines to each other; do
not read the absolute height as a quality statement, and do not let any of it substitute for
listening.</p>
<p><strong>It is not the limiter.</strong> The decoders bake a 0.977 ceiling and real masters
here peak at 1.019, so level clipping was the obvious suspect. It is not: re-measured with the
input scaled from 0.9 down to 0.225 the SNR moves by 0.03&nbsp;dB, and an optimal scalar
gain-match recovers only 0.27&nbsp;dB.</p>
<p><strong>Why accuracy is plotted with content held fixed.</strong> The first version of
these charts measured one excerpt per length — the first L latents of the stream — and the lines
bounced by several dB, which looked like a length-dependent defect. It was not. At a fixed L,
moving the excerpt swings SNR by <strong>9.4&ndash;15.0&nbsp;dB</strong>, while the entire L axis
at a fixed offset moved only <strong>2.95&nbsp;dB</strong>: a longer L is simply a different,
longer piece of music, so one-excerpt-per-L plots the music rather than the model. The charts
above therefore hold the audio identical at every point and vary only the processing length —
the same thing the TFLite rung table does, and it comes out just as flat.</p>
<p><strong>The absolute level.</strong> Round-trip SNR on this real-music set is 4.2&ndash;4.9 dB
for SAME-L and 3.0&ndash;3.9 dB for SAME-S. Measured on our own generated audio the same pipeline
scores ~16 dB, and published figures for this family span roughly &minus;23 dB (solo piano) to
&minus;4 dB (dense circus). Waveform SNR systematically understates a perceptual autoencoder.</p>
<p><strong>SAME-S draw-to-draw noise.</strong> The shipped SAME-S decoder has a
<code>RandomNormalLike</code> at its bottleneck, so its round trip carries run-to-run variance
that belongs to neither precision nor chunking.</p>
</footer>
</main>""")
print(f" wrote {OUT} ({len(rows)} rows)")