初始提交: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,156 @@
#!/usr/bin/env python3
"""Splice the limiter subgraph into a SAME-S / SAME-L decoder ONNX.
Both decoders end in the same six nodes:
... -> Cast(FLOAT) -> /Clip(-1,+1) -> /Mul(32767) -> /Clip_1(+-32767) -> Cast(int32)
-> Transpose -> pcm
The graft removes `/Clip` — which is what currently destroys the overshoot before anyone can
see it — and puts the limiter in its place, so the engine emits already-limited int32 PCM and
every caller gets it for free. `/Clip_1` stays as the belt-and-braces int16 bound.
The limiter nodes come from limiter.onnx, exported from the torch reference, so the semantics
are the reference implementation's rather than a hand-rebuild.
A second mode, `--noclip`, removes `/Clip` and inserts nothing. That engine emits the decoder's
RAW float scaled to int32 — no clipping, no limiting — which is the only way to verify the
limiter exactly inside the real engine rather than against a differently-numbered decoder.
usage: graft_limiter.py <in.onnx> <out.onnx> [--noclip]
"""
import os
import sys
import onnx
from onnx import helper
LIM = "limiter.onnx"
PREFIX = "lim/"
def main():
src, dst = sys.argv[1], sys.argv[2]
noclip = "--noclip" in sys.argv[3:]
print(f"loading {src} ...", flush=True)
m = onnx.load(src)
g = m.graph
lim = onnx.load(LIM).graph
clip = [n for n in g.node if n.op_type == "Clip" and n.name.endswith("/Clip")]
assert len(clip) == 1, f"expected exactly one /Clip, found {[n.name for n in clip]}"
clip = clip[0]
pre, post = clip.input[0], clip.output[0]
consumers = [(n, i) for n in g.node for i, t in enumerate(n.input) if t == post]
assert len(consumers) == 1 and consumers[0][0].op_type == "Mul", \
f"/Clip output goes somewhere unexpected: {[(n.op_type, n.name) for n, _ in consumers]}"
mul = consumers[0][0]
print(f" splice point: {pre} -> [{'nothing (--noclip)' if noclip else 'limiter'}] "
f"-> {mul.name}")
if noclip:
# Emit the decoder's RAW FLOAT instead of int32 PCM: drop Mul/Clip_1/Cast and hand the
# pre-clip tensor straight to the final Transpose. Going via int32 would quantise the
# reference to a 1/32767 grid before the limiter saw it, which shifts the gain by ~1 LSB
# and muddies an exactness check that should be clean.
tr = [n for n in g.node if n.op_type == "Transpose"
and n.output[0] == g.output[0].name][0]
# walk back from the Transpose to the splice point, dropping exactly the tail.
# Stopping on op_type is not enough — the node that PRODUCES `pre` is itself a Cast,
# so the walk would step straight past the splice and delete it.
drop = []
cur = tr.input[0]
while cur != pre:
n = [x for x in g.node if cur in x.output]
assert n, f"walked off the graph at {cur}"
drop.append(n[0])
cur = n[0].input[0]
assert len(drop) < 8, f"tail longer than expected: {[x.op_type for x in drop]}"
for n in drop:
g.node.remove(n)
tr.input[0] = pre
g.output[0].type.tensor_type.elem_type = onnx.TensorProto.FLOAT
del g.value_info[:]
print(f" raw-float mode: dropped {[n.op_type for n in drop]}, output is FLOAT")
onnx.checker.check_model(m, full_check=False)
print(f"saving {dst} ...", flush=True)
onnx.save(m, dst)
print("ok")
return
g.node.remove(clip)
mul.input[0] = pre
del g.value_info[:]
# the +-32767 clamp downstream would re-impose a bound, so widen it to int32 range
for n in g.node:
if n.op_type == "Clip" and n.name.endswith("/Clip_1"):
import numpy as np
from onnx import numpy_helper
lo = helper.make_tensor(PREFIX + "lo", onnx.TensorProto.FLOAT, [],
[-2.0e9])
hi = helper.make_tensor(PREFIX + "hi", onnx.TensorProto.FLOAT, [], [2.0e9])
g.initializer.extend([lo, hi])
n.input[1], n.input[2] = lo.name, hi.name
onnx.checker.check_model(m, full_check=False)
print(f"saving {dst} ...", flush=True)
onnx.save(m, dst)
print("ok")
return
# ── rename everything in the limiter graph so it cannot collide ──
ren = {}
def r(t):
if t in ("", None):
return t
if t == "lim_in":
return pre # feed it the decoder's pre-clip tensor
if t == "lim_out":
return PREFIX + "out"
ren.setdefault(t, PREFIX + t)
return ren[t]
inits = []
for w in lim.initializer:
w.name = r(w.name)
inits.append(w)
nodes = []
for n in lim.node:
n.name = PREFIX + (n.name or n.op_type)
n.input[:] = [r(t) for t in n.input]
n.output[:] = [r(t) for t in n.output]
nodes.append(n)
# ── rewire and insert, keeping topological order ──
g.node.remove(clip)
idx = list(g.node).index(mul)
for k, n in enumerate(nodes):
g.node.insert(idx + k, n)
mul.input[0] = PREFIX + "out"
g.initializer.extend(inits)
# limiter value_info would be stale (it was inferred for a standalone graph)
del g.value_info[:]
# ── round-to-nearest instead of the graph's truncating Cast ──────────────
# Cast(float -> int32) truncates toward zero, a biased quantiser: measured -79.6 dB
# error-to-signal versus -90.4 dB for round-to-nearest, with a 3.4x larger DC term. TRT has
# no INT16 type, so int32 is the narrowest the engine can emit and the host's .to(int16) is
# lossless — which means rounding HERE fully determines the final sample values.
if os.environ.get("GRAFT_NO_ROUND") != "1":
tr = [n for n in g.node if n.output and n.output[0] == g.output[0].name][0]
cast = [n for n in g.node if tr.input[0] in n.output][0]
to = {at.name: at.i for at in cast.attribute}.get("to")
assert cast.op_type == "Cast" and to == onnx.TensorProto.INT32, \
f"expected a Cast->INT32 before the output Transpose, got {cast.op_type} to={to}"
rnd = helper.make_node("Round", [cast.input[0]], [PREFIX + "rounded"],
name=PREFIX + "round_to_nearest")
cast.input[0] = PREFIX + "rounded"
g.node.insert(list(g.node).index(cast), rnd)
print(f" + Round before {cast.name} (round-to-nearest, was truncation)")
print(f" +{len(nodes)} nodes, +{len(inits)} initializers, -1 Clip", flush=True)
onnx.checker.check_model(m, full_check=False)
print(f"saving {dst} ...", flush=True)
onnx.save(m, dst)
print("ok")
if __name__ == "__main__":
main()
@@ -0,0 +1,59 @@
"""Promote the limiter ceiling from a baked constant to a runtime graph INPUT.
The grafted limiter computes gain = min(ceiling / env, 1) via
Reciprocal(env) -> Mul(1/env, ceiling) -> Clip(max=1.0)
with `ceiling` a scalar Constant (0.977 = -0.2021 dBFS). Making it an input gives a runtime knob
with no branching, no second engine and no IIfConditional (which is not graph-capturable):
ceiling = 0.977 the shipped setting
ceiling = 1.0 a 0 dBFS ceiling
ceiling = 0.891 -1 dBFS
ceiling >> 1 gain is identically 1.0 -> limiter bypassed, and the graph's +-1.0 clamp
catches the overshoot, reproducing the OLD hard-clip behaviour
The clamp stays in place, so the int32/int16 delivery tail is unchanged and the engine still emits
already-bounded PCM. Only the gain curve becomes tunable.
Cost: the ceiling can no longer be constant-folded into the reciprocal. That is one scalar Mul.
In the mega-graph it is a persistent buffer written once per render -- the same mechanism as
`seconds_total`, and free under graph replay.
"""
import argparse, sys
from pathlib import Path
import onnx
from onnx import helper, TensorProto
ap = argparse.ArgumentParser()
ap.add_argument("--src", required=True)
ap.add_argument("--out", required=True)
ap.add_argument("--name", default="limiter_ceiling")
a = ap.parse_args()
m = onnx.load(a.src, load_external_data=True)
g = m.graph
c = [n for n in g.node if n.name == "lim//Constant_4"]
assert len(c) == 1, f"expected exactly one ceiling constant, found {len(c)}"
c = c[0]
old = c.output[0]
users = [n for n in g.node if old in n.input]
assert len(users) == 1 and users[0].op_type == "Mul", \
f"ceiling should feed exactly one Mul, got {[(u.op_type, u.name) for u in users]}"
# scalar float input, matching the constant's rank so the Mul broadcast is unchanged
g.input.append(helper.make_tensor_value_info(a.name, TensorProto.FLOAT, []))
for i, t in enumerate(users[0].input):
if t == old:
users[0].input[i] = a.name
g.node.remove(c)
onnx.checker.check_model(m, full_check=False)
out = Path(a.out); out.parent.mkdir(parents=True, exist_ok=True)
big = sum(t.raw_data.__sizeof__() for t in g.initializer) > 1_900_000_000
if big:
onnx.save(m, str(out), save_as_external_data=True, all_tensors_to_one_file=True,
location=out.name + ".data")
else:
onnx.save(m, str(out))
chk = onnx.load(str(out), load_external_data=False)
names = [i.name for i in chk.graph.input]
print(f"wrote {out} ({out.stat().st_size/1e6:.0f} MB)")
print(f" graph inputs now: {names}")
assert a.name in names and not [n for n in chk.graph.node if n.name == "lim//Constant_4"]
print(f" ceiling is a runtime input; the +-1.0 clamp and int32 tail are untouched")
@@ -0,0 +1,75 @@
"""Remove the SAME-S decoder's bottleneck dither so decodes are bit-reproducible.
The shipped graph is:
signal = Slice_1 * running_std (running_std = 0.0657)
noise = RandomNormalLike(signal) * running_std * 0.001
out = signal + noise
i.e. a -60 dB Gaussian dither injected at the bottleneck. It makes every decode differ from the
last, which is why SAME-S has a "noise floor" (~0.9989 between two decodes of the SAME latent)
while SAME-L is bit-exact -- and why any SAME-S A/B has to be run against a matched call history
or it just measures this dither.
Surgery: rewire consumers of the Add onto the signal tensor directly and delete the noise branch
(RandomNormalLike, both Muls, the Add). Exact -- not a reliance on the builder folding a
zero-valued constant away.
"""
import argparse, shutil, sys
from pathlib import Path
import onnx
SRC = ("/admin/home-cj/.cache/huggingface/hub/models--stabilityai--stable-audio-3-optimized/"
"snapshots/2204d5086475bd5b7e6e2bd720772dd8e8160513/onnx/same-s/dec_dynamic_bf16.onnx")
ap = argparse.ArgumentParser()
ap.add_argument("--src", default=SRC)
ap.add_argument("--out", default="onnx/same-s/dec_dynamic_bf16_det.onnx")
a = ap.parse_args()
out = Path(a.out); out.parent.mkdir(parents=True, exist_ok=True)
m = onnx.load(a.src, load_external_data=True)
g = m.graph
prod = {o: n for n in g.node for o in n.output}
rnd = [n for n in g.node if n.op_type == "RandomNormalLike"]
assert len(rnd) == 1, f"expected exactly one RandomNormalLike, found {len(rnd)}"
rnd = rnd[0]
signal = rnd.input[0] # RandomNormalLike takes the signal only for its SHAPE
# walk the noise branch: RandomNormalLike -> Mul(running_std) -> Mul(0.001) -> Add(signal, noise)
chain, cur = [rnd], rnd.output[0]
while True:
nxt = [n for n in g.node if cur in n.input]
assert len(nxt) == 1, f"noise branch forks at {cur}: {[n.name for n in nxt]}"
n = nxt[0]; chain.append(n)
if n.op_type == "Add":
add = n; break
cur = n.output[0]
assert set(add.input) == {signal, chain[-2].output[0]}, f"unexpected Add inputs {list(add.input)}"
print("noise branch:", " -> ".join(f"{n.op_type}" for n in chain))
print(f" signal tensor kept: {signal}")
print(f" Add output rewired: {add.output[0]} -> {signal}")
# rewire every consumer of the Add's output onto the signal, then drop the branch
n_rewired = 0
for n in g.node:
for i, inp in enumerate(n.input):
if inp == add.output[0]:
n.input[i] = signal; n_rewired += 1
for o in g.output:
assert o.name != add.output[0], "Add output is a graph output; rewire needed there too"
dead = {id(n) for n in chain}
keep = [n for n in g.node if id(n) not in dead]
print(f" rewired {n_rewired} consumer input(s); removed {len(g.node)-len(keep)} node(s)")
del g.node[:]
g.node.extend(keep)
onnx.checker.check_model(m, full_check=False)
size = sum(t.raw_data.__sizeof__() for t in g.initializer)
if size > 1_900_000_000:
onnx.save(m, str(out), save_as_external_data=True, all_tensors_to_one_file=True,
location=out.name + ".data")
else:
onnx.save(m, str(out))
print(f"\nwrote {out} ({out.stat().st_size/1e6:.0f} MB)")
assert not [n for n in onnx.load(str(out), load_external_data=False).graph.node
if "Random" in n.op_type], "a random op survived"
print("verified: no random ops remain")