做 AUDIOGENIC 时,我想解决的事情很具体:玩家放进一首歌,程序根据这首歌临时做一场 Boss 战。音乐要参与关卡结构,不能只在后台播放;Boss 也不能换首歌以后还是原来那套动作。
一开始我把这个问题想得比较简单。浏览器已经能分析音频,模型也能返回结构化 JSON,那么把段落特征发给模型,让它写一份行为时间线似乎就可以了。
实际做下去后,事情没有这么顺。音频分析会认错半拍,弱段落没有足够特征,模型会重复招式,也会写出时间范围重叠的计划。即使计划本身没问题,游戏里还会有子弹刚生成就消失、预警和伤害范围对不上之类的小错。它们混在一起时,很难判断问题究竟出在音乐、模型还是战斗代码。
后来我把中间过程拆开了。现在的链路大致如下:
音频
-> 分帧、频谱与节拍分析
-> 段落和音乐原语
-> DeepSeek 生成 PrimitivePlan
-> 本地编译成 BehaviorTimeline
-> 游戏运行时逐帧执行

先把音频变成可以处理的段落
音频在浏览器里用 Web Audio API 解码。分析前先把多声道混成单声道。这里遇到过一个不太显眼的问题:如果左右声道接近反相,直接求平均会把信号互相抵消。最后我同时计算下混后的能量和各个单声道的能量;如果下混能量不到最强声道的 5%,就直接使用最强声道。
这个判断只占几行代码,但不处理的话,后面的节拍和频谱分析拿到的可能是一段接近静音的数据。
分帧窗口取约 46 毫秒,再向上调整到最近的 2 次幂,hop 是半个窗口。每帧乘 Hann 窗后做 FFT,记录短时能量、低频和高频占比、频谱质心以及频谱通量。低频目前看 40 到 250 Hz,高频看 2 到 8 kHz。段落划分会同时参考能量变化、频谱变化和检测到的节拍。
自动节拍检测经常碰到的不是完全错误,而是 128 BPM 被识别成 64 或 256,或者速度基本正确但第一拍错开了。为了不把 BPM 输入框暴露给玩家,我加了一个十次点击的准备过程。玩家跟着歌点十下,程序用点击间隔重新比较半速和双速候选,再修正第一拍的位置。

这部分本来只是一个分析参数,放进游戏流程后反而比较自然。玩家看到的是“和音乐同步”,不需要知道程序正在修正 tempo candidate 和 downbeat。
直接把段落特征交给模型不太好用
最早的行为层直接使用段落能量、频谱和节拍密度。模型也可以直接返回最终的 BehaviorTimeline。这样很快能跑起来,但调试比较麻烦。
比如某一段生成了激光。我能看到高频占比、能量和频谱通量,也能看到提示词,但很难说清模型究竟根据哪一个量选择了激光。模型换一次,结果又可能不同。规则模式和模型模式还各有一套映射方式,改一个招式往往要同时检查两条链路。
为此我在音频段落和行为计划之间加了六种音乐原语:
bass-impact表示低频冲击。bright-beam表示高频较强、声音偏亮。flux-break表示频谱变化突然增大。dense-pressure表示节拍和能量持续密集。stable-groove表示节奏相对稳定。climax表示高能量或高张力段落。
每个段落会给这些原语打分。低于 0.48 的结果先去掉,再留下最强的三个。这里限制三个主要是为了让输入保持简单。全部塞进去虽然信息更多,但模型几乎总能在每一段找到使用高潮技能的理由。
原语并不直接等于招式。bass-impact 可能编译成冲锋、震地或近战重击,还要看它所在的段落、强度和前后阶段。它更像音频分析和游戏行为之间的一层小协议。
弱音频后来又带来一个边界情况。有些段落六种原语都达不到 0.48,行为计划因此留出空洞。现在遇到这种情况,程序会根据频谱倾斜、通量、能量和节拍密度补一个低置信度原语。补出的强度仍低于正常阈值,这样后面能继续编译,也不会把一个安静段落误报成强信号。
模型输出还要再编译一次
DeepSeek 收到 BPM、起拍位置、总时长、段落特征、节拍网格样本和原语列表。请求经过 Vite 的服务端代理,浏览器里没有 API Key。当前请求最多生成 2400 tokens,90 秒后超时。
模型返回的是 PrimitivePlan。每一步只有起止时间、引用的原语、阶段作用、组合方式、强度和一段解释。阶段作用限定为准备、施压、爆发、换位和恢复;组合方式限定为单招、叠加和高潮组合。
这份计划还不能直接运行。它会先检查字段、枚举、时间范围、原语引用、段落覆盖和步骤重叠,然后交给本地编译器。
编译器目前有八种移动方式和十三种攻击方式。它负责选择具体技能,并补齐子弹数量、速度、开火间隔、预警强度和压力等级。低能量段即使被模型标成高潮,也不会直接得到高压组合技。近战攻击如果没有追击或快速位移,编译器也会调整移动方式。
连续重复是另一个单独处理的问题。模型很容易抓住一个明显特征反复使用,例如低频较强时连续安排三次近战。现在编译后还会检查主攻击序列,连续三段相同时替换后续动作。
规则模式没有另写一套运行时。它也先构造 PrimitivePlan,再经过同一个编译器生成 BehaviorTimeline。模型返回非法 JSON、时间线有缺口或请求失败时,规则计划会接上。战斗代码只认识最终时间线,不需要知道前面是谁生成的。
这条管线现在大致可以工作,但每一层都还有可以继续拆的地方。对我来说,比较有用的变化是问题终于能分开查了:音频特征不对就查分析,段落意图单调就查计划,招式数值有问题就查编译器,预警和伤害不一致就回到运行时。至少不需要再把所有异常都归到“模型这次生成得不好”。