跳到主要内容
渲染 16.1 秒,前 4 小节之后全是 0.000000

渲染 16.1 秒,前 4 小节之后全是 0.000000

今天也没搞懂
今天也没搞懂

· 阅读约 4 分钟

昨天半夜读到一行数字,我愣了半天。

RMS 0.000000。

Krisp 那个项目里的一句话。Krisp 是个浏览器里的节拍工作室,作者做给一个做音乐的朋友,在 Hacktoberfest 的 Build for a Friend 里提交的。一首 90 BPM 的 16 小节,加两秒混响尾巴,全长 42.7 秒;在没有 GPU 的笔记本上渲染 16.1 秒,大约实时的 2.7 倍。看到这儿我还觉得挺猛的,下一句就是:渲染结果只有第 1 到 4 小节有声音,之后全是 0。

整篇长文里这一句几乎算不上重点,夹在一堆性能数据后面,提了一嘴就过去了。

我盯着它看了很久。


因为导出 WAV 的时候,复用的是播放引擎。

createEngine(context) 能接任意音频上下文,导出时就把同一个音频图和同一个调度器塞进 Tone.Offline 里跑。我第一眼看到这个设计,心里冒出一句「这才对嘛」——不写两套,不担心播放和导出听感不一致,不用同步两份状态。

结果就是它。

我想了想为什么,大概是这样:播放是实时的,而实时这件事本身就意味着“未来还没发生”。100 ms 预看就是这么工作的,调度器只负责把 100 毫秒之内要响的音符排出去,剩下的等下一次 tick。Transport 自己还不循环,循环区间靠 wrapTick 算。

离线渲染里没有这个“未来”。上下文一开,整条时间轴就铺在那儿了,你得一口气从 0 算到 42.7 秒。

预看还在,但它预看的那段“未来”,在离线里是空的。

这是我自己猜的。我到现在也没完全搞明白中间断在哪一环:是预看窗口没被推动,还是 wrapTick 在离线上下文里算出来的 tick 越了界。

这就是我现在最夹生的地方。我能看出“复用”这件事在实时和离线之间有一道缝,可我指不出那道缝在第几行。


说句不好听的,我是那种看到“复用同一个引擎”就会直接点头的人。

三周前的我大概连“播放和导出是两种时间”这句都想不到,只会觉得前四小节跑通了就挺好,后面没声音?可能是混响尾巴没渲染完吧。就这样。


还有一处,我也贴一下。

作者算 swing:90 BPM 下每个十六分音符 166.7 毫秒,50% swing 把隔一个音符推后大概 41.7 毫秒。然后他去量导出的 WAV,奇数踩镲晚 0.24 个 step,约 40 毫秒,公式值是 41.7;偶数踩镲的位移小于 0.02 个 step。

差 1.7 毫秒。他就这么写出来了,没圆。

我看到这种地方反而踏实。这跟“看,他也有误差”不是一回事,这种误差本来就在,愿意写出来说明他真的量过,不是照着理想值讲故事。

同类的东西还有:冷启动加载鼓 VAE 约 14.2 秒,第一次 Humanize 约 15 秒,而且只发生一次;作者明说没测带 GPU 的设备。还有那个“我们没做”的列表——不做时间拉伸,只支持 4/4 拍和 16 或 32 步,共享工作室口令而不是账号。

我现在看到这种“我们没有做”的清单,比看到功能列表信得多。功能列表谁都会写。


还有个数字我到现在不知道该怎么读。

Entire 的统计:这个仓库 36 次提交、7 个检查点、591.3k tokens,输入 996,输出 590.3k。

输出是输入的 590 倍。

我不知道这是统计口径的问题,还是真的 agent 一直在往外写、人在往里看。但第一眼看到 590.3k 的时候我心里紧了一下:如果这真是我以后的工作方式,那我到底在干嘛?审阅?点头?还是那个只贡献了 996 个 token 输入的人?

这个问题我今天回答不了,先记着。


说回那道缝。

昨天我在自己一个小项目里试着搭了个类似的形状,四小节,播放听着没问题,导出出来只有第一小节有声音。

没通。

我怀疑是预看设得太小,或者 Offline 那个上下文根本没被 resume 过,也可能两个都不是。明天想把调度器里那三个纯函数单独拎出来跑一遍,stepSeconds、swingDelay、hitTime,不接音频,只喂 tick 看返回什么。如果离线上下文里 tick 序列本身就是歪的,那问题就不在预看。

评论区蹲一个大佬,有没有人真的在 Tone.Offline 里跑过带预看的调度器?我现在连“该怀疑预看还是该怀疑 wrapTick”都还分不清。

进度条:读了一篇很长的文章,看懂的部分大概一半,剩下的一半先存着。