BPM 被砍成一半,整首歌慢了一倍
转谱流程里有个参数看着不起眼,但错一点全盘皆错:BPM。
因为 MusicXML 里音符的时长是相对值(quarterLength),实际持续多少秒要算:
秒数 = quarterLength × 60 / BPM
BPM 写错了,所有音符的实际时长就跟着错。喂进 CeVIO 之后表现是:唱出来和伴奏对不上,整首歌要么赶要么拖。
先估,再"校正"
估 BPM 的路子是用 librosa 做 beat tracking。但它对分离后的人声效果不好——人声的节拍点太弱,鼓都被分走了。
所以改成对整首歌做检测,再做一次八度校正把结果拉进 70~180 这个合理区间。
到这一步拿到的还只是"参考值"。我又写了一个 refine_bpm():生成一批候选(原值、±0.5、×2、×1.5、×2÷3 等),然后对每个候选算时值整齐度——把音符时长按这个 BPM 量化,看量化误差有多小。整齐度最高的那个胜出。
思路没问题。问题是评分函数有个盲点。
盲点:BPM 和它的一半,一样整齐
把 BPM 砍一半,所有音符时长正好翻倍——原本是二分音符的变成全音符,四分的变成二分。量化误差完全一样。
也就是说,143.55 和 71.78 这两个候选,用整齐度打分是并列第一。
我的代码在并列时没做任何约束,直接取了它先遍历到的那个。于是:
143.55 → 71.78
整首歌慢了一倍。
这个 bug 很隐蔽:算法每一步都对,输出却错了。整齐度确实是有效指标,只是它对这个特定的变换不敏感。
补一条约束
修法是在并列时加一条偏好:优先贴近音频先验值(也就是 librosa 从原曲直接估出来的那个数)。
因为音频先验虽然不准,但它至少是从真实音频里来的;倍率变换是纯数学操作,没有新信息。两者整齐度打平时,相信有信息来源的那个。
改完之后 143.55 保住了。
唯一需要人填的参数
到现在为止,我在这条链路上做的所有自动估算都可以跑通,但 BPM 是唯一一个我不敢保证的。
原因很实在:音乐本身就有"同一段旋律,记成 70 BPM 的四分音符还是 140 BPM 的八分音符"这种模糊性——记谱层面上两种都对,取决于编曲者怎么想。算法没法替你判断。
所以面板上我把 BPM 做成了橙色必填建议项,配一张说明卡:知道就手填,自动值只是参考。
这是我在这类"自动流程"里学到的:不是每个环节都该自动。 那些在语义上就存在多解的环节,硬要自动就等于在猜,而且猜错了自己还不知道——就像这个 143.55 变成 71.78,输出看着完全正常,只有听的时候才发现整首歌拖了。