都是实测出来的,每条后面都有当时的数字。跟直觉相反的那几条尤其别改回去。
不能靠压低别的层。 两条路的实测对照:
<250Hz
三层原版 -50.0dB
三层但压低后两层让 chop 突出 -50.1dB ← 低频一点没变
四层,加一层低频 body -26.3dB ← +23.6dB
压别的层是错觉式的重——听感上"干净了",但低频量纹丝不动,而且丢层次。"重"的物理本质是低频质量,只能新增能量,不能搬移已有能量。
EQ 也不行:给瞬态加低频减高频得到的是"更闷",不是"更重"。实测木头撞击 light→heavy,高频涨 18.5dB、低频只涨 10dB——越重高频涨得越快,加低频减高频是逆着物理趋势走。
两次独立验证:
piece-impact的 lethal 档、投矛pierce的第一击。
要让两个材质听起来是"两种金属"而不是"两个物种":
时长 >2k <500 判断
铁 0.496s -32.9 -36.6
青铜(对) 0.642s -33.3 -32.3 高频差 0.4dB → 同类;低频+4.3、时长+146ms → 不同种 ✓
铁盒(错) 0.375s -45.4 -26.8 高频差 12dB → 直接变成两个物种 ✗
高频决定"是什么材料类别",低频和时长决定"是这个类别里的哪一种"。
一组相关音效要听起来属于同一个东西,让它们共享一个层:
- 五门棋盘:
contact层全用同一批木棋子,只换盘面body→ "同一副子落五种盘" - 护盾三件套:共享
ring层 → "同一面盾的三种遭遇"
护盾破碎时把共享的 ring 硬切掉(140ms 截断),比加任何碎裂声都更能表达"它没了"——因为前两个音里它一直在。
衰减音倒过来放就是渐强。所有"充能 / 凝聚 / 结界升起"类音效的起手层都能这么来,不用新录:
metal_board2 衰减音 → areverse → atrim 280ms → afade in
"生成"类动作的通用结构是两段式:聚拢(渐强,无明确起点)+ 成型(明确瞬态)。收尾那个瞬态是关键——只有渐强没有收尾,听感是"还在充能";有了收尾才是"已经立住"。
碎片不是独立随机事件,它们来自同一个物体,尺寸分布、飞行距离、弹跳衰减全是关联的。随机撒听起来像"很多东西同时掉",不像"一个东西碎了"。
碎片数 3 大块(40-160ms 落)+ 4 小碎(260-700ms 落)
弹跳 每片 2-3 次,间隔 × 0.77 递减,音高不变(还是那一片)
音量 大块 -4~-8dB / 小碎 -17~-25dB,每弹 -7.5dB
时间分布 后段更密(碎片要飞行才落地,一片还会弹好几次)
音高 跨度 1.3-1.4 倍就够,再宽会听成上行音阶
时间分布是前疏后密,不是前密后疏——这条最反直觉。逆向现役音效实测:
时长 onset 分布
block-break 1388ms 4 42/104/348/615ms 间隔越来越大
boss-shatter 6475ms 236 500-1000ms:14 → 1000-2000ms:41 越往后越密
物理上:初裂 → 大块分离 → 碎片飞行 → 落地 → 弹跳。飞行需要时间,弹跳更晚,一片会弹好几次,所以小事件天然在后面且密度增加。
素材本身的限制:用"敲击玻璃杯"这种有明确基频的音去堆,堆多少都像一堆铃铛。真实碎片落地是宽带噪声脉冲,几乎无音高。缩短到 30ms 以下能削弱音高感(人耳要几个周期才辨得出音高),但根本解是录真正的碎片落地音。
0904 已按这条重做:
tools/recipes/block-break.mjs用 Sonniss 的真碎玻璃(玻璃板 / 花瓶 / 碎片 / 落地各一类)替掉敲杯堆叠,打碎它的那一击仍是自录glass_slab__knuckle(碎玻璃素材几乎没有 <300Hz,「被撞」的身体得自己给)。上面那组 42/104/348/615ms 的间隔当年没素材填、出来是 100ms 的死洞,这次各层起点终点都错开、任何时刻至少两层在响。
不能一套参数走天下。实测:
| source | 轴 | 体现在 |
|---|---|---|
wood_block |
力度 | 响度 + 时长同步 |
grill_rack |
速度 | 只有时长(620→365ms),响度几乎不变 |
cardboard pull_out |
速度 | attack + 时长 + 响度三者同步 |
flamethrower |
火力 | 频谱倾斜(低频 +1.4dB,高频 −3.3dB),不是音量 |
long_rod |
长度 × 力度 | 短杆的中/大力度只差 0.3dB(刚性饱和) |
所以插值/外推前要先测这个 source 的轴到底动的是哪个量。火焰那条尤其典型:纯拉音量得到的是"更响的小火",耳朵分得出。
tools/ 里那个 onset 分布分析能数出任何音频有几个事件、怎么分布、音量怎么递减。碎裂那次凭"密集瞬态"的直觉做出来的东西,时间结构跟真实碎裂是反的,光靠听定位不到"前密后疏错了"这么具体。
要对齐一个已有音效的质感时,先量它,再照着曲线生成。
生成模型(Suno 这类)能填自录填不了的缺口:火、风、水、兽吼、金属长嘶鸣。但它给的东西结构上不是族。实测同一个 prompt 生 N 次:
族内亮度σ
Suno(默认 prompt) 3.9 - 8.4dB
Suno(切成原子后) 1.6 - 5.3dB
自录 0.1 - 1.7dB
差 3-80 倍。同一个 prompt 生 6 次不是"同一个东西的 6 次",是"6 个相似但不同的东西"。所以别拿它补已有的族——一个族里混两种来源,轴的插值就不成立了。
它还给不了干素材。瞬态→250ms 后的衰减,Suno 只有 7-17dB,自录是 33-48dB —— 那 250ms 后还在响的就是它自带的房间和设计过的余韵。
| 用法 | 行不行 |
|---|---|
| 自录不了的单点素材(火/风/水/兽吼) | ✅ |
| 截取瞬态当原子(扔掉它的房间和余韵) | ✅ 越短一致性越好 |
| 当族用(要力度/位置轴) | ❌ 结构性做不到 |
第二条有实测支撑:air_whoosh(短促单一成分)σ 只有 1.6dB,已进自录区间;air_slit(持续过程)σ 5.3dB。越接近"一个瞬间",一致性越好;越是"一段过程",越各是各的。
这条跟判据二是同一件事,换了个执行面:
✗ a sheet of paper catching fire, quick whoosh into crackle
—— 这是"一张纸被点燃"这个事件,两个成分焊死在一起
✓ paper crackling as it burns
✓ a short sharp air whoosh
—— 拆成两条,点燃时都播,持续燃烧只播第一条
拆开之后 whoosh 那条的一致性是整批最好的(σ 1.6dB)。录的时候要忍住别做完整的一刀,写 prompt 时要忍住别描述完整的事件。
<一句大白话说是什么> + <1-2 个形容词> + dry recording, close mic, no music
no music必写(不写会往音乐化跑)dry recording/close mic是录音术语不是规格,模型认- 别加时长、别加 one-shot、别堆频率描述 —— 复杂规格反而让它出错
- 词不对时换物理描述:"wind whistling" 出不来,改 "air rushing through a narrow slit" 就有了(前者太靠近音乐化的氛围垫子)
入库前照常量一遍族内σ 和衰减,不合格的就别放进 atoms/。
每个文件各自归一到同一响度,力度轴直接废掉——轻放和重击一样响。整族用同一个系数缩放,族内相对关系原样保留,只把最响的那个对齐到目标峰值。
配套:短瞬态用 peak/RMS 不用 LUFS(ebur128 需要 ≥400ms 分析块,撞击音 300-400ms 正好在边界上,读数不可信)。
合成时按峰值归一(limiter 顶到 −3dBFS),峰值齐了,但平均响度可能差 25dB。耳朵听的是平均响度,而且游戏 SFX 要在 BGM + 其他音效垫底的环境里全程听得见,所以动态范围必须比"物理真实"窄。
实测本库归一前:
LUFS 动态差
spear-stick -33.3 27.5dB ← 游戏里基本听不见
block -25.2 24.1dB
block-break -14.7 15.2dB
(现役参照) -10 ~ -28 9 ~ 24dB
spear-stick 比现役最轻的还轻 5dB。归一到 −14 LUFS 后全库收在 RMS −15 ~ −20。
1. loudnorm 单遍对短音频会给出错的增益。 它是动态模式,2 秒以内收敛不了 —— 实测把 −14.7 LUFS 的素材"归一"到了 −21.2,反而更轻。正确做法是三步:只压缩 → 实测 → 用 volume 精确补,loudnorm 只当尺子不当手术刀。
2. crest 大的素材补增益补不上去。 峰值比平均高 25dB 时,补 15dB 后多出来的全被 limiter 削掉,卡在目标以下 3dB 死活上不去。得按 crest 先选压缩比(>22dB 用 6:1 / 18-22 用 4:1 / 14-18 用 3:1),把峰值收下来才补得进去。再加一轮迭代补偿兜底。
3. <400ms 的素材 ebur128 根本测不出,会被静默跳过(补 0dB,等于没处理)。必须退到 RMS。经验偏移:归一到 −14 LUFS 的成品,RMS 落在 −17 左右。
工具:tools/normalize-loudness.mjs,ratio 不给就按 crest 自动选,LUFS 测不出自动退 RMS。
统一 LUFS 只对同层级的事件成立。把过程音归到跟结果音一样响,重音关系就反了:
RMS 层级
命中链(impact + hit) -18.6 结果 ← 重音
挥击起手 -15~-17 过程 ← 比结果还响 4dB,错
挥击起手(修正后) -24~-26 跟 hit-melee 同档
实测这批攻击音照搬上一批命中音的 -14 LUFS,做出来的挥击声比整条命中链还响,听感是"发力在挥、落点没劲"。过程音(挥动、蓄力、脚步)要比它导致的结果音低 6-10dB,不然玩家的注意力会钉在错误的那一帧上。
修正时整族平移(volume -9dB),不要各自重新归一 —— 那会把族内已经调好的相对关系(钝击本就比戳刺重)一起抹平。
有一类层不对应任何物理事件 —— 锣、鼓、弦。它给的是"这一下很重要"的标记和文化质感,不是"发生了什么"。别拿物理层那套规矩套它:
物理层(atoms/) |
戏剧层(library/) |
|
|---|---|---|
| 干湿 | 必须干,截瞬态去房间 | 可以带余韵和房间 |
| 来源 | 自录,同一条信号链 | 外部 CC0 采样库 |
| 判据 | 受控变量、族内 σ < 2dB | 许可证 + velocity 分层 |
戏剧层可以带房间,是因为它不在场景的物理空间里,在叙事空间里 —— 跟电影配乐不需要匹配画面混响是一回事。这条放宽了选材:不用挑干采样,也不用像对付生成素材那样只截瞬态。
light/medium 筝 丝弦 有音高、短
heavy 钹 薄金属 炸开、无明确音高
lethal 锣 厚金属 轰鸣、长余韵
材质从丝到薄金属到厚金属,跟伤害递进天然对应。锣只给 lethal —— 它是"终结"的语义,响一次宣告一个段落结束,用在普通命中会语义通胀。
同一件事的反面:heavy 一开始用锣的 mf 档觉得弱,正确修法是换 f 档而不是给 mf 加增益。锣的力度轴上,从 mf 到 fff 高频涨 19.5dB 而低频几乎不动 —— 差别在音色不在音量,加 dB 补不出来。
这条最容易一刀切错。同样是叠在打击上的乐器层:
300-600Hz >5k 处理
木头(hit) -28.3 -36.7
筝 -31.4 ←撞 -47.3 延后 100ms
钹 -51.0 -42.2 ←不撞 必须对齐
筝跟木头在 300-600 只差 3dB,木头的辨识度恰恰来自这段木质共鸣,不错开就被糊住。而且不能靠 EQ 削 —— B4 的基频就坐在那,削了就没筝了,只能靠时间。
钹在 >5k,跟木头压根不抢,这时候错开反而是错的:两个瞬态挨得够近(5ms 和 20ms)才会融合成"一下",延后 100ms 直接听成两下。
battle / boss D 调
battle-2 / -3 G 调
Dan Tranh 原调弦 B 大调五声(B C# D# F# G#)—— 一个音都不重合
不协和跟触发频次无关,响一次就是一次。移调后取 D 大调五声(D E F# A B) —— 在 D 调是本调,在 G 调也全落在音阶内,两首都不打架。移动量最多 1 个半音,asetrate 对音色的改变可以忽略。
查调用 Goertzel 扫 12 个半音求 chroma 就够,不必上 librosa。但音高别信文件名 —— 实测 Dan Tranh 的 C#3 基频是 D4,八度标记整体差 1;拨弦乐器基频弱、二次泛音强,检测也容易报高八度。
这是戏剧层能做而物理层做不到的事:玩家打人用协和音,挨打用不协和音,玩家不看数字就知道刚才发生了什么。
但单音编码不了协和度 —— 不协和是音程产生的。单个 D 在 D 调是主音、在 G 调是属音,两边都稳,当"挨打"根本不成立。要难听得靠音程:D + E 同时响是大二度(最刺的音程之一),或者三全音。
三全音归到哪一边不是理论能定的:它在金属乐里是力量和侵略性的标志,所以给"攻击"比给"受击"更对。理论只能告诉你音程是什么,选哪个还得靠耳朵。
p / mp / mf / f / fff、v2/v3/v5/v7 这类分层跟自录的 f1/f2/f3 是同一个东西,可以插值、可以按伤害档位映射。这跟生成模型有本质区别(第八条:那边每次生成都是不同的东西)。rr1/rr2 这类 round robin 是同力度的不同次演奏,直接当变体池。
选库时优先看这两样,比听 demo 有用。
手工做一个音效通常是一步一个中间 wav:切素材 → 加效果 → 混 → 压 → 编码。
压成一条 filter_complex 之后听起来该一样,但 0820 校准劫线时实测差了
10 多 dB,追下去是两件跟音色无关的事被顺手带掉了。
手工版每步都写 -ar 48000 -ac 1,采样率在步骤之间被悄悄统一了。压成单链后
源文件的原始采样率直接透到滤镜里 —— 而有些滤镜的参数含义依赖输入采样率:
elec_hum.wav 是 96kHz。asetrate=46800 的本意是「差不到半音」的微失谐
(46800/48000 → 0.44 半音),喂 96k 素材时它变成了降八度。
实测频带能量整体下移一个八度:40Hz 档 +2.4dB、320Hz 档 -7.9dB。
拍频、共振峰、任何按 48k 算出来的常数全部作废。所有声源进链之前先统一转
48k 单声道,一次转好复用,别指望链里的 aresample 兜底 —— 它在 asetrate
之后才生效,那时候已经晚了。
Sonniss 的包大量是 96k(沙类十条全是),这不是个例。
vibrato 跟 amix 在同一条 filter_complex 里会吐 NaN 采样(实测
ffmpeg 8.1.1)。而且它非确定性:
- 三路
[snap][hum][buzz]amix出 37760 个 NaN(几乎整段),换成[buzz][snap][hum]就是 0 个 - 同一条支路单独跑 48 次一次都不复现,整条配方跑起来偶发
- 中招时是整段全 NaN,说明是启动期延时缓冲没填满就被下游拉走了
NaN 一路穿到 limiter,RMS level dB: nan,测出来的 LUFS 全是垃圾 ——
表现出来就是「增益怎么调都不对」,很容易误判成配方参数问题。
两层防:一条支路渲一个 wav 再混(落盘强制 flush,故障面缩到最小), 外加每个中间产物验一次 NaN,中招重渲:
const inspect = (f) => { /* astats 读 Number of NaNs / Number of Infs */ };
const render = (args, p, what) => {
for (let i = 0; i < 4; i++) { ff(args); if (!inspect(p).bad) return p; }
throw new Error(`${what} 连续 4 次都带 NaN`);
};这类 bug 不能靠事后听审发现 —— NaN 在 ogg 里可能就是一段静音或杂音, 但它先把你的测量污染了。凡是渲染中间产物,都验一次。
手工版的 volume=8.3dB 只在当时那条链上成立,改任何一段都得重调,
而且改完没人知道要重调。改成两次测量:
- 裸信号峰值 → 对齐到固定值再进压缩器。压缩器阈值是绝对的, 不定死喂进去的电平,压缩量就不可复现(这也是早期阴阳劫 LUFS 差 8-12dB 的根因之一:compressor 在 volume 之前)
- 在成品上测 LUFS 补差值。limiter 削峰和 vorbis 编码都会再改一点响度, 高 crest 的素材(湮灭)能差 0.7dB —— 只有量成品才是真的
劫线四条改成测量驱动后,跨轮次的输出逐位稳定,跟手工交付物的频带能量 偏差 ≤1dB。