- 程序章程 research/new8/00-program.md(8款概念/回合协议/验收门/进度账) - 设计:docs/design/15-radio.md+voice-ids(132句,6状态9抉择4结局,电话滤波资产侧带通)/16-dungeon.md+voice-ids(125句,脉冲回声/7层seed生成/三档战斗/元进度遗产) - 资产:TTS 970文件(GPU flock串行)+ Mureka BGM 8首 $0.24(台账 mureka_log.md)+ 程序合成 sfx(含预pan侧墙/开口、phone_ring铃声、amb_cave呼吸层、turn_tick)+ 终检 check_all_assets 8游戏全绿 - radio:game.js+playtest 221断言(4结局矩阵/pendingChoice盲玩公平/RING两级看门狗/菜单循环/存档schema) - dungeon:game.js+playtest 1610断言(seed全网格回声一致性/学期零死/格挡80%/死亡归因真话/元进度/菜单冻结/上滑5段状态链) - 引擎修复:audio.js _playVoice 加载竞态补杀 token(R1真机71分钟语音风暴根因)+ 同id 3s窗口≥5次熔断保险丝 + gesture.js 长按boost事件门(+66行,9套件回归全绿) - 大厅:新增 radio/dungeon 卡片 + GAMES 数组 - harness:sessions/radio.mjs+dungeon.mjs(主线+压力双剧本)+ transcribe 注册 + voice_text.json 刷新(8游戏704句)+ 13号文档 §20 登记 - R1 存档:4人设评审×2游戏 + 真机试玩报告×2(radio 5/5愿意玩;dungeon 0/4→确诊风暴)+ synthesis×2 + 转写8份 - R2 修复:radio F1-F5(看门狗补挂/rep_low改词重录/真铃声/ASK续玩语义/链末跳过保护)+ dungeon D0-1..D1-6(风暴根治/回声真实性/密度下限+呼吸层/导航最短路/菜单关闭反馈/金币档同账本)+ 实现核对登记
332 lines
15 KiB
Python
332 lines
15 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""《回声地窟》SYN 程序合成音效(批次 1,16-dungeon.md §9.2 标注 SYN 的条目)。
|
||
|
||
17 个 id / 19 个文件(side_* 由 mono 源经 ffmpeg pan 派生为预 pan 立体声):
|
||
pulse 声波脉冲:180->60Hz 正弦扫频 + 噪声瞬态,0.15s,中频突出(中频段 1-2kHz 补一层)
|
||
mat_stone/door/stair/water 材质尾音:闷石敲/空心木咚/金属当啷/水滴湿滑,各 0.2-0.25s
|
||
side_wall_l/r 侧向回声·墙(闷哒);side_open_l/r 侧向回声·开口(空旷回灵)
|
||
—— 立体声文件能量只在 L 或 R 声道(引擎 play() 无实时 pan,
|
||
space whoosh_left/right 同工艺;SA3 立体声分离度不达标时本 SYN 即兜底路径)
|
||
step_own/turn bump 单步/拖步转身/撞墙闷响(脚步=低频 100-300Hz,与脉冲中频带分离防糊)
|
||
coin/potion/gear 金币叮当/喝药咕咚/换装金属轻响
|
||
atk_swing/parry/hurt 挥拳风声/格挡亮叮/受击闷击
|
||
levelup 上行钟声琶音(内容音,允许;D 小调五声,与 rpg 的 A 小调区分)
|
||
|
||
SA3 条目(echo_1..4、stairs_down、mob_step_*、tel_*、atk_hit、enemy_die)本轮跳过,
|
||
清单见 research/new8/batch1-assets-report.md。
|
||
|
||
可复现:本脚本入库,一键出 web/dungeon/audio/sfx/*.{ogg,m4a};中间 wav 在
|
||
gen/staging/sfx/(不入库)。规格:ogg libvorbis q4 44.1kHz + m4a aac 128k;
|
||
one-shot 真峰值 <= -3dBFS(解码回读法,源归一 -4dBFS 留 MDCT 过冲余量);无 loop 类条目。
|
||
|
||
用法:.venv-tts/bin/python web/dungeon/gen/gen_sfx.py (纯 CPU,无 GPU 依赖)
|
||
"""
|
||
import os
|
||
import subprocess
|
||
|
||
import numpy as np
|
||
import soundfile as sf
|
||
|
||
SR = 44100
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
STAGING = os.path.join(HERE, "staging", "sfx")
|
||
DST = os.path.join(HERE, "..", "audio", "sfx")
|
||
FFMPEG = "/home/xsl/SoundGame/tools/bin/ffmpeg"
|
||
PEAK_SRC = 10 ** (-4.0 / 20) # 源归一 -4dBFS(有损编码过冲余量)
|
||
PEAK_LIMIT = 10 ** (-3.0 / 20) # 解码回读上限 -3dBFS
|
||
os.makedirs(STAGING, exist_ok=True)
|
||
os.makedirs(DST, exist_ok=True)
|
||
|
||
|
||
def t(d):
|
||
return np.arange(int(SR * d)) / SR
|
||
|
||
|
||
def env_exp(d, attack, release):
|
||
n = int(SR * d)
|
||
e = np.ones(n)
|
||
na, nr = max(1, int(SR * attack)), max(1, int(SR * release))
|
||
e[:na] = np.linspace(0, 1, na)
|
||
e[-nr:] *= np.linspace(1, 0, nr)
|
||
return e
|
||
|
||
|
||
def lowpass(x, k):
|
||
return np.convolve(x, np.ones(k) / k, mode="same")
|
||
|
||
|
||
def bandpass(x, lo, hi):
|
||
"""简易带通:低通(hi) 减 低通(lo)。k = SR/(pi*fc) 近似窗长。"""
|
||
kh, kl = max(1, int(SR / (np.pi * hi))), max(1, int(SR / (np.pi * lo)))
|
||
return lowpass(x, kh) - lowpass(x, kl)
|
||
|
||
|
||
def bell(freq, dur, decay=6.0, partials=((1, 1.0), (2.4, 0.35), (3.9, 0.15))):
|
||
x = t(dur)
|
||
return sum(a * np.sin(2 * np.pi * freq * r * x) * np.exp(-x * decay * (1 + r * 0.4))
|
||
for r, a in partials)
|
||
|
||
|
||
def save(name, sig):
|
||
"""mono 存(side_* 供 pan 派生)。工艺同 tools/fix_clicks.py:
|
||
去直流(减均值)→ 首尾 8ms cos² 淡入淡出(去爆豆,快攻击听感不变)→ 峰值归一 -4dBFS。"""
|
||
sig = sig - np.mean(sig)
|
||
nf = max(1, int(SR * 0.008))
|
||
fade = 0.5 * (1 - np.cos(np.pi * np.arange(nf) / nf))
|
||
sig = sig.astype(np.float64).copy()
|
||
sig[:nf] *= fade
|
||
sig[-nf:] *= fade[::-1]
|
||
sig = sig / (np.max(np.abs(sig)) + 1e-9) * PEAK_SRC
|
||
sf.write(os.path.join(STAGING, name + ".wav"), sig.astype(np.float32), SR)
|
||
|
||
|
||
rng = np.random.default_rng(20260901)
|
||
|
||
# ---- pulse:声波脉冲(0.15s,扫频 + 噪声瞬态 + 中频突出层) ----
|
||
d = 0.15
|
||
x = t(d)
|
||
f = 60 + (180 - 60) * np.exp(-x * 22) # 180->60Hz 下扫
|
||
sweep = np.sin(2 * np.pi * np.cumsum(f) / SR) * np.exp(-x * 20)
|
||
transient = bandpass(rng.standard_normal(len(x)), 900, 2400) * np.exp(-x * 55) # 1-2kHz 中频瞬态
|
||
save("pulse", 0.45 * sweep + 0.8 * transient)
|
||
|
||
# ---- 材质尾音(0.2-0.25s) ----
|
||
# 闷石敲:低频短促敲击
|
||
d = 0.22
|
||
x = t(d)
|
||
save("mat_stone", 0.9 * np.sin(2 * np.pi * 95 * x) * np.exp(-x * 40)
|
||
+ 0.3 * lowpass(rng.standard_normal(len(x)), 40) * np.exp(-x * 70))
|
||
# 空心木咚:两个衰减正弦 + 木腔共鸣
|
||
d = 0.25
|
||
x = t(d)
|
||
save("mat_door", np.sin(2 * np.pi * 150 * x) * np.exp(-x * 26) + 0.5 * np.sin(2 * np.pi * 230 * x) * np.exp(-x * 38))
|
||
# 金属当啷:非谐波金属泛音簇
|
||
d = 0.24
|
||
x = t(d)
|
||
save("mat_stair", sum(a * np.sin(2 * np.pi * f * x) * np.exp(-x * dc)
|
||
for f, a, dc in [(1720, 1.0, 16), (2610, 0.6, 22), (3480, 0.4, 28), (860, 0.5, 12)]))
|
||
# 水滴湿滑:短促水滴(下滑正弦)+ 高频飞溅
|
||
d = 0.22
|
||
x = t(d)
|
||
f = 900 - 500 * x / d
|
||
drop = np.sin(2 * np.pi * np.cumsum(f) / SR) * np.exp(-x * 34)
|
||
splash = bandpass(rng.standard_normal(len(x)), 2500, 7000) * np.exp(-x * 60) * 0.35
|
||
save("mat_water", drop + splash)
|
||
|
||
# ---- 侧向回声(mono 源 -> ffmpeg pan 派生预 pan 立体声) ----
|
||
# side_wall 闷哒:低频闷冲击(能量集中在 150-600Hz)
|
||
d = 0.3
|
||
x = t(d)
|
||
save("side_wall", lowpass(rng.standard_normal(len(x)), 60) * np.exp(-x * 30)
|
||
+ 0.6 * np.sin(2 * np.pi * 130 * x) * np.exp(-x * 36))
|
||
# side_open 空旷回灵:中频拍点 + 两记渐弱反射尾(空间感)
|
||
y = np.zeros(int(SR * 0.7), dtype=np.float32)
|
||
for start, amp, dc in [(0.0, 1.0, 24), (0.13, 0.55, 18), (0.3, 0.3, 13)]:
|
||
n = len(y) - int(SR * start)
|
||
xx = np.arange(n) / SR
|
||
tap = bandpass(np.random.default_rng(int(start * 100) + 5).standard_normal(n), 300, 2600) * np.exp(-xx * dc) * amp
|
||
y[int(SR * start):] += tap
|
||
save("side_open", y)
|
||
|
||
# ---- 移动(脚步=低频 100-300Hz) ----
|
||
# step_own 单步
|
||
d = 0.22
|
||
x = t(d)
|
||
save("step_own", lowpass(rng.standard_normal(len(x)), 70) * np.exp(-x * 38)
|
||
+ 0.4 * np.sin(2 * np.pi * 80 * x) * np.exp(-x * 45))
|
||
# step_turn 拖步转身:两段错位的拖擦(慢攻击)
|
||
d = 0.5
|
||
x = t(d)
|
||
drag1 = lowpass(rng.standard_normal(len(x)), 90) * env_exp(d, 0.05, 0.12)
|
||
shift = int(0.22 * SR)
|
||
drag2 = np.roll(drag1, shift) * 0.7
|
||
save("step_turn", 0.9 * drag1 + 0.9 * drag2)
|
||
# bump 撞墙闷响:60-110Hz 重击 + 闷噪声
|
||
d = 0.35
|
||
x = t(d)
|
||
save("bump", np.sin(2 * np.pi * 68 * x) * np.exp(-x * 18) + 0.5 * lowpass(rng.standard_normal(len(x)), 55) * np.exp(-x * 28))
|
||
|
||
# ---- 拾取 ----
|
||
# coin 金币叮当:非谐波高频泛音簇,两次碰撞(rpg coin 同族、参数区分)
|
||
y = np.zeros(int(SR * 0.45), dtype=np.float32)
|
||
for start, dec in [(0.0, 26), (0.11, 34)]:
|
||
n = len(y) - int(SR * start)
|
||
seg = np.arange(n) / SR
|
||
sig = sum(a * np.sin(2 * np.pi * f * seg) * np.exp(-seg * dec) * 0.3
|
||
for f, a in [(4700, 1.0), (6300, 0.7), (8300, 0.45), (10500, 0.28)])
|
||
y[int(SR * start):] += sig
|
||
save("coin", y)
|
||
# potion 喝药咕咚:三声下滑「咕」+ 气泡噪声
|
||
y = np.zeros(int(SR * 0.8), dtype=np.float32)
|
||
for i, start in enumerate([0.0, 0.18, 0.38]):
|
||
d_ = 0.16
|
||
xx = t(d_)
|
||
f0 = 340 - i * 55
|
||
f = f0 - 130 * xx / d_
|
||
g = np.sin(2 * np.pi * np.cumsum(f) / SR) * env_exp(d_, 0.01, 0.05)
|
||
bub = bandpass(rng.standard_normal(len(xx)), 500, 1600) * np.exp(-xx * 30) * 0.25
|
||
y[int(SR * start):int(SR * start) + len(g)] += (g + bub) * (1 - i * 0.15)
|
||
save("potion", y)
|
||
# gear 换装金属轻响:短促金属扣合(低强度)
|
||
d = 0.28
|
||
x = t(d)
|
||
save("gear", 0.7 * sum(a * np.sin(2 * np.pi * f * x) * np.exp(-x * dc)
|
||
for f, a, dc in [(1180, 1.0, 20), (1990, 0.5, 30), (520, 0.7, 14)]))
|
||
|
||
# ---- 战斗 ----
|
||
# atk_swing 挥拳风声:带通噪声,幅度先涨后落(频率感觉下坠)
|
||
d = 0.32
|
||
x = t(d)
|
||
env = np.sin(np.pi * np.minimum(x / d, 1.0)) ** 1.5
|
||
swoosh = bandpass(rng.standard_normal(len(x)), 400, 3000) * env
|
||
save("atk_swing", swoosh)
|
||
# parry 格挡亮叮:高频金属亮铃(快攻击、余振)
|
||
d = 0.45
|
||
x = t(d)
|
||
save("parry", sum(a * np.sin(2 * np.pi * f * x) * np.exp(-x * dc)
|
||
for f, a, dc in [(2500, 1.0, 9), (3750, 0.55, 13), (5300, 0.35, 17)]))
|
||
# hurt 受击闷击:低频闷击 + 平滑噪声团(与 rpg hurt 同族、参数区分)
|
||
d = 0.32
|
||
x = t(d)
|
||
save("hurt", np.sin(2 * np.pi * 55 * x) * np.exp(-x * 16)
|
||
+ 0.55 * lowpass(rng.standard_normal(len(x)), 50) * np.exp(-x * 22))
|
||
|
||
# ---- levelup:上行钟声琶音(内容音,允许;D 小调五声 D4 F4 G4 A4 C5 D5) ----
|
||
y = np.zeros(int(SR * 1.8), dtype=np.float32)
|
||
for i, f in enumerate([293.66, 349.23, 392.00, 440.00, 523.25, 587.33]):
|
||
seg = np.arange(int(SR * 1.0)) / SR
|
||
b = sum(a * np.sin(2 * np.pi * f * r * seg) * np.exp(-seg * dcy)
|
||
for r, a, dcy in [(1, 1.0, 4), (2.4, 0.35, 7), (3.9, 0.15, 10)])
|
||
s = int(SR * i * 0.14)
|
||
n = min(len(b), len(y) - s)
|
||
y[s:s + n] += b[:n] * 0.4
|
||
save("levelup", y)
|
||
|
||
# ---- turn_tick 战斗回合确认音(R2 D1-6:回合 15s 无操作的合成回执;
|
||
# standard 档没有 gd_turn 语音,纯震感像死机——R1 live P2) ----
|
||
# 与 guide nudge 同族(880Hz 木鱼质感),0.16s 快衰减 + 高频点缀,可与其他
|
||
# 回合音叠放不糊(短、中高频、快衰减)。
|
||
d = 0.16
|
||
x = t(d)
|
||
save("turn_tick", np.sin(2 * np.pi * 880 * x) * np.exp(-x * 38)
|
||
+ 0.35 * np.sin(2 * np.pi * 1760 * x) * np.exp(-x * 52))
|
||
|
||
|
||
def save_loop(name, sig):
|
||
"""loop 源存(amb_cave 专用):去直流 → 环回交叉淡化(尾 50ms 与头 50ms
|
||
加权拼接,接缝连续无爆音)→ **不做首尾淡化**(淡化会让循环每圈塌一次,
|
||
规格:去直流不淡化)→ 峰值归一 -4dBFS(同 one-shot 余量口径)。"""
|
||
sig = sig - np.mean(sig)
|
||
k = int(SR * 0.05)
|
||
ramp = np.linspace(0.0, 1.0, k)
|
||
head, tail = sig[:k].copy(), sig[-k:].copy()
|
||
sig = sig[:-k]
|
||
sig[:k] = head * ramp + tail * (1 - ramp)
|
||
sig = sig / (np.max(np.abs(sig)) + 1e-9) * PEAK_SRC
|
||
sf.write(os.path.join(STAGING, name + ".wav"), sig.astype(np.float32), SR)
|
||
|
||
|
||
# ---- amb_cave 洞穴环境垫(R2 D0-3② 探索呼吸层:静默 >20s 播一段,10s 无缝循环源) ----
|
||
# 洞穴空气声=45-320Hz 带通噪(带通天然零直流、无次声漂移——褐噪 cumsum 的能量
|
||
# 全在 <30Hz,过编码 highpass 后只剩 -30dB 空气,R2 首版教训)×慢呼吸 LFO
|
||
# (4 整周期/10s,接缝连续)+ 两粒远离接缝的远处水滴。真峰值/直流口径与
|
||
# one-shot 一致(解码回读 ≤-3dBFS、|直流|≤0.001)。
|
||
_n = int(SR * 10.0)
|
||
_air = bandpass(rng.standard_normal(_n), 45, 320)
|
||
_lfo = 0.7 + 0.3 * np.sin(2 * np.pi * 4 * np.arange(_n) / _n)
|
||
_y = np.zeros(_n)
|
||
for _start, _f0 in [(2.3, 620.0), (6.7, 540.0)]:
|
||
_xx = np.arange(int(SR * 0.3)) / SR
|
||
_f = _f0 - 300 * _xx / 0.3
|
||
_drop = np.sin(2 * np.pi * np.cumsum(_f) / SR) * np.exp(-_xx * 26) * 0.16
|
||
_s0 = int(SR * _start)
|
||
_y[_s0:_s0 + len(_drop)] += _drop
|
||
save_loop("amb_cave", _air * _lfo * 0.9 + _y)
|
||
|
||
|
||
def pan_derive(name):
|
||
"""mono 源 -> 预 pan 立体声(能量只在 L 或 R 声道),ffmpeg pan 派生;
|
||
未指定的输出声道默认静音,即 pan=stereo|c0=c0(仅左)/ pan=stereo|c1=c0(仅右)。"""
|
||
src = os.path.join(STAGING, name + ".wav")
|
||
for side, out_ch in [("l", "c0"), ("r", "c1")]:
|
||
out = os.path.join(STAGING, f"{name}_{side}.wav")
|
||
subprocess.run([FFMPEG, "-y", "-v", "error", "-i", src,
|
||
"-af", f"pan=stereo|{out_ch}=c0", "-c:a", "pcm_f32le", out], check=True)
|
||
|
||
|
||
for _n in ["side_wall", "side_open"]:
|
||
pan_derive(_n)
|
||
|
||
|
||
def encode(name, loop=False):
|
||
"""staging wav -> ogg q4 + m4a 128k,解码回读校验:
|
||
逐声道真峰值 <= -3dBFS(硬 pan 立体声不能按声道均值漏检)、|直流| <= 0.001(与在库资产基线一致)。
|
||
工艺:one-shot 源前后垫 60/120ms 静音(编码器软着陆,边界帧伪影落在垫音里);
|
||
loop 源(amb_cave)**不垫不淡**(垫音/淡入出会破坏无缝循环),仅 highpass=f=30
|
||
(抑制 MDCT/AAC 解码直流再生,可听频段无影响);解码回读超限时按实测值迭代补偿
|
||
(真峰值补 volume,直流在源上预减实测偏移;均确定性,最多 4 轮)。"""
|
||
src = os.path.join(STAGING, name + ".wav")
|
||
base, _sr0 = sf.read(src, always_2d=True)
|
||
if not loop:
|
||
pad_h = np.zeros((int(SR * 0.06), base.shape[1]), dtype=np.float64)
|
||
pad_t = np.zeros((int(SR * 0.12), base.shape[1]), dtype=np.float64)
|
||
base = np.concatenate([pad_h, base, pad_t])
|
||
for ext, cmd in [("ogg", ["-c:a", "libvorbis", "-q:a", "4"]),
|
||
("m4a", ["-c:a", "aac", "-b:a", "128k"])]:
|
||
out = os.path.join(DST, f"{name}.{ext}")
|
||
vol, dc_adj = 0.0, 0.0
|
||
tmp = "/tmp/_sfx_enc.wav"
|
||
for _attempt in range(4):
|
||
sig = base - dc_adj
|
||
if vol < 0:
|
||
sig = sig * (10 ** (vol / 20))
|
||
sf.write(tmp, sig.astype(np.float32), SR)
|
||
subprocess.run([FFMPEG, "-y", "-v", "error", "-i", tmp, "-af", "highpass=f=30",
|
||
"-ar", "44100", *cmd, out], check=True)
|
||
chk = "/tmp/_sfx_qc.wav"
|
||
subprocess.run([FFMPEG, "-y", "-v", "error", "-i", out, "-c:a", "pcm_f32le", chk], check=True)
|
||
y, _sr = sf.read(chk, always_2d=True)
|
||
if loop:
|
||
y = y[int(SR * 0.02):-int(SR * 0.02)] # QC 掐头去尾(编码器边界帧不计峰)
|
||
m = y.mean(axis=1)
|
||
tp = 20 * np.log10(np.max(np.abs(y)) + 1e-9) # 逐声道峰值(硬 pan 不被均值稀释)
|
||
dcv = float(np.mean(m))
|
||
ok_tp, ok_dc = tp <= -3.0, abs(dcv) <= 0.001
|
||
if ok_tp and ok_dc:
|
||
break
|
||
if not ok_tp:
|
||
vol -= (tp + 3.0) + 0.25 # 超限差值 + 0.25dB 余量
|
||
if not ok_dc:
|
||
dc_adj -= dcv # 源上预减解码实测直流
|
||
else:
|
||
raise AssertionError(f"{name}.{ext}: 补偿后仍超限 tp={tp:.2f}dBFS dc={dcv:.4f}")
|
||
extra = ""
|
||
if loop: # 循环接缝量化:解码回读首尾样本差不超源峰的 5%(听感无咔哒)
|
||
joint = float(np.max(np.abs(y[0] - y[-1])))
|
||
joint_db = 20 * np.log10(joint + 1e-9) - 20 * np.log10(np.max(np.abs(y)) + 1e-9)
|
||
extra = f" joint={joint_db:+.1f}dB rel peak"
|
||
print(f"[ok] {name}.{ext} peak={tp:.2f}dBFS dc={dcv:+.4f} ch={y.shape[1]}{extra}")
|
||
|
||
|
||
FILES = ["pulse", "mat_stone", "mat_door", "mat_stair", "mat_water",
|
||
"side_wall_l", "side_wall_r", "side_open_l", "side_open_r",
|
||
"step_own", "step_turn", "bump", "coin", "potion", "gear",
|
||
"atk_swing", "parry", "hurt", "levelup", "turn_tick"]
|
||
for _n in FILES:
|
||
encode(_n)
|
||
encode("amb_cave", loop=True)
|
||
|
||
# amb_cave 循环规格专项:源级去直流(|均值|<=1e-4)且无首尾淡化(首尾样本非零)
|
||
_amb, _ = sf.read(os.path.join(STAGING, "amb_cave.wav"), always_2d=True)
|
||
assert abs(float(_amb.mean())) <= 1e-4, "amb_cave: 源直流超标"
|
||
assert np.max(np.abs(_amb[0])) > 1e-3 and np.max(np.abs(_amb[-1])) > 1e-3, "amb_cave: 首尾被淡化(loop 规格违例)"
|
||
|
||
# side_* 极性抽检:能量只在声像侧声道(解码回读)
|
||
for _n, _side in [("side_wall_l", 0), ("side_wall_r", 1), ("side_open_l", 0), ("side_open_r", 1)]:
|
||
y, _ = sf.read(os.path.join(STAGING, _n + ".wav"), always_2d=True)
|
||
other = 1 - _side
|
||
assert np.max(np.abs(y[:, other])) < 1e-6, f"{_n}: 非目标声道有能量"
|
||
assert np.max(np.abs(y[:, _side])) > 0.3, f"{_n}: 目标声道无能量"
|
||
print("dungeon SYN sfx done:", len(FILES), "files x2 formats")
|