Chordity

한국어 · English · 日本語 · 中文 · Français · Español

已经或有意参与协作、做出贡献的朋友,请私下与我联系。本项目是公开源代码、任何人都可以免费使用的软件。用于个人、非营利目的时,可以不付费照原样使用。但著作权和所有权归原作者、协作者和贡献者所有:未经原作者许可,不得修改本程序,也不得将其中一部分拆出放进自己的程序中使用(包括仅供本人使用的情况),不得声称本程序归自己所有,也不得在其他地方分发或销售。也不允许把修改后的内容上传到 GitHub 分叉(fork)中。学校、培训机构、社团、教会、宗教团体、公司等团体使用时并非一律免费,请先与我联系。详细条件见 LICENSE。

Chordity v1.11.10

Chordity 是一个网页:放入歌曲文件,它就会在每个小节上标出和弦,并紧跟歌曲的拍子打节拍器。

在和弦变化的瞬间,钢琴会弹出那个和弦,你可以边用耳朵确认边跟着弹。无需安装,无需注册,也无需联网。音乐文件不会离开你的电脑。

结果可能有误。分析原理、判断依据,以及检查和修正结果的方法,都整理在帮助页面中。点击应用最下方的说明文字也能打开。

Chordity: per-measure chord recognition, a beat-locked metronome and synthesized piano chord playback in a single offline HTML file. No libraries, no uploads.

播放中的画面:黄色荧光笔框圈住当前正在响的和弦,框内按已播放的部分填成橙色,该和弦名称被放大,当前拍的斜线加深显示

目录


第 1 部分 轻松了解

适合这些时候使用

使用方法

  1. 用 Chrome、Edge、Safari、Firefox 等浏览器打开 chordity.html 文件。
  2. 点击打开文件(“파일 열기”),或把音乐文件拖到窗口上。稍等几秒,和弦谱就会出现。
  3. 点击播放(“재생”)按钮。节拍器会随音乐在每一拍发出“哒”的一声(小节第一拍是较高的“嘀”;如果和弦变化单位是 ½ 拍,拍与拍之间还会有轻轻的“嗒”),黄色荧光笔框圈住当前正在响的和弦,该和弦名称随之放大。框内像进度条一样按已播放的部分填成橙色;和弦一变,框会像弹簧一样弹跳着移到下一个和弦。原曲在哪里弹和弦,钢琴就在哪里弹出那个和弦(和弦变化或重新弹奏同一和弦时重新弹,在延留的地方则继续鸣响)。

MP3、WAV、M4A、FLAC、OGG 等浏览器能播放的文件都可以。

如何看懂画面

看到的内容 含义
黄色荧光笔框 当前正在响的和弦。从弹奏该和弦的那一拍一直圈到下一个和弦出现的位置(如果延留的灰色和弦延续到下一小节,就圈到那里;换行时每行一段)。和弦一变,框会像弹簧一样弹跳着移到下一个和弦
框内的橙色填充 该和弦到目前为止已演奏的部分(进度条)。填充的末端就是当前播放位置,每拍开始时正好到达该拍斜线的位置
放大的和弦名称 当前正在响的和弦(新弹奏的和弦)的名称。出现时一下子放大,进入下一个和弦后慢慢缩回原来的大小。放大程度以不碰到下一个名称为限
斜线 / 一拍。变深的斜线就是当前拍
粗斜线 重新弹奏和弦的拍(黑字和弦所在的拍)。可以看出和弦是按什么节奏弹的。灰色和弦和 N.C. 的位置通常是普通斜线
C Am G7 和弦名称。位于某条斜线正上方时,表示和弦在那一拍变化
G/B G 和弦,但最低音是 B
小号字、分上下两行排列的和弦名称 一个小节里和弦太多、名称碰到下一个和弦时会缩小字号;仍然放不下时,就把该小节的名称上下两行交替排列(名称的左端就是弹奏该和弦的拍)。在窄屏上,如果小节最后一拍的长名称分两行也放不下,就从次要部分开始省略并加上 …(Bm7/F♯ → Bm7…)。完整名称可以把鼠标悬停其上查看,或在播放时于上方的当前和弦栏中查看
灰色和弦 不重新弹奏,原样延留前一小节的和弦。即使是同一和弦,只要在小节第一拍重新弹奏,就显示为黑字
N.C. 没有和弦的部分(只有鼓或安静的时候)
第 0 小节 歌曲从小节中间开始的部分(弱起小节)
小节上方的 2박 之类标记 只有这一小节的拍数不同(2박 意为“2 拍”)
黑色标签 E 장조로 从这一拍起调性改变(转调)。上方的调性栏中也会显示为 E♭ 장조 → E 장조(장조 即“大调”)。歌曲第一小节会标出起始调性和拍号,不带助词(C 장조 4/4,就像乐谱的调号、拍号)
黑色标签 3/4으로 从这一小节起拍号改变(拍号变化)。上方的拍号栏中也会显示为 4/4 → 3/4 → 4/4
白色胶囊 72 132로 从这一拍起的速度(BPM,四舍五入),位于斜线下方一行。歌曲开始的那一拍只显示起始速度的数字;速度突然变化的地方,在新速度后加上韩语助词 로/으로(意为“变为……”)
白色六边形框 여기까지 63 速度逐渐变化的区段(斜线下方一行)。框覆盖的长度就是变化的区段;播放位置在框内时,已经过的部分会填上荧光笔颜色,并像 현재 67 这样显示当前速度(여기까지 意为“到此为止”,현재 意为“当前”)。填满后会淡化成白色,框末端下一拍的白色胶囊(到达的速度)会短暂亮起
速度(上方栏和播放条) 正在播放位置的速度。速度逐渐变化时会像 120、119、118 … 这样跟着变动。播放条即使滚动也固定在屏幕最上方,始终可见
8B、9A 加小字的名称 选择 Camelot 标记时的和弦名称(见下文“用 Camelot 显示”)
I、vi、V7/ii 之类的罗马数字 选择罗马数字级数标记时的和弦名称(见下文“用罗马数字级数显示”)
圆形指示灯 当前是第几拍。最大的圆表示小节第一拍
上方的“下一个 G”(“다음 G”) 即将出现的和弦
节拍器(“메트로놈”)、和弦演奏(“코드 연주”)按钮 每按一次切换开/关
音乐(“음악”)、咔嗒声(“클릭”)、和弦(“코드”)滑条 分别调节原曲、节拍器和钢琴和弦声音的音量

节拍器听起来不对劲时

大多数情况下会自动对齐,但有些歌曲每个人数拍的方式也不同。请打开拍子不对吗?校正(“박이 안 맞나요? 보정하기”),试着按下面的按钮。按下立即生效。

如果听起来是这样 就按这个按钮
咔嗒声落在拍与拍之间 移动 ½ 拍(“½박 옮기기”)
咔嗒声太密 减半 ÷2(“절반으로 ÷2”)
咔嗒声太稀 加倍 ×2(“두 배 빠르게 ×2”)
是 6/8 感觉的歌,咔嗒声却太多 三个一组 ÷3(“셋씩 묶기 ÷3”)
小节开始的位置偏了一拍 提前一拍(“한 박 앞당기기”)、推后一拍(“한 박 늦추기”)
咔嗒声感觉略早或略晚 咔嗒时机(“클릭 타이밍”)
想把拍子校正恢复到初始状态 撤销拍子校正(“박 보정 되돌리기”)
想把和弦类型等设置恢复到初始状态 恢复默认设置(“설정 기본값으로”)

当前改动了哪些内容,会在同一栏的生效中(“적용 중”)里分成“拍子校正”(“박 보정”)和“设置”(“설정”)两部分显示。即使把这一栏收起,标题旁也能看到摘要。

如果想让和弦显示得更简单,请在和弦类型(“코드 종류”)中选择“基本”(“기본”);如果连一拍之内变化的和弦也想看到,请选择“½ 拍单位”(“½박 단위”)。在 ½ 拍单位下,节拍器也会在拍与拍之间(八分音符的位置)加入轻而高的“嗒”,听起来像“嘀-嗒-哒-嗒-哒-嗒-哒-嗒”。

用不同方式显示和弦名称

在和弦谱正上方的和弦标记(“코드 표기”)条中,可以用三种方式查看同一个和弦。按下后,和弦谱、当前与下一个和弦、文本和弦谱会一同立即改变,所选方式下次打开时也会保留。用键盘时,把焦点放在该条上按 ←、→ 切换。

按钮 示例 适合这些时候
音名(“음이름”) E♭, B♭/D, Cm7 看着和弦直接跟弹,或演奏者之间交流和弦时
Camelot(“카멜롯”) 5B, 6B, 3A⁷ 寻找相配的调性与和弦,或选择 DJ 混音衔接点时
罗马数字级数(“로마 숫자”) I, V⁶, ii⁷/IV 分析歌曲的和声进行,或移到其他调演奏时

下面三张图是同一段 E♭ 大调的 11 个小节分别用各种方式显示的样子。

用音名显示

音名标记:E♭, B♭/D, Cm, B♭m7, E♭7, A♭maj7 …

这是最熟悉的和弦名称。根音写得大,m、sus、dim 等性质写得稍小,7、maj7 等扩展音用上标,复杂的和弦也能一眼读懂。

用 Camelot 显示

Camelot 标记:5B, 6B, 5A, 3A⁷, 5B⁷, 4Bmaj7 …

用 DJ 衔接曲目时使用的 Camelot 轮编号来标记。把调性按五度圈顺序放在 1~12 号位置上,大调一系加 B,小调一系加 A。编号相同或相差一的调性与和弦之间,声音相互协调。

用罗马数字级数显示

罗马数字级数标记:I, V⁶, vi, ii⁷/IV, V⁷/IV, IVᴹ⁷ …

这是和声学教材中使用的罗马数字和声分析。它不用音名标记和弦,而是标出该和弦建立在调内第几个音上,从而揭示歌曲以怎样的骨架行进。调不同时,相同的进行也会显示为相同的数字,便于移到其他调演奏,或比较多首歌曲的进行。

标记 含义
I, IV, V(大写) 大三和弦一系的和弦。数字表示建立在调性第几个音上的和弦
i, ii, vi(小写) 小三和弦一系的和弦
°, ø, +, M 减三和弦(°)、半减七和弦(ø)、增三和弦(+)、大七和弦(M7)
7 七和弦
6, ⁶₄ 三和弦的转位。低音为三音时是 6,为五音时是 ⁶₄
⁶₅, ⁴₃, ⁴₂ 七和弦的转位。分别对应低音为三音、五音、七音时
V/x, vii°/x, ii/x 为了走向 x 而暂时使用的和弦(副属和弦等)。V7/vi 是通向 vi 的属七和弦
♭VI, ♭VII 等 调内音阶以外的级数。例如从同主音小调借来的和弦(借用和弦)等

用钢琴听和弦

打开和弦演奏(“코드 연주”)后,和弦变化的瞬间钢琴就会弹出该和弦。与原曲叠在一起听,就能立刻用耳朵确认标出的和弦是否正确,跟弹时也能作为向导。钢琴会自动对齐歌曲的音高,因此不会与原曲走音。

快捷键

键 作用
Space 播放、暂停
M 打开、关闭节拍器
P 打开、关闭和弦演奏
← → 上一小节、下一小节
Home 回到开头

点击某个小节就会跳到该小节;点击或拖动上方的波形也能跳到想去的位置。韩文输入法状态下快捷键同样有效。

效果好的歌曲与困难的歌曲

有鼓的流行、摇滚、抒情、舞曲、嘻哈、K-pop、原声流行等拍子清晰的歌曲效果很好。曲中速度大幅变化的歌曲、5/4·7/4 之类的变拍子、像 F/G 这样低音不在和弦内的和弦,以及摇滚的强力和弦(E5)也都能识别。像爵士那样张力音和行走低音很多的歌曲、像波萨诺瓦那样以两拍为单位感受的歌曲(小节会被记成两倍长度)、失真极强的金属乐,以及像雷鬼那样拍子可以有两种数法的歌曲,结果可能出错。这时请使用上面的校正按钮。

值得了解的事项

手机画面(深色主题)


↑ 返回目录

第 2 部分 原理

本程序不使用预先训练好的人工智能模型。它依靠把声音的物理性质和音乐规律转化为公式的信号处理与概率模型来工作。因此每一个判断依据都可以解释清楚,并且可以在画面下方的分析信息(“분석 정보”)中直接查看这些依据。

2-1. 把声音变成图

声音是空气的振动,录音文件则是每秒对这种振动测量数万次所得的一串数字。只看数字很难知道响的是什么音,因此要把声音切成非常短的片段,对每个片段计算各种高度的声音混合了多少(频谱)。把它们按时间顺序排列,就得到横轴为时间、纵轴为音高的声音图像(频谱图)。之后的所有分析,都是在读这张图。

这里存在一个无法回避的取舍。片段切得短,就能准确知道声音何时发出,但是什么音会变得模糊;切得长则正好相反。因此找拍时用短片段,找和弦时另用长片段。最低的低音,相邻半音之间的频率差非常小,所以需要最长的片段。

2-2. 寻找拍:新发声瞬间的规律

打鼓或按下琴键时,那一瞬间声音能量会突然增大。这样的瞬间称为起音(onset)。在频谱图中只把比前一刻变大的部分加起来,就得到一条在每个起音处尖尖突起的曲线。

拍就是这些起音以固定间隔反复出现的骨架。首先把曲线沿时间方向平移并与自身重叠比较(自相关),找出重叠得最好的间隔,也就是速度。然后在整首歌中一次性找出“尽可能多地经过强起音,同时间隔不大幅偏离速度”的一串拍(动态规划)。由于不是一个个单独确定拍,而是整体一次性优化,即使夹杂几个弱拍,拍串也不会断开,演奏速度略有起伏也能跟上。这时“固定间隔”的基准不是整首歌的速度,而是以几秒为单位求出的局部速度。因此在加花之类节奏暂时变复杂的地方不会被带偏,而歌曲真正变慢或变快时又能跟上。

2-3. 确认是不是真正的拍

找到规律的拍串并不算结束。常见的错误有两种。

把反拍当成拍。 踩镲把拍与拍之间填得很强时,拍串可能被抓偏半拍。但音乐是有规律的:底鼓、军鼓、低音进入的瞬间,以及和弦变化的瞬间,大多集中在正拍上。因此在找到的拍位置和移动半拍后的位置上比较这些事件的强度,如果移动半拍的一方明显更强,就把整串拍移动半拍。

把速度抓成两倍或一半。 慢抒情歌中八分音符琶音很密时,很容易把每个八分音符都误认为拍。这时把拍隔一个分成两组来看。如果真正的拍上有底鼓和军鼓,中间只有琴键声,那么一组会比另一组强得多。于是丢掉弱的一组,把速度改为一半。反过来,如果每两拍正中间都规律地出现军鼓,说明速度抓得太慢,就改为两倍。加倍这一方向一旦出错,立刻就会出现反拍咔嗒声,因此只在严格得多的条件下才执行。

在真实录音中,踩镲和琴键会填满反拍,两组的强度差可能很小。因此还要看底鼓和军鼓落在哪一组。如果速度正确,底鼓(第 1·3 拍)和军鼓(第 2·4 拍)会落在不同的组;如果被抓成两倍,两者都会落在同一组(真正的拍)。把速度改为一半时,不是把节拍隔一个剔除,而是用新速度从头重新跟踪。因为八分音符层级的跟踪哪怕在曲中某处只滑了一格,剔除后的节拍从那以后就会全部错开半拍。

快歌被抓成一半的两种形态。 160~180 BPM 左右的快歌,很容易把拍抓慢一个层级,也就是抓成一半速度。这时形态取决于慢拍落在哪里。如果慢拍落在军鼓上(第 2·4 拍),每拍上都会均匀地出现军鼓,底鼓则出现在拍与拍之间。如果是速度正确的慢歌,底鼓落在拍上(第 1·3 拍),军鼓隔一拍出现,因此不会出现这种形态。如果慢拍落在底鼓上(半速感觉),拍与拍之间出现的军鼓会和拍上一样多。两种情况都只在速度估计给出几乎同等强度的两倍速度候选时才改为两倍。如果让慢拍一直落在军鼓上,小节第一拍也会错位,一拍内混入两个和弦,就会出现大量七和弦和斜线和弦。

减半这一方向也要再确认一次。如果把八分音符当成了拍,两拍正中间就是十六分音符的位置,底鼓或低音之类的低频击打很少。因此如果两拍之间也经常出现低音,就认为拍是正确的,不减半。只凭奇偶两组的强度差想要减半时,如果军鼓集中在与底鼓相反的那一组(反拍军鼓,backbeat),同样说明拍是正确的,不减半。

识别“军鼓”的方法是物理性的。军鼓底面的响弦振动时,会产生从中频到极高频一起变大的噪声。琴键的音只在少数几个高度上变大,踩镲只在高频一侧变大。因此用两个值的乘积来衡量“中频和高频是否同时变大”,就能单独挑出军鼓。因为两者只要有一个小,乘积就会变小。

2-4. 划分小节

要把拍按几个一组组成小节,就得知道小节第一拍(强拍)在哪里。第一拍会留下痕迹:和弦改变、低音改变、底鼓重重打进,声音有时也会变大。给每一拍的这些痕迹打分,在遵守“小节通常以相同长度反复”这一规律的前提下,找出得分最高的分组方式。4 拍和 3 拍哪一种与痕迹更吻合,也用这个方法决定。如果一拍内分成三份的感觉很强,就视为 6/8、12/8 之类的复拍子。

在鼓打反拍军鼓(backbeat)的歌曲中,底鼓在第 1·3 拍、军鼓在第 2·4 拍这一奇偶形态也被用作强拍的痕迹。在和弦比小节第一拍早一个八分音符变化的切分 riff 中,和弦变化的痕迹会被拉向前一拍,但鼓的奇偶形态不变,因此小节不会错开一拍。另外,3·5·7 拍只有在第一拍的痕迹确实按那个长度反复出现时才会选用。这是为了防止在短歌中仅凭少数几个罕见的大痕迹就看成 7 拍。

2-5. 识别和弦

八度不同的同一个音(低音的 do 和高音的 do)在和声中起相同的作用。因此把所有音折叠进 do、do♯、re … si 这 12 格,就得到那一刻所响各音的指纹(色度,chroma)。如果是 C 和弦,do、mi、sol 三格会很突出。

真实乐器不会只发出一个音。弹 do 时,高八度的 do、再上面的 sol、再上面的 do、mi …… 这样的泛音会一起鸣响。例如 C 和弦中 mi 产生的泛音会填进 si 那一格,不懂泛音的比较方法就会把 C 误认为 Cmaj7。因此和弦的指纹也要把泛音包括在内做好,再拿来比较。

低音(最低的音)单独来看。和弦是 C 而低音是 mi 时,写成 C/E。

每段录音的基准音都略有不同。很多歌曲的 A 音并不正好是 440 Hz。因此先测出整首歌的音平均偏高或偏低多少,按此校正后再放进 12 格。

2-6. 用概率表达音乐规律

如果只在每一拍选出最相似的和弦,旋律的经过音会让和弦每拍都像闪烁一样变来变去。因此把两条常识用概率加进去:和弦不会频繁变化,以及如果变化,最常在小节第一拍变,很少在两拍之间变。综合考虑这些规律和声音的相似程度,一次性选出整首歌中最合理的和弦序列(隐马尔可夫模型与维特比算法)。同时也估计歌曲的调性,给与调性相配的和弦一点加分。

2-7. 不跑偏的节拍器

这个节拍器不是像“每分钟嘀嗒 97 次”那样按固定间隔敲击,而是在找出的每一拍的时刻上敲击。因此演奏稍微变快或变慢也能跟上。

时机也很精确。如果用计时器发出“现在敲!”来发出咔嗒声,电脑繁忙时就会晚几十 ms。因此把咔嗒声提前 0.2 秒预约到播放音乐的音频设备时钟上。音乐和咔嗒声使用同一个时钟和同一条输出路径,因此以单个采样为单位咬合;即使在输出有延迟的设备上,两者也一起延迟,彼此不会错开。

2-8. 用钢琴弹出和弦

如果放入录好的钢琴声音,文件会变大,还需要外部文件。因此钢琴声音是按物理定律直接生成的。钢琴弦不是理想弦,而是坚硬的钢丝,泛音会比精确的整数倍略高一些(非谐性)。琴槌敲击在弦长约 1/7 处,因此在该点不振动的泛音会响得较弱。一个音由两三根弦一起发出,弦与弦之间音高有极细微的差异,声音会缓慢起伏(拍音),并产生先快速衰减、后长久余留的两段式衰减。越高的泛音消失得越快,声音逐渐变得柔和。把这些性质用公式叠加起来,逐个音地合成。

和弦分为左手低音一个音和右手和音来弹。右手选择从前一个和弦出发手移动最少的转位,因此和弦变化时声音也不会忽高忽低。钢琴按照分析中找到的歌曲基准音来调音,并与节拍器预约在同一个音频时钟上,在和弦变化的瞬间准确鸣响。

2-9. 找出调性变化的位置

歌曲的调性体现在“主要使用哪些音”上。E♭ 大调的歌主要用 E♭、F、G、A♭、B♭、C、D,转到 E 大调后则用 E、F♯、G♯、A、B、C♯、D♯。因此在每一拍上把前后几个小节的音分布与 24 个调性的典型分布进行比较,并用概率加入调性不常变化这条规律,找出整首歌中最合理的调性走向。只掠过一两个小节的其他调的和弦(例如 C 大调歌曲中的 E7 → Am)不算转调。使用相同音的关系大小调(C 大调与 A 小调)之间的移动,由于调号不变,也不算转调。

转调位置按拍来细化。如果经由新调的属和弦(例如 E 大调的 B7)转过去,就按照乐谱上更换调号的惯例,把新的主和弦出现的位置当作转调位置。和弦名称的 ♯/♭ 写法也随该处的调性。在 E 大调区段中写成 G♯m7 而不是 A♭m7。


↑ 返回目录

第 3 部分 工作方式

3-1. 整体流程

flowchart TD
  F["音频文件"] --> D["浏览器解码<br/>PCM,设备采样率"]
  D --> PB["播放缓冲区"]
  D --> MO["混合为单声道"]
  MO --> R22["零延迟重采样<br/>22,050 Hz"]
  R22 --> R11["零延迟重采样<br/>11,025 Hz"]
  R22 --> RF["节奏特征<br/>分频带谱通量,音量"]
  R11 --> HF["和声特征<br/>峰值 STFT,调音,色度"]
  RF --> TP["速度估计<br/>自相关梳状得分 × 偏好分布"]
  TP --> DP["DP 节拍跟踪<br/>精细化,整理,延伸"]
  DP --> PC["正拍相位验证"]
  HF --> PC
  PC --> LC["拍层级验证<br/>两倍,一半,4:3,3:2"]
  LC --> A[("分析结果 A")]
  HF --> A
  A --> BT["buildTimeline<br/>校正运算,拍号与强拍 HMM,和弦 HMM"]
  U["用户校正,设置"] --> BT
  BT --> TL[("时间线 TL")]
  TL --> CH["和弦谱画面"]
  TL --> SC["节拍器调度器"]
  TL --> PS["和弦演奏<br/>排列,钢琴合成"]
  PB --> OUT["限幅器,AudioContext 输出"]
  SC --> OUT
  PS --> OUT

分析分为重量级阶段和轻量级阶段。

3-2. 载入与预处理

  1. 浏览器内置解码器把文件解码为 PCM。这一个缓冲区同时用于播放和分析。因为如果分别解码,或经过浏览器的重采样器,两个结果的时间轴可能相差几 ms。
  2. 把各声道取平均得到单声道,并用自行实现的重采样器生成 22,050 Hz(节奏用)和 11,025 Hz(和声用)信号。滤波器核左右对称,因此信号一个采样也不会偏移。
  3. 把最大振幅归一化为 1。

3-3. 节奏特征

步骤 作用 主要数值
短时傅里叶变换 把 22,050 Hz 信号逐段变为频谱 窗 1024 点(46.4 ms),跳步 256 点(11.6 ms,每秒 86.13 帧)
对数压缩 让安静声音的变化也显现出来 log(1 + 1000·|X|)
最大值滤波 把两帧之前的频谱沿频率方向扩宽 ±1 格后再比较 不会把颤音、滑音误认为起音
分频带通量 只把增大的量按频带取平均 低频 21–200 Hz(底鼓、贝斯),中频 200–2,500 Hz(军鼓鼓身、琴键),高频 2.5–10 kHz(踩镲、军鼓响弦)
起音包络 每个频带除以标准差后求和 高频按一半加权(减小反拍踩镲的影响)
音量 每帧的 RMS 用于判定有声区段和显示波形

3-4. 和声特征

步骤 作用 主要数值
高音部峰值 STFT 用于和弦组成音的频谱峰值 窗 2048 点(186 ms),110–4,200 Hz,每帧最多 36 个
低音部峰值 STFT 用于低音和调音 窗 4096 点(372 ms),32–1,400 Hz,每帧最多 24 个
峰值条件 只取明显的峰 相对帧最大值 −45 dB 以上,且为周围 ±6 格平均的 2 倍以上
对数抛物线插值 以比格间距更精细的精度求频率和幅度 使用峰值及其两侧格的对数幅度
基准音估计 把所有峰值的“半音小数部分”在圆上取平均 例:+18 cent → A4 = 444.6 Hz
色度 把校正后的音高累加到 12 格中 越接近半音中心权重越大(cos²)。高音部为 MIDI 48–96,低音部为 28–57
和声变化曲线 前后 4 帧色度的余弦距离 用于正拍相位验证

3-5. 速度与节拍

  1. 有声区段:把相对歌曲的大音量(前 5%)在 −38 dB 以上的区段视为有声。
  2. 速度:用 ±3 秒平均音量去除起音曲线以使其平稳,然后求自相关。在 40–240 BPM 范围内以 0.25 BPM 间隔扫描,把同时考察 1·2·3·4 倍间隔的梳状得分乘以以 110 BPM 为中心的偏好分布,再对最大值附近做抛物线插值。
  3. DP 节拍跟踪:对 Ellis(2007)的方法做了两处修改后使用。间隔与以 8 秒窗求出的局部速度曲线(为滤除加花取 7 秒中位数)比较;惩罚强度与拍周期的平方成正比,使慢歌中同样多少 ms 的晃动也得到同样的惩罚。
  4. 精细化:把每个节拍移到 ±2 帧(±23 ms)内的起音峰值上,并做抛物线插值。起音较弱的节拍用相邻节拍的线性回归来填补。最后校正 6.5 ms 的检测延迟。
  5. 整理:删除有声区段之外的节拍,并在前后以相同间隔接续,覆盖整个有声区段。
  6. 验证:依次进行正拍相位验证和拍层级验证(原理见 2-3)。如果把拍层级改为一半,就用新速度重新执行 3~5,并再次验证相位。如果拍看起来被分成三份,还会检查慢 4/3 倍的速度和快 3/2 倍的速度(1.11.5·1.11.6,见 4-1 的 (9))。

3-6. 拍号、小节、和弦(轻量阶段)

  1. 应用校正运算:把用户按下的 ½ 拍、×2、÷2、÷3 按按下的顺序应用到节拍列表上。÷2 和 ÷3 根据拍的重音来决定保留哪一拍。
  2. 按拍汇总:对拍区间内的色度以中间为重进行加权平均。低音色度减去整首歌中位数的一半,以去除像有音高的底鼓那样一直垫在下面的成分。
  3. 调性与转调:在每一拍上,把前后 ±6 拍的音分布与 Krumhansl–Kessler 调性轮廓进行比较,并用以 12 种调号为状态的 HMM 划分调性区段。短于 24 拍的区段并入相邻区段。如果没有转调,就使用一个按整首歌分布选出的调性。识别出和弦后,再按区段用和弦重新确定调性:连同音分布一起,考察和弦符合哪个调的音阶、主和弦所占的时间、V → I 终止、开头和结尾的和弦,以及四小节乐句第一拍的和弦。
  4. 预备和弦:只用大三和弦和小三和弦快速求解一遍,作为强拍证据。
  5. 细分判定:比较拍内 1/2 处与 1/3、2/3 处的起音强度,判断是否为复拍子。
  6. 强拍 HMM:自动选择拍号(4 拍对 3 拍;复拍子时为 4 对 2),并反映用户对强拍的移动。
  7. 最终和弦 HMM:以 1 拍或 ½ 拍为单位,在包括转位在内最多 289 个状态上用维特比求解。
  8. 小节构成:在每个强拍处划分小节,并一并附上小节内和弦变化的位置与长度、是否延续自前一小节、符合该处调性的音名(♯ 或 ♭)、Camelot 代码和罗马数字级数。画面显示在和弦谱上方和弦标记条中所选的一种,更换标记时不会重新分析。转调边界按拍细化,略微偏向小节第一拍(经由属和弦时则放在主和弦处),并记录是第几小节第几拍。

3-7. 播放与节拍器

3-8. 校正按钮的作用

按钮 内部运算 动作
移动 ½ 拍(“½박 옮기기”) half 用相邻节拍的中点生成新的节拍列表,并重新对齐到起音峰值。超出到音乐开始之前的拍会被丢弃
加倍 ×2(“두 배 빠르게 ×2”) x2 在每两个节拍之间加入中点
减半 ÷2(“절반으로 ÷2”)、三个一组 ÷3(“셋씩 묶기 ÷3”) d2, d3 每 2 个(3 个)保留一个。保留的位置取拍重音之和较大的一侧
提前一拍、推后一拍(“한 박 앞당기기”“늦추기”) downShift ∓1 把小节内的位置编号逐格轮转
拍号(“박자”) meter 用指定的拍数重新求解强拍 HMM

校正以对节拍列表的运算记录形式保存,每次都从头按顺序重新应用。“撤销拍子校正”(“박 보정 되돌리기”)就是清空这份记录。和弦类型、和弦演奏方式、强调强拍(“첫 박 강조”)、自动滚动(“자동 스크롤”)、咔嗒时机(“클릭 타이밍”)不是保存在歌曲上,而是保存在浏览器中的设置,因此用“恢复默认设置”(“설정 기본값으로”)另行恢复。“生效中”(“적용 중”)里只显示这两类中与默认值不同的项目。

3-9. 和弦演奏

步骤 作用
和弦区间 引擎对小节中的每个和弦事件,同时传出组成音(0 = C)和要弹奏的低音。关闭斜线标记时,低音为根音。
击键列表 按弹奏时机生成。“跟随歌曲”(“곡 따라”)在和弦变化处,以及即使是同一和弦但在原曲中重新弹奏的小节第一拍(谱上的黑字)弹奏,在延留处(灰色)则继续鸣响。“和弦变化时”(“코드가 바뀔 때”)对跨小节延续的同一和弦只弹一次,“每小节重弹”(“마디마다 다시”)在小节第一拍重弹,“每拍”(“박마다”)在所有拍上弹奏。在 N.C. 处不弹,并停止前一个和弦。
排列(voicing) 低音在 D2~C♯3,右手把全部组成音密集排列,选择最接近上一排列中心的转位。
合成与缓存 把歌曲中用到的音按首次出现的顺序逐个预先合成(每个音一次)。歌曲的基准音改变时重新生成。
预约 与节拍器相同的调度器把 0.2 秒之后的击键预约到音频时钟上。不应用咔嗒时机校正,按音乐时刻原样弹奏。
声音结束 在和弦区间结束的时刻(下一次击键或 N.C.),像制音器落下一样以 70 ms 的时间常数衰减。
重新开始 从和弦中间开始播放或跳转时,立即把正在进行的和弦弹响一次。
混音 音乐、咔嗒声、和弦各有独立的音量;钢琴混入少量 1.6 秒的房间混响后,施加固定补偿增益(+6.4 dB)。三种声音在输出前经过限幅器(−1 dBFS),无削波地合在一起。

节拍器和和弦演奏也各自分别重新同步。即使开关节拍器或更改咔嗒时机,正在鸣响的钢琴和弦也不会中断。


↑ 返回目录

第 4 部分 深入探究

4-1. 算法与公式

符号

符号 含义
$F = 22050/256 \approx 86.13$ 节奏帧率(帧/秒)
$X(t,k)$ 帧 $t$、频率格 $k$ 处的 STFT 值
$\tau$ 拍周期(以帧为单位)
$b_i$ 第 $i$ 个节拍的时刻(秒)
$m$ MIDI 音符号(实数),与每小节拍数按上下文区分

(1) 零延迟重采样

把输出/输入采样率之比用最大公约数约分,记作 $L/D = f_\text{out}/f_\text{in}$,则第 $n$ 个输出采样在输入中的位置为 $nD/L = i_n + \phi_n$( $i_n$ 为整数部分,$\phi_n$ 为小数部分)。

y[n] = \sum_{j=0}^{2W-1} x[\,i_n - W + 1 + j\,]\; h(\phi_n + W - 1 - j)
h(u) = 2f_c\,\mathrm{sinc}(2f_c u)\,w\!\left(\frac{u}{W+1}\right),\qquad
w(r) = 0.42 + 0.5\cos\,\pi r + 0.08\cos\, 2\pi r
f_c = 0.46\,\min\!\left(1, \frac{f_\text{out}}{f_\text{in}}\right),\qquad W = \left\lceil \frac{10}{2f_c} \right\rceil

由于 $h$ 是偶函数(左右对称),相位为 0,即延迟为 0。相位 $\phi_n$ 用最多 4,096 级的表预先计算,输入位置用整数累加器( $D = qL + r$ )更新,因此不会累积浮点误差。

(2) 实数 FFT

用一次 $N/2$ 点复数 FFT 计算 $N$ 点实数信号。设 $z[m] = x[2m] + i\,x[2m+1]$,$Z = \mathrm{FFT}_{N/2}(z)$,则

X[k] = \underbrace{\tfrac12\left(Z[k] + \overline{Z[N/2-k]}\right)}_{F_e[k]}
     + e^{-2\pi i k/N}\,\underbrace{\tfrac{-i}{2}\left(Z[k] - \overline{Z[N/2-k]}\right)}_{F_o[k]},\qquad 0 \le k \le N/2

$Z$ 的下标按 $N/2$ 为周期来看( $Z[N/2] = Z[0]$ )。

(3) 分频带谱通量与起音包络

Y(t,k) = \ln\!\left(1 + \lambda \cdot \tfrac{4}{N}\,|X(t,k)|\right),\qquad \lambda = 1000
\Delta(t,k) = \max\!\Big(0,\; Y(t,k) - \max_{|\nu|\le 1} Y(t-2,\,k+\nu)\Big)
f_b(t-1) = \frac{1}{|B_b|}\sum_{k \in B_b} \Delta(t,k),\qquad b \in \{\text{low}, \text{mid}, \text{high}\}

结果记录在所比较的两帧( $t$、$t-2$ )的中间 $t-1$ 处。用于节拍跟踪的起音包络如下。

O(t) = \frac{f_\text{low}(t)}{\sigma_\text{low}} + \frac{f_\text{mid}(t)}{\sigma_\text{mid}} + \frac12\,\frac{f_\text{high}(t)}{\sigma_\text{high}}

(4) 速度估计

用 ±3 秒窗的音量进行平准化,并减去平均值。

E(t) = \frac{O(t)}{\sqrt{\frac{1}{|W_t|}\sum_{s \in W_t} O(s)^2} + \epsilon} - \bar E,\qquad W_t = [\,t - 3F,\ t + 3F\,]

自相关除以重叠长度,用 $r(0)$ 归一化后,再以 $[\tfrac14, \tfrac12, \tfrac14]$ 平滑,记为 $\tilde r$。

r(\ell) = \frac{1}{n-\ell}\sum_{t=0}^{n-\ell-1} E(t)\,E(t+\ell)

对每个 BPM 候选 $v$,同时考察拍周期 $\tau_v = 60F/v$ 的 1~4 倍延迟。

J(v) = \pi(v)\cdot\max\!\Big(0,\ \sum_{k=1}^{4} c_k \max_{|\delta| \le \delta_k} \tilde r(k\tau_v + \delta)\Big),\qquad c = \left(1, \tfrac12, 0.33, \tfrac14\right)
\pi(v) = \exp\!\left(-\frac12 \log_2^2\, \frac{v}{110}\right),\qquad
\delta_1 = 0.5,\quad \delta_{k\ge 2} = 0.5 + 0.004\,k\tau_v

以 0.25 为间隔扫描 $v \in [40, 240]$,在最大值处用三点抛物线插值得到带小数的 BPM。

(5) DP 节拍跟踪 (Ellis, 2007)

用宽度 $\sigma = \tau/32$ 的高斯函数平滑起音后得到的局部得分和累积得分如下。

\ell(t) = \frac{1}{\sigma_O}\sum_{i} O(t+i)\,e^{-\frac12\left(\frac{32 i}{\tau}\right)^2}
C(t) = \ell(t) + \max_{\tau(t)/2 \,\le\, d \,\le\, 2\tau(t)}\Big[\,C(t-d) - \alpha \ln^2\,\frac{d}{\tau(t)}\,\Big],\qquad \alpha = \mathrm{clip}\!\left(2000\,T^2,\ 100,\ 1600\right)

$T = \tau/F$ 是拍周期(秒)。由于 $\alpha\ln^2(d/\tau) \approx 2000\,(\Delta t)^2$( $\Delta t$ 为间隔偏差的秒数),惩罚与速度无关,以绝对时间为基准。原方法的固定值 100 是以百分比为基准的,在慢歌中同样的百分比对应长得多的 ms,拍就会被切分音带偏。

局部拍周期 $\tau(t)$ 由 8 秒窗(1 秒间隔)的自相关 $r_w$ 求出,取 7 个窗的中位数以滤除加花之类的瞬时晃动,再逐帧线性插值。

\tau_w = \arg\max_{\ell \in [0.85\tau,\ 1.15\tau]} \Big( r_w(\ell) + \tfrac12\, r_w(2\ell) \Big),\qquad
\tau(t) = \mathrm{clip}\!\Big(\mathrm{interp}_t\big(\mathrm{median}_{7}\,\tau_w\big),\ 0.85\tau,\ 1.15\tau\Big)

回溯的起点是 $C$ 的局部最大值中超过其中位数一半的最后一个点。在路径的首尾,平滑后的局部得分不到整体 RMS 一半的节拍会被裁掉。

前奏网格(1.10.4)。 在乐器较少的前奏中,小节末尾提前打出的重音(第四拍之后八分音符处的贝斯·镲片)比拍与拍之间的起音强得多,动态规划可能把这些重音当成拍,滑向比正段慢百分之几的拍,使小节线偏差约半拍。因此,在歌曲前 48 拍内,如果从某一拍开始、以其后 8 拍的周期等间隔向前反推出的网格与当前拍相差超过四分之一拍,就比较两个网格。标准是从第一个声音到该拍为止的起音与八分音符位置(正拍和反拍都算)吻合的程度;只有反推网格好出 0.15 以上(起音加权平均,宽 30 ms 的高斯)时,才把前奏的拍换成该网格。这时小节线也沿用正段相位的反推结果。如果前奏确实是不同的速度或自由速度,原来的拍与起音更吻合,就保持不变。「マリーゴールド」的前奏(从第一个声音 0.55 秒到乐队进入的 7.34 秒)原先被抓成 0.596 秒间隔,即 3 拍弱起小节 + 2.55·4.94 秒处的小节线,现在改为 0.566 秒间隔的三个完整小节(0.55·2.82·5.08 秒)。这些小节线与前奏 riff 每 2.26 秒(4 拍)重复一次的起点在 5 ms 以内吻合。GTZAN 1000 首中,此规则只对一首雷鬼起作用,得分不变。

前奏局部偏移(1.11.7)。 即使是以固定速度制作的歌曲,如果不用鼓而以钢琴等开头,前奏中提前十六分音符弹出的音可能比拍与拍之间的起音更强,动态规划就可能把这些音当成拍。这时前奏整体的拍数是对的,因此不会触发上面“相差超过四分之一拍”的条件,但拍在前奏中几乎一直提前约 1/4 拍,间隔参差不齐,和弦区间也会错位。因此,如果从某一拍开始其后 8 拍速度均匀(间隔在周期的 ±3% 以内),并且把该网格等间隔向前反推时,当前拍中有偏离超过 0.15 拍的拍,就用前奏起音与十六分音符位置的吻合程度比较两个网格。只有当反推网格更吻合,并且有证据表明这是以固定速度制作的歌曲(前奏起音峰值 30 个以上,反推网格的十六分音符吻合度 0.85 以上)时,才把前奏的拍换成该网格。

(6) 节拍精细化

在 $\pm 2$ 帧内的起音最大值 $j$ 处做抛物线插值。

\hat t = j + \frac12\cdot\frac{O(j-1) - O(j+1)}{O(j-1) - 2O(j) + O(j+1)},\qquad b = \frac{\hat t}{F} + t_\text{cal},\quad t_\text{cal} = 6.5\ \text{ms}
强度不到中位数 0.35 倍的节拍,用左右各 4 个邻居以权重 $1/(1+ j-i )$ 做线性回归来确定位置。有声区段为 $[t_s, t_e]$、拍间隔为 $p$ 时,把节拍裁剪到 $[\,t_s - p/4,\ t_e + p/4\,]$ 内;前方在 $b_0 - p \ge \max(0, t_s - p/4)$ 期间、后方在 $b_\text{last} + p \le t_e - p/2$ 期间,以相同间隔接续。

(7) 正拍相位验证

和声变化曲线 $\nu$ 由经平方根压缩后 L2 归一化的色度 $\hat c$ 构成。

\nu(t) = \left(1 - \cos\!\Big(\sum_{d=0}^{3}\hat c_{t+d},\ \sum_{d=1}^{4}\hat c_{t-d}\Big)\right)\cdot \min\!\left(1, \sqrt{\frac{\min(e_t, e_{t-1})}{e_{70\%}}}\right)

拍重音曲线是用有声区段平均值 $\mu$ 归一化后的四条曲线的加权和。

S(t) = 1.0\,\frac{f_\text{low}}{\mu_\text{low}} + 0.7\,\frac{f_\text{mid}}{\mu_\text{mid}} + 0.2\,\frac{f_\text{high}}{\mu_\text{high}} + 0.8\,\frac{\nu}{\mu_\nu}
\text{on} = \frac1N\sum_i \max_{|\delta|\le 2} S(b_i + \delta),\qquad
\text{off} = \frac1N\sum_i \max_{|\delta|\le 2} S\!\left(\tfrac{b_i + b_{i+1}}{2} + \delta\right)

若 $\text{off} > 1.1\cdot\text{on}$,就把全部节拍移动半拍(节拍数 8 个以上时)。

(8) 拍层级验证

定义底鼓曲线 $K$ 和军鼓曲线 $S_n$。$S_n$ 是中频和高频的几何平均,因此只有两个频带同时升高时才会变大。

K(t) = \frac{f_\text{low}(t)}{\sigma_\text{low}},\qquad
S_n(t) = \sqrt{\frac{f_\text{mid}(t)}{\sigma_\text{mid}}\cdot\frac{f_\text{high}(t)}{\sigma_\text{high}}}

对每个节拍求 $p_i = K + S_n + \tfrac12 S$(±2 帧内的最大值),考察偶数号与奇数号平均值之比。

r_2 = \frac{\max(\bar p_\text{even}, \bar p_\text{odd})}{\min(\bar p_\text{even}, \bar p_\text{odd})}
校正 条件 动作
减半 ( $r_2 \ge 2.4$ 且 $s_{S_n} > -0.1$ ,或同侧偏向)且 $\kappa \ge 2$、BPM ≥ 110、BPM/2 ≥ 55 以拍周期 $2\tau$ 从头重新跟踪
加倍 BPM ≤ 100、2·BPM ≤ 210、$\sigma \ge 1.3$、$\kappa \ge 2$ 加入中点并对齐起音
加倍(后拍型) BPM ≤ 100、2·BPM ≤ 200、$c_2 \ge 0.8$、$\sigma \le 0.8$、$\kappa \le 0.95$、$\lvert d_{S_n}\rvert \le 0.25$ 同上
加倍(半速型) BPM ≤ 100、2·BPM ≤ 210、$c_2 \ge 0.85$、$\sigma \ge 0.9$、$1 \le \kappa \le 10$ 同上
\sigma = \frac{\sum S_n(\text{mid})}{\sum S_n(\text{beat})},\qquad
\kappa = \frac{\sum K(\text{beat})}{\sum K(\text{mid})},\qquad
d_{S_n} = \frac{S_{n,\text{even}} - S_{n,\text{odd}}}{S_{n,\text{even}} + S_{n,\text{odd}}}

mid 是节拍之间的中点,$c_2$ 是速度候选中为当前速度两倍(±8%)的候选得分除以第一名得分的值。后拍型是慢拍落在军鼓上,拍上每拍都均匀出现军鼓( $d_{S_n} \approx 0$ )、拍与拍之间出现底鼓的情况;半速型是拍上有底鼓、拍与拍之间也有军鼓的情况。半速型的 $\kappa \le 10$ 是为了排除拍与拍之间几乎没有低频击打、没有鼓的钢琴曲。减半校正中的 $\kappa \ge 2$ 是确认节拍之间是否为十六分音符位置(低频击打很少)的条件,$s_{S_n} > -0.1$ 是确认不是反拍军鼓(backbeat)的条件(1.10.0)。

同侧偏向是以奇偶节拍中底鼓较强的一侧为 + 来测量的偏向。

s_X = \mathrm{sgn}(K_\text{even} - K_\text{odd})\cdot\frac{X_\text{even} - X_\text{odd}}{X_\text{even} + X_\text{odd}},\qquad
\text{same side} \iff s_K \ge 0.15\ \land\ s_{S_n} \ge 0.15\ \land\ s_\nu > 0\ \land\ r_2 \ge 1.3

在正确的拍上,由于反拍军鼓,军鼓落在底鼓的另一侧,因此 $s_{S_n} < 0$(合成曲目为 −0.28 ~ −0.61);被抓成两倍时,两者都落在真正的拍一侧,因此 $s_{S_n} > 0$。

加倍校正之所以严格得多,是因为风险不对称。错误地减半只会让咔嗒声变稀,而错误地加倍会让新增的咔嗒声全部落在反拍上。

(9) 拍细分判定

把节拍区间 24 等分,在每个相位上对起音最大值取平均得到轮廓 $P(\varphi)$,则

\text{triple} \iff \min\!\big(P(\tfrac13), P(\tfrac23)\big) > 1.15\,P(\tfrac12)\ \ \land\ \ \tfrac12\big(P(\tfrac13) + P(\tfrac23)\big) > 1.1\,\mathrm{median}(P)

4:3 拍层级确认(1.11.5)。 十六分音符很密的慢速 4/4 歌曲,看起来像是每三个十六分音符(附点八分音符)起音就重复一次,受速度偏好分布(以 110 BPM 为中心)影响,可能选出实际速度(例:69 BPM)的 4/3 倍(92 BPM),并被记成一拍分为三份的 12/8。如果拍细分判定为三分,就在当前节拍速度 $b$ 的 3/4 附近(±4%)找到乘偏好分布之前梳状得分 $C$ 最大的速度 $b’$,用它重新跟踪节拍,只有同时满足以下所有条件时才更换。

\frac{C(b')}{C(b)} \ge 0.93,\qquad
\frac{C(b')\,\pi(b')}{C(b)\,\pi(b)} \ge 0.85,\qquad
\frac{P'(\tfrac12)}{\tfrac12\big(P'(\tfrac13) + P'(\tfrac23)\big)} \ge 1.4,\qquad
G = \rho(8) + \rho(16) - \rho(12) - \rho(24) \ge 0,\qquad
\pi(x) = e^{-\frac12\log_2^2(x/110)}

第一个条件的意思是:去掉偏好分布后,慢速度对起音周期的解释与当前速度几乎相同或更好;第二个条件的意思是:在乘以偏好分布的最初速度估计中,两个候选也几乎打平。$P’$ 是用新节拍测得的拍细分轮廓,新拍必须明显分为两个八分音符而不是三连音。$\rho(L)$ 是在把当前拍三等分的网格(十六分音符)上,用前后 4 格高音部·低音部色度距离构成的和声变化序列在延迟 $L$ 处的自相关;因此和声每 8·16 格(4/4 的半小节·一小节)变化时 $G$ 为正,每 6·12 格(12/8 的半小节·一小节)变化时在延迟 12·24 处重复,$G$ 为负。新速度必须在 50~140 BPM。如果重新抓到的节拍只跟着底鼓贴在了反拍上(拍间中点的军鼓比拍上强、底鼓则拍上更强:$S_n(\text{beat})/S_n(\text{mid}) < 0.9$ 且 $K(\text{beat})/K(\text{mid}) > 1$),就移动半拍。最初只设了第一个条件,结果吉他伴奏歌曲和极快的歌曲共 8 首变差,于是依次加入第三、第二个条件加以收窄;第四个条件是不减少被更改歌曲数的安全余量(见 4-6 中 1.11.5 的验证)。

3:2 拍层级确认(1.11.6)。 反过来,八分音符均匀连续的歌曲(独奏吉他伴奏、八拍摇滚等),看起来像是每三个八分音符(附点四分音符)重音就重复一次,可能被抓成实际速度的 2/3 倍,并被记成 12/8·6/8。对未经 4:3 确认更改的歌曲,在当前速度 $b$ 的 3/2 附近(±4%)找到 $C$ 最大的速度 $b’’$ 重新跟踪,只有同时满足以下所有条件时才更换。

\frac{C(b'')}{C(b)} \ge 0.93,\qquad
\frac{P''(\tfrac12)}{\tfrac12\big(P''(\tfrac13) + P''(\tfrac23)\big)} \ge 1.4,\qquad
G \ge 0.05,\qquad
\frac{\bar S(\text{beat})}{\bar S(\text{mid})} \ge 1.05,\qquad
90 \le b'' \le 240

$G$ 与 4:3 确认中的指标相同(这里把当前拍三等分的格是八分音符,因此和声每 8·16 格即 4/4 的一小节·两小节变化时为正)。第四个条件要求重新抓到的节拍位置上的重音 $S$ 比节拍间中点强 5% 以上,用来排除速度对了但节拍贴在反拍上的情况。移动半拍的规则与 4:3 确认相同。新速度慢于 90 BPM 的情况(当前速度低于 60 BPM)大多确实是慢速三连音抒情歌,因此不予考察。最初以范围和 $G$ 条件起步,结果变好的歌曲 26 首、变差的 22 首,于是依次加入得分比、重音比、八分音符划分条件,收窄到没有变差的歌曲为止;后来在和弦评分中出现一首大小调变差的歌曲,于是把速度下限提高到 90 BPM(见 4-6 中 1.11.6 的验证)。

(10) 频谱峰值、调音、色度

用对数幅度 $L_-, L_0, L_+ = \ln X(k-1) , \ln X(k) , \ln X(k+1) $ 得到
\delta = \frac12\cdot\frac{L_- - L_+}{L_- - 2L_0 + L_+},\qquad
\hat f = (k + \delta)\frac{f_s}{N},\qquad
\hat A = \frac{4}{N}\exp\!\left(L_0 - \tfrac14(L_- - L_+)\,\delta\right),\qquad
m = 69 + 12\log_2\,\frac{\hat f}{440}

基准音偏差(以半音为单位)是小数部分 $d_j = m_j - \mathrm{round}(m_j)$ 的圆周平均(低音部 STFT 中 MIDI 40–90 的峰值,权重 $\sqrt{A_j}$ )。

\theta = \frac{1}{2\pi}\mathrm{atan2}\!\Big(\sum_j \sqrt{A_j}\sin\, 2\pi d_j,\ \sum_j \sqrt{A_j}\cos\, 2\pi d_j\Big),\qquad
A4 = 440 \cdot 2^{\theta/12}\ \text{Hz}

用校正后的音高 $\tilde m = m - \theta$ 累积色度。$w(\cdot)$ 分别是高音部和低音部的音域窗。

C(t, q) = \sum_{j:\ \mathrm{round}(\tilde m_j) \bmod 12 = q} A_j\, w(\tilde m_j)\, \cos^2\!\big(\pi(\tilde m_j - \mathrm{round}(\tilde m_j))\big)

(11) 拍同步汇总、低音下限、调性

对拍区间 $[t_0, t_1)$ 内的帧以中间为重取加权平均。

\bar C(q) = \frac{\sum_j \omega_j\,C(j, q)}{\sum_j \omega_j},\qquad \omega_j = 0.1 + \sin(\pi u_j),\quad u_j = \frac{j h - t_0}{t_1 - t_0}

低音色度中常驻成分的去除和调性估计如下。$\vec{K}_\text{mode}$ 是 Krumhansl–Kessler 大调、小调轮廓,$\mathrm{rot}_t$ 是旋转到主音 $t$。

B'_s(q) = \max\!\Big(0,\ B_s(q) - \tfrac12\mathrm{median}_i B_i(q)\Big)
(\hat t, \widehat{\text{mode}}) = \arg\max_{t,\ \text{mode}}\ \mathrm{corr}\!\big(\vec{h},\ \mathrm{rot}_t(\vec{K}_\text{mode})\big),\qquad
h_q = \sum_i \sqrt{\frac{T_i(q)}{\sum_{q'} T_i(q')}} + \frac12\sqrt{\frac{B_i(q)}{\sum_{q'} B_i(q')}}

(12) 和弦模板与发射分数

以泛音偏移 $d = (0, 12, 19, 24, 28, 31)$ 半音、权重 $0.6^{h-1}$ 累加组成音,并加上低音泄漏( $\rho = 0.25$ )。

\tilde T(q) = \sum_{p \in \text{chord}}\ \sum_{h=1}^{6} 0.6^{h-1}\,[\,q \equiv p + d_h\,] \;+\; \rho\sum_{h=1}^{6} 0.6^{h-1}\,[\,q \equiv p_\text{bass} + d_h\,]\pmod{12}
T = \frac{(\tilde T/\max\tilde T)^{\gamma}}{\left\|(\tilde T/\max\tilde T)^{\gamma}\right\|_2},\qquad \gamma = 0.45

观测 $o_s$ 也用同样的方式(最大值归一化、$\gamma$ 次幂、L2 归一化)由高音部色度生成。和弦 $c$ 的发射分数如下。

e_s(c) = \beta\,\langle o_s, T_c\rangle + \pi_q + \pi_\text{inv} + \kappa\,[\,c \subset \text{key}\,]
+ \eta\,g_s\Big(\ln\big(0.06 + \hat b_s(p_\text{bass})\big) - \ln\big(0.06 + \tfrac{1}{12}\big)\Big)

$\beta = 30$、$\kappa = 0.6$、$\eta = 1.5$;$\hat b_s$ 是总和为 1 的低音色度,$g_s = \min!\big(1, E^\text{bass}s / (0.5\,E^\text{bass}{60\%})\big)$ 是低音较弱时缩小低音项的系数。无和弦(N)的分数在色度越平坦、声音越小时越大。

e_s(\mathrm{N}) = \beta\left(\frac{1}{\sqrt{12}}\sum_q o_s(q) - 0.12\right) + Q_s,\qquad
Q_s = \begin{cases} 12 & r_s < 0.02 \\ 6\,(0.08 - r_s)/0.06 & r_s < 0.08 \\ 0 & \text{otherwise}\end{cases}

$r_s$ 是该时隙的高音部能量除以整体第 60 百分位数的值。先验分数 $\pi_q$ 和转位先验分数 $\pi_\text{inv}$ 如下。

性质 maj min 7 maj7 m7 sus4 sus2 dim aug m7♭5 dim7
$\pi_q$ 0 0 −0.4 −0.6 −0.4 −1.2 −1.4 −1.3 −1.8 −1.2 −1.6
低音 根音 三音 五音 七音
$\pi_\text{inv}$ 0 −0.7 −1.0 −1.0

转位只对 maj、min、7、maj7、m7 设置。各词汇的状态数为:基本 73 个、标准 241 个、扩展 289 个(含 N)。

削弱歌声旋律(1.11.0)。 和弦识别出错的最大原因,是歌声旋律混入和声频带,被读成共同音多的相邻和弦(C ↔ Am、G7 ↔ Gm7 等)。流行音乐录音通常把人声放在中间、伴奏向左右展开,因此在左声道减右声道得到的侧信号 $S = (L - R)/2$ 中,人声几乎被消去,伴奏保留下来。于是用侧信号再生成一次高音部色度,加到最终和弦判定的高音部色度上($C_\text{treble} = C_M + C_S$)。低音也在中间,侧信号中会缺少低音音符的泛音,因此从区间低音色度中把强音的高音部成分以一半的比重补回来。这样混合后的色度只用于选择根音和大/小调类别;如果选出的根音·类别与用混合前色度读出的和弦相同,七和弦·转位的名称就采用混合前的结果。拍、强拍、拍号、调性的估计不使用混合。单声道文件或两个声道几乎相同的文件(侧/中能量低于 −30 dB)不生成侧信号,结果不变。立体声文件的分析时间会增加。

对齐重复段(1.11.0)。 流行歌曲中,像第一、二段主歌和副歌那样,同样的和弦进行会反复出现,每次只有旋律和加花不同。在每一拍上用高音部·低音部色度单位向量度量曲内相似度,当以 8 拍窗平均的对角线(相隔 16 拍以上的相同位置)超过曲内前 3%,就把该处最吻合的位置作为这一拍的“重复配对”(只取小节内拍位置相同的配对,每拍最多 6 个)。最后一段副歌升高半音·全音的歌曲,也会按调差移位后进行比较。在最终和弦维特比之前,把每一拍的和弦得分与配对的得分进行加权平均(自身 1,每个配对 0.7 × 相似度),但从配对那里只借用各根音的最高分,大、小、7、sus 之类的性质则由该拍自身的声音决定。这样,因一次旋律而偏向相邻和弦的拍会由其他重复段纠正,而不会把改弹成不同和弦的性质也拉过来。相似度表的大小是拍数的平方,因此超过 3,000 个时隙的超长输入不使用。

半减七和弦的名称(1.11.1)。 标准词汇中没有 m7♭5(加入后其他歌曲的 m7·dim 会改变,整体变差),因此半减七和弦(小调 ii°7、大调 vii°7)被写成了 dim。在和弦与调性全部确定之后,对每个写成 dim 的区间,如果混合前高音部色度中的小七度音(根音 + 10 半音)达到三和弦组成音平均值的 0.5 倍以上,就只把名称改为 m7♭5。根音、大小调、边界、调性均不变。以 GuitarSet 中实际演奏的和弦为基准,性质正确的样本从 59.37% 增加到 60.28%(+97 / −3,流行音乐类为 +69 / −0),被更改的样本大部分是演奏者实际弹奏的 m7♭5。

(13) 和弦维特比:O(T·K)

由于转移得分是“保持为 0,改变为 $-\lambda_s$(与到达的和弦无关)”,只需每一步的一个最大值,就能求出所有到达状态的最佳转移。

V_s(c) = e_s(c) + \max\!\Big(V_{s-1}(c),\ \max_{c'} V_{s-1}(c') - \lambda_s\Big)
时隙位置 小节第一拍 偶数拍子的中间拍 其他拍 拍与拍之间(½ 拍分辨率) 预备和弦(所有拍)
$\lambda_s$ 1.5 3.0 4.0 6.0 3.0

(14) 强拍证据与拍号 HMM

把拍 $i$ 的六个特征分别标准化( $Z$ )后相加。

z_i = 1.0\,Z(h)_i + 1.0\,Z(l)_i + 0.5\,Z(g)_i + 0.6\,Z(k)_i + 0.3\,Z(e)_i + 0.4\,Z(o)_i
符号 特征
$h_i$ 高音部色度变化 $1 - \langle \hat t_i, \hat t_{i-1}\rangle$
$l_i$ 预备和弦是否改变
$g_i$ 低音色度变化
$k_i$ 拍位置上的低频起音强度
$e_i$ 音量增量(dB,只取正值)
$o_i$ 是否新进入主和弦

状态是小节内的位置 $p \in {0, \dots, m-1}$。发射分数在 $p = 0$ 处为 $1.5\,z_i$,在偶数拍子( $m \ge 4$ )的 $p = m/2$ 处为 $0.45\,z_i$,其余为 0。

P(p \to p+1) = 1 - 2\varepsilon,\qquad P(k \to 0) = \varepsilon\ \ (1 \le k \le m-2),\qquad P(m-1 \to m-1) = \varepsilon,\qquad \varepsilon = 3\times 10^{-4}

以很小的概率允许小节缩短和延长,因此遇到变拍子或被截断的小节,路径也不会崩溃。拍号的自动选择依据对比度 $\Delta_m$(被选为强拍的拍的 $z$ 平均 − 其余拍的 $z$ 平均)。候选为 4 拍和 3 拍(复拍子时为 4 和 2),只有在对比度比 4 拍高出 0.25(复拍子 0.3)以上时才选另一方。

鼓的奇偶先验(1.10.0)。 对每一拍把底鼓强度 $K$ 和军鼓强度 $S_n$ 标准化,在前后 8 拍内乘以 $(-1)^j$ 后求和,再除以绝对值之和,得到局部一致度(军鼓取反号)。

c^X_i = (-1)^i\,\frac{\sum_{|j-i| \le 8} (-1)^j X_j}{\sum_{|j-i| \le 8} |X_j|} \in [-1, 1],\qquad
z_i \leftarrow z_i + \mathrm{sgn}(c^K_i)\,\min\big(|c^K_i|, |c^{-S_n}_i|\big)\quad \text{if } |c^K_i|, |c^{-S_n}_i| \ge 0.3,\ c^K_i\,c^{-S_n}_i > 0

这一先验表示:强拍是底鼓一侧的拍(第 1·3 拍),而不是军鼓一侧(第 2·4 拍)。第 1 拍和第 3 拍的区分仍由和声证据决定。只有当底鼓和军鼓都明显交替且彼此相反时才使用,因此像独奏吉他扫弦那样只有一方交替的地方为 0。如果在歌曲 20% 以上的拍上得到这样的确认(有架子鼓),那么在低频被贝斯或切分重音弄模糊的地方,仅凭军鼓一致度(0.5 以上)也会加分。在 GTZAN pop 中,这一先验所指的奇偶与正确强拍的奇偶 99.6% 一致。

拍号选择的增强(1.10.0)。 3·5·7 拍除了满足对比度条件外,还要求强拍证据在该小节长度上的自相关大于 4 拍时才选用;如果在歌曲 30% 以上的部分确认了鼓的反拍军鼓,就去掉 3·5·7 拍候选(曲中拍号变化模型保持不变)。4 拍对 2 拍(半小节)的判定使用加入鼓先验之前的 $z$。这是为了防止底鼓同样抬高第 1·3 拍而使 4/4 被读成 2/4。

\rho_z(m) + \rho_z(2m) > \rho_z(4) + \rho_z(8)

重新审视长度不同的小节(1.11.4)。 在把小节第一拍的和弦提前半拍~一拍弹奏的歌曲中,按拍计算的和声变化 $h, l, g$ 被分散到第四拍和第一拍,强拍证据容易把第三拍当成第一拍,小节线错开半个小节,并夹进 2 拍长的小节。如果固定拍号的求解结果中有长度不同的小节且没有曲中拍号变化,就改用前 2 拍与后 2 拍窗的高音部·低音部色度距离 $h^{(2)}_i, g^{(2)}_i$(提前的变化也会汇集到第一拍)替代按拍的变化,构成新证据重新求解。

z'_i = z_i - Z(h)_i - Z(l)_i - 0.5\,Z(g)_i + Z(h^{(2)})_i + Z(g^{(2)})_i,\qquad
h^{(2)}_i = 1 - \cos\Big(\textstyle\sum_{j=i-2}^{i-1} \hat T_j,\ \sum_{j=i}^{i+1} \hat T_j\Big)
重新求解的结果只有在以下三条全部满足时才采用(适用于自动选择拍号得到 4/4 的歌曲和直接选择 4/4 的歌曲;强拍移动则原样移动修正后的网格)。① 没有一个长度不同的小节(整首歌为同一小节相位);② 无论用原证据 $z$ 还是新证据 $z’$,把整首歌视为同一相位时,该相位的强拍得分都比其他相位高出两个长度不同小节的值($2\, \ln \varepsilon $)以上;③ 同一乐句在 4·8 小节后重复时,新网格所选同位置拍的和声比原网格所选拍更相似的情况,按符号检验 $z \ge 3$ 显著更多(同一乐句的小节相位也应相同,这说明原解的相位翻转是假的)。如果无条件采用,在 GTZAN 中虽然强拍变好的歌曲(71)更多,但也有变差的歌曲(34),因此收窄条件,使现有歌曲一首也不变差。

歌曲开头的相位(1.10.3)。 如果歌曲被判定为以弱起小节开始,而前两个小节内夹着一个短小节,且从歌曲开头到该短小节末尾的拍数是小节长度的倍数,那就是把强拍相位晚抓了一拍、再用短小节凑齐的情况(假弱起小节 + 假“3 拍”小节)。这时从歌曲开头起按完整小节重新计数。GTZAN 流行 6 类的不规则小节从 711 个减少到 695 个,强拍 F 值不变。

(15) 节拍器调度

如果在音频时钟的 $t_s$ 时刻从歌曲位置 $o_s$ 开始播放音乐,那么加上用户校正 $\delta$ 后第 $i$ 个咔嗒声的预约时刻、当前播放位置和画面显示位置如下。

t^\text{click}_i = t_s + (b_i + \delta - o_s),\qquad
\text{pos} = o_s + \max(0,\ t_\text{now} - t_s),\qquad
\text{pos}_\text{display} = \text{pos} - L_\text{out}

调度器以周期 $T_\text{tick} = 25$ ms 醒来,预约满足 $b_i + \delta \le \text{pos} + H$ 的咔嗒声( $H = 0.2$ s,后台标签页为 1.5 s)。由于 $H \gg T_\text{tick}$,只要计时器单次延迟不超过约 175 ms,咔嗒声就早已预约好了。咔嗒声是预先计算好以下波形的缓冲区(强拍 $f$ = 1,850 Hz·$A$ = 0.9,其余 1,250 Hz·0.6,在前 2 ms 加入线性衰减的噪声)。和弦变化单位为 ½ 拍时,在预约拍 $b_i$ 的同时,也在其后的拍间位置 $(b_i + b_{i+1})/2 + \delta$ 预约一个小咔嗒声(2,600 Hz,$A$ = 0.3,衰减 8 ms,噪声减半)。由于是半拍之后的声音,慢歌中也不会迟到;移动位置时,从当前拍后面的咔嗒声开始重新预约(1.10.0)。

x(t) = A\Big[\,0.75\,\big(\sin\, 2\pi f t + 0.3\sin\, 2\pi (2.02 f) t\big)\,e^{-t/16\,\text{ms}}\Big]

(16) 钢琴合成

由 MIDI 音符 $m$ 和歌曲的基准音偏差 $\theta$(半音)确定基频和非谐泛音。

f_0 = 440 \cdot 2^{(m - 69 + \theta)/12},\qquad f_k = k f_0\sqrt{1 + B k^2},\qquad B = 3.5\times 10^{-4}\cdot 2^{(m-60)/24}

由琴槌敲击位置 $x_h = 1/7.5$ 和琴槌毛毡对高频的衰减确定泛音幅度。

a_k = \frac{|\sin(\pi k x_h)|}{k^{0.8}}\cdot\frac{1}{1 + (f_k/f_b)^2},\qquad f_b = \mathrm{clip}\big(2600 + 40(m-60),\ 1800,\ 5000\big)\ \text{Hz}

每个泛音是快速衰减(瞬时音)与慢速衰减(余音)之和,低泛音由多根弦 $s$ 以略有不同的音高 $d_s$ 一起鸣响。

y(t) = \sum_{k}\ \sum_{s=1}^{S_k} \frac{a_k}{S_k}\left(0.60\,e^{-t/\tau^{F}_k} + 0.40\,e^{-t/\tau^{S}_k}\right)\sin\!\left(2\pi f_k\, 2^{d_s/1200}\, t + \varphi_k\right)
\tau^{S}_k = \max\!\left(0.12,\ \tau_1\Big(\frac{f_0}{f_k}\Big)^{0.7}\right),\qquad \tau^{F}_k = 0.15\,\tau^{S}_k,\qquad \tau_1 = \mathrm{clip}\!\left(8.5\cdot 2^{-(m-38)/16},\ 1.0,\ 8.5\right)\ \text{s}

弦数 $S_k$ 在 $k \le 6$ 时为 1(m < 35)、2(m < 47)、3(其他),$k > 6$ 时为 1。各弦的调音差为 $d = (0, +1.0, -0.8)$ cent。泛音只用到第 32 个、8 kHz 为止,降到 −100 dB 以下之后不再计算。起始相位 $\varphi_k$ 用每个音固定的随机数使各泛音不同,但同一泛音的多根弦以相同相位起振(见 4-2 的拍音)。在此基础上加入 30 ms 的琴槌噪声、2 ms 的起音斜坡和末尾 60 ms 的淡出,并调整到前 0.3 秒的 RMS 为 0.12。长度为 $\mathrm{clip}(5.6 - 0.07(m-38),\ 2.6,\ 5.6)$ 秒。整个钢琴通路乘以固定补偿增益 +6.4 dB(×2.09)。正弦不用 Math.sin,而是用旋转递推生成,每 2,048 个采样把幅度复位为 1。

\begin{pmatrix} c_{i+1} \\ s_{i+1} \end{pmatrix} =
\begin{pmatrix} \cos\,\omega & -\sin\,\omega \\ \sin\,\omega & \cos\,\omega \end{pmatrix}
\begin{pmatrix} c_i \\ s_i \end{pmatrix},\qquad \omega = \frac{2\pi f}{f_s}

(17) 排列(voicing)与击键

右手候选是对组成音的每种转位,把最低音放在 F3~E4(53~64)并向上叠置的密集排列。

n_\text{bass} = 38 + \big((p_\text{bass} - 38) \bmod 12\big),\qquad
\hat V = \arg\min_{V}\ \big|\bar V - \mu\big| + 2\max(0,\ V_\text{top} - 77),\qquad
\mu \leftarrow 0.7\,\bar{\hat V} + 0.3\cdot 62

$\bar V$ 是排列的平均音高,$\mu$ 从 D4(62)开始跟随上一排列,但会逐渐被拉向中间。力度为低音 0.62,右手每个音 $0.5\sqrt{3/n}$( $n$ 为右手音数);左右声像为 $\mathrm{clip}((m-60)/36,\ -0.5,\ 0.5)$。击键时刻 $t_j$ 以与 (15) 相同的方式换算到音频时钟上,但不加用户咔嗒校正 $\delta$;从和弦区间结束的时刻 $t^\text{end}_j$ 起,以 70 ms 的时间常数降低音量。

t^\text{piano}_j = t_s + (t_j - o_s),\qquad g(t) = g_0\,e^{-(t - t^\text{end}_j)/70\,\text{ms}}\quad (t \ge t^\text{end}_j)

(18) 转调跟踪

生成节拍 $i$ 的音分布 $h_i$,以及对前后 ±6 拍做三角加权汇总得到的 $H_i$。

h_i(q) = \sqrt{\frac{T_i(q)}{\sum_{q'} T_i(q')}} + \frac12\sqrt{\frac{B_i(q)}{\sum_{q'} B_i(q')}},\qquad
H_i = \sum_{|j-i| \le 6}\Big(1 - \frac{|j-i|}{7}\Big)\,h_j

调号类 $g$(大调主音 $g$,关系小调 $g+9$)的得分是与两个调性轮廓相关系数中的较大者。

c_i(g) = \max\Big(\mathrm{corr}\big(H_i,\ \vec K_\text{major}(g)\big),\ \mathrm{corr}\big(H_i,\ \vec K_\text{minor}(g+9)\big)\Big),\qquad g = 0, \dots, 11

用与和弦维特比相同的 $O(T \cdot 12)$ 方式求解只在调性改变时给惩罚 $\Lambda$ 的 12 状态 HMM。

V_i(g) = c_i(g) + \max\Big(V_{i-1}(g),\ \max_{g'} V_{i-1}(g') - \Lambda\Big),\qquad \Lambda = 6

短于 24 拍的区段并入 $\sum c_i$ 较大的相邻区段。每个区段在大调和关系小调中选择与区段整体分布相关性较大的一方。边界用未经窗口汇总的逐拍得分 $u_j$ 在 ±8 拍内重新寻找。

b^\ast = \arg\max_{|b - b_0| \le 8}\ \sum_{j=b-16}^{b-1}\big(u_j(A) - u_j(B)\big) + \sum_{j=b}^{b+15}\big(u_j(B) - u_j(A)\big) + 0.5\,[\,b\ \text{is a downbeat}\,]

如果 $b^\ast$ 处的和弦是新调的 V、V7、Vsus4,并且 8 拍内接着出现新调的主和弦(大调 I·Imaj7,小调 i·im7),就把边界移到该主和弦处。这样划分出的区段调性,用于和弦发射分数 (12) 中的调性加分和音名标记。

和弦识别结束后,按区段用和弦重新确定调性。对区段的和弦序列(相同和弦合并为一个,长度 $\ell_j$ 拍)和候选调性 $K$,从 24 个调中选出下列得分最大的调性。

J(K) = \frac{\sum_j \ell_j\,[\,c_j \subset K\,]}{\sum_j \ell_j} + 0.5\,\frac{\sum_j \ell_j\,[\,c_j = \mathrm{I}_K\,]}{\sum_j \ell_j} + 0.3\min\!\Big(1, \frac{N_{V \to I}}{3}\Big) + 0.15\,[\,c_1 = \mathrm{I}_K\,] + 0.15\,[\,c_\text{last} = \mathrm{I}_K\,] + 0.4\,\frac{N_\text{phrase}(\mathrm{I}_K)}{N_\text{phrase}} + 1.2\,\mathrm{corr}\Big(\sum_i h_i,\ \vec K\Big)

如果相邻区段重新确定的调相同,就合并为一个区段(1.10.0)。因为划分区段的调号类不同,用和弦重新确定的调却可能相同,若不处理,调性没变也会被记为转调。相邻区段分属关系大小调(G 大调与 E 小调)时,也合并为一个区段,并用合并后区段的和弦重新确定调性。这遵循关系调之间的移动不记为转调的惯例;而这样分开的歌曲属于大小调判断模棱两可的歌曲,因此在合并后的区段中,只有小调得分比关系大调高出 0.2 以上时才记为小调(流行音乐中大调更常见,1.10.0)。

过滤几乎打平的转调(1.11.1)。 按区段用和弦重新确定调性时,如果某区段所选的调在 $J$ 上只比相邻区段的调(或其关系调中在本区段得分较高的一方)领先不到 0.1,就把该区段也视为相邻区段的调。这是为了防止几乎没有根据的转调阻碍关系调合并,进而使整首歌的调名连锁出错。在 1.11.0 中,「고민중독」(Gomin Jungdok)的前奏以 D 大调 1.788 对 G 大调 1.781 被分开,整首歌被记成 B 小调 → E 小调 → F 小调;借助这条规则,恢复为 G 大调 → A♭ 大调(正确答案)。在测试的所有资料中,落在这一差距内的情况只有三次(0.007、0.02、0.036),真正的转调都领先 0.7 以上。

m7 与六和弦重叠时的调名(1.11.4)。 大调歌曲中的 vi7(F♯ 大调的 D♯m7)与 I6(F♯6)组成音相同,仅凭和弦无法区分,但主和弦得分只计入小调一方(D♯ 小调的 i7),因此 vi7 较多的大调歌曲可能被记成关系小调的名称。只有当用和弦选出的调是小调、区段的音分布与关系大调最吻合(24 个调中排第一)、其差距按 $1.2\,\Delta r$ 计超过 0.15(几乎打平幅度 0.1 的 1.5 倍),且该小调根音上的 m7 占区段和弦时间的 1/3 以上时,才把该 m7 也算作关系大调的主和弦重新比较。不会改到其他大调。如果无条件使用,在 GTZAN 中原本正确的 29 首调会被改错(原本错误的 26 首被改对),因此收窄了条件。

$[\,c \subset K\,]$ 表示和弦组成音是否全部属于调性音阶(小调包括导音),$\mathrm{I}K$ 是主和弦(大调 I·Imaj7,小调 i·im7),$N{V \to I}$ 是 V·V7 → I 进行的次数,$N_\text{phrase}$ 是从区段第一个完整小节起每 4 小节的第一拍数。最后一项的比重 1.2 是用 18 首调性明确的歌曲确定的。

转调名称的整理(1.10.3)。 如果歌曲第一个调性区段不超过 8 小节,且是下一区段同类(大·小)的下属调·属调,就并入下一个调。因为前奏从 iv 或 V 开始时,乐句第一个和弦会使前奏单独被定为另一个调,从而记出“B♭ 小调 → F 小调”这样的虚假转调。另外,有转调的歌曲把调名统一为最长区段的类别。关系大小调的组成音相同,只是名称上的选择;若每个区段各自选择,向上一个全音的转调就会被记成“C 小调 → F 大调”,掩盖了究竟改变了什么,因此记为“C 小调 → D 小调”。

转调边界的整理(1.10.4)。 转调边界由音分布窗口决定,因此容易因横跨两个调的音较多的和弦(前一调的 V7 等)而向新调一侧提前一两个小节。在和弦全部确定后,对边界之后 2 小节内的每个小节第一拍,逐拍累加“和弦组成音属于前一调音阶的比例 − 属于新调音阶的比例”,把边界推迟到使前一调的和弦最多地留在前一调中的小节第一拍(仅当好出 0.5 拍以上时)。不向前移动。这是为了不与“经由新调属和弦、从主和弦起记为新调”的位置相冲突。另外,如果新调的第一拍是 N.C.,该小节一半以上是 N.C.(间歇,break),且新调的主和弦尚未出现,就把该小节的声音视为下一乐段的弱起,从下一小节第一拍起记为新调(1.10.5 收窄了条件:短暂停顿后立即弹出新主和弦的小节,从该小节起即为新调)。「夜に駆ける」的 B 小调从第 102 小节(C 小调的 G7)经过第 103 小节(前半小节无声、只有无伴奏的歌声起始音的间歇),移到了伴奏以 B 小调进入的第 104 小节。「고민중독」的 A♭ 大调转调从提前 2 小节的位置移到了提前 1 小节的位置(通向转调的过渡小节)。

(19) Camelot 标记

Camelot 轮把调性按五度圈顺序放在 1~12 号上,大调加 B,小调加 A。大调主音 $t$(C = 0)的编号,以及与关系大调(主音 $t+3$)共用编号的小调编号如下。

n_\text{major}(t) = \big((7t + 7) \bmod 12\big) + 1,\qquad n_\text{minor}(t) = n_\text{major}\big((t + 3) \bmod 12\big)

$7t \bmod 12$ 表示从 C 向上走了几次五度(由于 $7 \cdot 7 \equiv 1 \pmod{12}$,上方五度的调性编号加 1)。再加上 7,使 C 大调成为 8B。

编号 A(小调) B(大调)
1 A♭m (G♯m) B
2 E♭m (D♯m) F♯ (G♭)
3 B♭m D♭
4 Fm A♭
5 Cm E♭
6 Gm B♭
7 Dm F
8 Am C
9 Em G
10 Bm D
11 F♯m A
12 C♯m (D♭m) E

和弦使用根音相同、三音相同的调性的编号,和弦类型以角标保留。没有三音的 sus 和弦放在 B 一侧。

和弦类型 Camelot 代码 示例
大三和弦、7、maj7、aug、sus2、sus4 根音大调的编号 + B C → 8B, G7 → 9B7, Fmaj7 → 7Bmaj7, Esus4 → 12Bsus4
小三和弦、m7、dim、m7♭5、dim7 根音小调的编号 + A Am → 8A, Dm7 → 7A7, Bdim → 10Adim, Bm7♭5 → 10A7♭5

大调 $n$B 的自然音阶和弦中,I·IV·V 为 $n$B·$(n-1)$B·$(n+1)$B,ii·iii·vi 为 $(n-1)$A·$(n+1)$A·$n$A,全部集中在 $n \pm 1$ 之内(12 之后是 1)。

(20) 罗马数字和声分析

以该处的调性(主音 $t$,大调·小调)为基准,把根音 $r$ 的音程 $d = (r - t) \bmod 12$ 换算为级数。小调以自然小调音阶为基准,导音上的减三和弦按惯例不加临时记号,写作 vii°。

$d$ 0 1 2 3 4 5 6 7 8 9 10 11
大调 I ♭II II ♭III III IV ♯IV V ♭VI VI ♭VII VII
小调 I ♭II II III ♯III IV ♯IV V VI ♯VI VII ♯VII
规则 内容
大小写 三音为小三度的和弦(m、m7、dim、m7♭5、dim7)用小写
性质记号 dim·dim7 → °,m7♭5 → ø,aug → +,maj7 → M
转位数字 三和弦为原位(无)、6、⁶₄。七和弦为 7、⁶₅、⁴₃、⁴₂。关闭低音标记时为原位
副属和弦 调外的大三和弦·属七和弦紧接着下行纯五度解决到下一个和弦,而该和弦是主和弦以外的调内三和弦 x 时,记为 V/x、V7/x。小调的 VII → III 也记为 V/III
副导和弦 调外的减和弦进行到上方半音的调内三和弦 x 时,记为 vii°/x、vii°7/x、viiø7/x
副 ii 级和弦 调外的小三和弦·m7·m7♭5 接到下方纯五度的 V/x 时,记为 ii/x、ii7/x、iiø7/x
转调之前 下一区段调性的 V·V7 进行到该调的主和弦时,按新调记为 V

(21) 按曲风的增强

速度跳变。 在每个 8 秒窗(1 秒间隔)中,于整体拍周期 0.6~1.65 倍的范围内求出拍周期得分 $s_w(\ell)$,再用以速度为状态的维特比选择走向。原速度附近(±4%)的得分若达到窗内最大值的 60% 以上就饱和为 1,使其不轻易离开;其他速度按实际得分比较,以免走向 2/3 倍之类的虚假周期。窗与窗之间的移动成本如下。

c(d) = \begin{cases} 0 & d \le 0.005 \\ -6 - 100\,(d - 0.005) & 0.005 < d \le 0.04 \\ -12 - 2d & d > 0.04 \end{cases},\qquad d = \left|\ln\frac{\ell_w}{\ell_{w-1}}\right|

逐渐变化的速度在每窗 0.5% 以内可以免费跟随,而用一连串小步走到远处的路径会比一次跳变更昂贵。如果路径中没有超过 4% 的跳变,就直接使用 (6) 的局部速度曲线;如果有,就在跳变点把区段分开,对每个区段用同样的方式(区段中心速度 ±15%,7 窗中位数)生成曲线。

行走低音的层级。 当速度在 85 BPM 以下,而低频起音的拍上/拍间之比不超过 2.5,且没有反拍军鼓(集中在底鼓另一侧的军鼓,$s_{S_n} \le -0.15$)时,就上调一个层级;这样上调的歌曲在 150 BPM 以下时再确认一次。速度候选为 40~300 BPM。

拍号候选。 单拍子歌曲比较 4·3·5·7 拍。非 4 拍的拍号只有在强拍对比度比 4 拍高出(3 拍 0.25,5·7 拍 0.3)以上,并且被强行拉长或缩短来凑数的小节不超过 15% 时才选用。在 4 拍路径中,如果第三拍的强拍证据达到第一拍的 85% 以上,就视为 2 拍。

强力和弦。 在强力和弦状态的得分中,根音上方 2·3·4·5 半音的色度之和超过根音与五音之和 0.7 倍的部分予以扣分。

e_s(c_5) \leftarrow e_s(c_5) - 6\,\max\Big(0,\ \sum_{d \in \{2,3,4,5\}} o_s(r+d) - 0.7\,\big(o_s(r) + o_s(r+7)\big)\Big)

另外,只有当曲中强力和弦达到 16 拍以上、且占整体 8% 以上时才写出;少于此数时就去掉强力和弦重新判定。这是为了防止抒情歌中的 sus4 零星地被读成强力和弦。

高全音的低音。 设置在大三和弦上放置比根音高全音的低音的状态(F/G、B♭/C,先验分数 −1.6),只有当时隙低音色度的 30% 以上是该音时才予以承认(否则为 −3)。罗马数字级数把低音视为根音,记作 V11;Camelot 则在低音调性的编号后加 11。

转位的整理。 在同一和弦内低音移动三次以上时(行走低音),把短于 2 拍的转位并入前后;以五音为低音的情况视为与根音交替的交替低音而予以合并(整个区间都是五音低音时保留)。抒情歌中一拍长的下行低音(V⁴₂ → iii7)保持不变。

拍号变化。 把强拍模型的状态扩展为(拍号,小节内位置),只在小节结束时以极小的概率 $\mu = 10^{-7}$ 转到其他拍号(3·4·5·7 拍)(1.10.0 中由 $10^{-6}$ → $10^{-7}$:为了不把歌曲结尾处和弦逐拍提前变化的地方读成 3/4)。只有新拍号持续 4 小节以上时才视为拍号变化(一两小节的属于不规则小节);如果没有这样的变化,就不使用该模型的结果,因此拍号恒定的歌曲结果不变。

拍号变化的确认(1.11.4)。 在强拍证据混乱的区段(上声部长时间延续、和声靠低音移动的副歌等),噪声可能恰巧与其他小节长度吻合,使一首始终是 4/4 的歌曲最后的副歌被记成 5/4·7/4。因此对每个改变了拍号的区段,测量逐拍高音部·低音部色度在相隔小节长度 $m$ 的乐句间隔(4 小节·8 小节)的拍之间有多相似,即 $S(m) = \max_{k \in {4, 8}} \overline{\cos}(i,\ i + k m)$;如果主拍号的 $S$ 比新拍号的 $S$ 高出 0.02 以上,就去掉该拍号变化重新求解。因为流行音乐以 4·8 小节的乐句反复,一两小节的间隔无法区分拍号。为了能看到两次 8 小节乐句的重复,只在区段按主拍号计有 16 小节以上时使用(在 30 秒片段中区段较短,重复平均值不稳定,曾有歌曲的强拍因此变差)。

速度显示。 对每一拍取前后 4 拍间隔的中位数求速度,用于播放时画面上方的 BPM。在歌曲第一拍显示前 16 拍的速度;其他地方,在前后 8 拍的速度突然变化超过 12%、且新速度(后 16 拍)与上一个显示相差超过 8% 的位置加上标记。标记位置是差值开始超过最大值一半的那一拍;如果两拍之内有小节第一拍,就对齐到那里。

逐渐变化的速度。 寻找用前后 8 拍间隔中位数平滑后的速度比 4 拍前持续朝同一方向变化超过 1%、合计变化超过 8% 并跨越 16 拍以上的区段(包含突变位置的区段除外)。平滑后的曲线在歌曲末尾跟随得较慢,因此如果在区段末尾之后 16 拍内每拍速度又变化超过 1%,就把那里视为终点。由于起点向前延长 4 拍、终点向后延长,相邻区段可能重叠:同方向重叠或相接时合并为一个区段;反方向重叠时,在重叠拍内的速度极值点处分开,再重新确认上述条件(16 拍、8%、无突变位置)(1.10.0)。在和弦谱上,从区段起始拍到终点拍之前覆盖一个六边形框,在终点拍放置显示到达速度的白色胶囊。播放时框会填充到播放位置,到达终点拍后,填充在 0.8 秒内按 $(1-u)^2$ 淡化($u$ 为到达后经过的时间 ÷ 0.8 秒)。到达胶囊在到达拍上亮起以下时长后立即熄灭($\Delta b$ 为到达拍的长度)。

T_{\mathrm{on}} = 1.1\,\Delta b + 0.06\,\mathrm{s}

1.1 倍使延伸到下一拍的程度与速度成正比,0.06 秒是固定的余韵,使熄灭的瞬间看起来不与下一拍的开始重叠。在 100 BPM 时余韵为 0.12 秒;歌曲越慢略长,越快略短。

r_j = \left|\ln\frac{\mathrm{med}\{\Delta b_{j-8}, \dots, \Delta b_{j-1}\}}{\mathrm{med}\{\Delta b_{j}, \dots, \Delta b_{j+7}\}}\right| \ge 0.12

(22) 延续与变化位置

延续(灰色)。 当小节第一拍的和弦与前一格相同时,比较其组成音的强度:拍之前约 0.12 秒的平均值与拍之后约 0.12 秒的最大值(高音部色度看全部组成音,低音色度看低音)。只要任一方跳升超过 1.2 倍,就视为重新弹奏,写成黑字;否则视为延留,写成灰色。

r = \frac{\max_{t \le \tau \le t + 0.12}\ \sum_{k \in \mathcal{C}} c_\tau(k)}{\operatorname{mean}_{t - 0.12 \le \tau < t}\ \sum_{k \in \mathcal{C}} c_\tau(k) + \epsilon} \ge 1.2

弱拍变化的整理。 在偶数拍子的弱拍(第 2·4 拍)上被判定为和弦变化时,如果紧邻的前一个强拍上只属于旧和弦的音没有被重新弹奏(上述比值低于 1.3),并且在该强拍上新和弦的得分并不比旧和弦低 2.5 以上,就把变化移到前一个强拍。这是为了在分解和弦伴奏中,Esus4 → E 的三音(G♯)到第 4 拍才响、因而被晚一拍判定的情况,按乐谱惯例写在第 3 拍上。小节第一拍的边界,以及旧和弦被重新弹奏或仍在鸣响的边界,不做移动。

小节第一拍的一拍和弦(1.11.8)。 最终 HMM 对小节内的和弦变化施加惩罚(第二拍为 4.0),因此只在小节第一拍响的一拍和弦,即使在该拍得分领先,也可能被并入后面拍的和弦。因此,当从小节第一拍起写的是同一个和弦 $Y$ 时(不是从前一小节延续来的和弦,且第二拍也是同一和弦),找出只看第一拍时最吻合的、根音不同的和弦 $Z$;若满足以下全部条件,就把第一拍写成 $Z$。$E_b$ 是拍 $b$ 的和弦得分,$r$ 是上面延续判定中的强度比(拍后最大值相对拍前平均值),$\mathcal{C}_Z \setminus \mathcal{C}_Y$ 是只属于 $Z$ 的音。

E_b(Z) - E_b(Y) \ge 3.5,\qquad
E_{b+1}(Y) > E_{b+1}(Z),\qquad
r_b(\mathcal{C}_Z \setminus \mathcal{C}_Y) \ge 1.2,\qquad
r_{b+1}(\mathcal{C}_Y \setminus \mathcal{C}_Z) \ge 1.2,\qquad
\mathrm{bass}(Z) \ne \mathrm{bass}(Y)

第一、二个条件表示第一拍上 $Z$ 更好、第二拍上 $Y$ 更好;第三、四个条件是两个和弦确实分别弹奏的证据(第一拍上只属于 $Z$ 的音新响起,第二拍上只属于 $Y$ 的音被再次弹奏)。如果只设得分条件,就连前一小节和弦的余响与新和弦混在一起、使第一拍看起来像另一个和弦的情况也会被改动;用 GuitarSet 的正确答案检查后发现,这样的情况反而更常见(见 4-6 中 1.11.8 的验证)。

和弦名称的整理(1.10.0)。 确定调性后,边界保持不变,只整理名称。调内的 m7 以三音为低音持续时(Am7/C、Em7/G、Bm7/D),它等同于由相同音构成的、低音上的六和弦(C6、G6、D6),因此按乐谱惯例写成低音上的大三和弦(C、G、D)。只有当该音在低音色度中按区间总和最强时才更改;如果紧邻前后有同一个 m7 以不同低音相连(Gm7/B♭ → Gm7),说明只是同一和弦内低音在移动,保持不变。可能听错低音的地方也保持不变:该音不到区间低音的一半,且每拍最强音为该音的格不到 3/4 时(多个音混在一起而模糊),不做更改;夹在相同和弦之间的短暂 m7(Em7 – F♯m7/A – Em7)也是经过和弦,不做更改。对于 sus4,如果区间高音部色度中三音(大·小中较大者)达到四音的 0.7 倍以上,就写成同根音的三和弦。这是为了不把混在旋律或有音高的鼓中的四音读成 sus4。在正确答案为 sus4 的已知区间(合成曲目、GuitarSet 演奏正确答案、真实录音)中,这一比值没有超过 0.65。

小节末切分的整理(1.10.3)。 在小节最后一拍被判定为变化的和弦,如果以相同根音一直延续到下一小节第一拍,多半是提前弹奏的切分(提前一个八分音符的扫弦)或低音的经过性接近音。乐谱会把这样的变化写在下一个第一拍上,因此如果该拍上新和弦的得分领先旧和弦不超过 6.5,就把变化移到下一个第一拍。这一阈值是把 4 首真实录音中的 11 个候选位置与对齐的和弦谱对照得出的(移动的 7 处中有 6 处与和弦谱相同,确实在该拍变化的 2 处保持不变)。

4-2. 物理世界

声音、音高、平均律

声音是空气压力的振动,音高是振动的频率。十二平均律把一个八度(频率 2 倍)分为 12 个相等的比。

f(m) = 440 \cdot 2^{(m - 69)/12}\ \text{Hz},\qquad
m(f) = 69 + 12\log_2\,\frac{f}{440},\qquad
\text{cent}(f_1, f_2) = 1200\log_2\,\frac{f_1}{f_2}

八度为频率 2 倍这一事实,是把各音折叠进 12 格(色度)的依据。由于老录音的磁带速度、乐器调音、有意为之的调音方式等原因,录音的基准音常常偏离 440 Hz。例如 +18 cent 就是 $440 \cdot 2^{0.18/12} \approx 444.6$ Hz。

泛音

弦或管的振动会同时发出基频 $f_0$ 的整数倍 $h f_0$ 成分(泛音)。泛音与基音相距 $12\log_2\, h$ 个半音。

泛音 $h$ 半音距离 最近的平均律音 差值 模板权重
1 0 0(同音) 0 cent 1
2 12.00 12(八度) 0 cent 0.6
3 19.02 19(八度上方五度) +2.0 cent 0.36
4 24.00 24(两个八度) 0 cent 0.216
5 27.86 28(两个八度上方大三度) −13.7 cent 0.13
6 31.02 31(两个八度上方五度) +2.0 cent 0.078
7 33.69 34 −31.2 cent 不使用

第 7 泛音偏离平均律达 31 cent,会错误地落入其他音格,因此从模板中去掉。把泛音纳入模型的效果很大。即使只弹 C 大三和弦(C、E、G),E 的第 3 泛音是 B,G 的第 3 泛音是 D,忽略泛音就会看起来像 Cmaj7 或 Cadd9。在小三和弦 Cm 中,C 的第 5 泛音形成 E(大三度),看起来会偏向大调一侧。模板必须把这些成分也包含进去,比较才公平。

各乐器的频带

声音 主要频带 物理特征 在分析中的用途
底鼓 约 40–100 Hz 短促的低音轰鸣,音高迅速下降 低频通量 → 拍、强拍证据
贝斯 约 41 Hz(E1)–400 Hz 整数倍泛音,持续时间长 低音色度 → 转位(斜线和弦)
军鼓 鼓身约 150–250 Hz,响弦噪声约 2–10 kHz 宽频带噪声 中频与高频同时上升 → 反拍军鼓(backbeat)
踩镲 约 5–15 kHz 金属性噪声 高频通量(在拍判定中降低权重)
钢琴、吉他、人声 基音约 80 Hz–1 kHz,泛音达数 kHz 整数倍泛音 高音部色度 → 和弦

时间与频率的取舍

窗长 $T$ 的频率分辨率大约为 $1/T$,理论下限为 $\Delta t\,\Delta f \ge 1/(4\pi)$(高斯窗时取等号)。而半音间隔与频率成正比。

\Delta f_\text{semitone}(f) = f\,(2^{1/12} - 1) \approx 0.0595\,f

在贝斯最低音 E1(41.2 Hz)处,半音间隔只有 2.45 Hz,要区分就需要 $T \gtrsim 1/2.45 \approx 0.41$ 秒。在 A2(110 Hz)处为 6.5 Hz,在 A4(440 Hz)处为 26.2 Hz,就宽裕了。因此按用途分别使用不同的窗。

用途 采样率 窗 窗长 跳步 格间距
节奏(起音) 22,050 Hz 1024 46.4 ms 11.6 ms 21.5 Hz
高音部(和弦) 11,025 Hz 2048 185.8 ms 23.2 ms 5.38 Hz
低音、调音 11,025 Hz 4096 371.5 ms 46.4 ms 2.69 Hz

低音窗略短于 0.41 秒,但一次响起的低音通常只有一个,因此用对数抛物线插值以远比格间距精细的精度估计峰的位置,加以弥补。

采样与混叠

采样率 $f_s$ 无法容纳高于 $f_s/2$(奈奎斯特频率)的成分;如果直接降采样,高频成分会折叠进低频(混叠)。因此重采样器同时充当在新奈奎斯特频率的 92% 处截止的低通滤波器。22,050 Hz 信号保留到约 10.1 kHz,涵盖踩镲频带(2.5–10 kHz);11,025 Hz 信号保留到约 5.07 kHz,涵盖高音部峰值范围(至 4.2 kHz)。所需信息都在这以下,因此可以减少采样数,大幅降低计算量。

音量、混响、对数

人的感知

钢琴琴弦

这是和弦演奏中的钢琴合成所模拟的物理现象。

现象 物理 合成中的处理
非谐性 由于钢弦的弯曲刚度,泛音越高就越高于整数倍。$B = \pi^3 E d^4 / (64 T L^2)$( $E$ 杨氏模量,$d$ 直径,$T$ 张力,$L$ 长度) $f_k = k f_0\sqrt{1 + Bk^2}$,音区越高 $B$ 越大
敲击位置 琴槌敲击在弦长约 1/7~1/9 处时,以该点为波节(不动点)的泛音几乎不响 $\lvert\sin(\pi k x_h)\rvert$ ,$x_h = 1/7.5$
琴槌毛毡 柔软的毛毡短暂接触,滤掉极高的成分 $1/(1 + (f_k/f_b)^2)$
双重衰减 一个音的多根弦通过琴马交换能量,先快速衰减,后长久余留 瞬时音 60%,余音 40%
拍音 一个音的几根弦调音彼此相差 1 cent 左右,声音缓慢起伏 低泛音用 2~3 根弦合成,同一泛音的各弦从相同相位起振,使其逐渐错开
各泛音的衰减 泛音越高能量损失越快,声音随时间变暗 $\tau_k \propto (f_0/f_k)^{0.7}$
各音区的持续 低音弦又长又重,鸣响更久 基音余音 1.0~8.5 秒

延迟与距离

4-3. 体系

组成

flowchart LR
  subgraph UI["UI 脚本"]
    LD["载入<br/>选择文件、拖放、解码"]
    CT["校正、设置控件"]
    RD["渲染器<br/>和弦谱、指示灯、波形"]
    PL["播放器与调度器"]
    PN["和弦演奏<br/>排列、钢琴合成缓存"]
  end
  subgraph EN["引擎脚本 Engine"]
    AN["analyze"]
    BT["buildTimeline"]
    TX["chartText"]
  end
  LD -->|PCM 声道| AN
  AN -->|分析结果 A| BT
  CT -->|设置| BT
  BT -->|时间线 TL| RD
  BT -->|拍、强拍| PL
  BT -->|和弦区段| PN
  BT --> TX
  PL --> AO["限幅器、AudioContext 输出"]
  PN --> AO

仓库结构

.
├── chordity.html             整个应用:引擎脚本 + UI 脚本 + 样式(单个文件,约 230 KB)
├── information.html          帮助页(韩语):判断依据、分析原理与公式、验证结果(单个文件)
├── LICENSE                   使用条款
├── README.md
├── README.{en,ja,zh,fr,es}.md  README 译文:English, 日本語, 中文, Français, Español
└── docs/
    ├── screenshot-desktop.png
    ├── screenshot-mobile.png
    ├── notation-name.png
    ├── notation-camelot.png
    └── notation-roman.png

引擎 API

chordity.html 的第一个 <script> 定义全局 Engine(在 Node 中调用时也通过 module.exports 导出)。以下说明用于本项目的开发与验证。把引擎拆出来在自己的程序中调用,属于修改·部分使用,未经原作者许可不得进行(LICENSE 第 2 条第 가 项)。

// buf: AudioBuffer (예: AudioContext.decodeAudioData 결과)
const channels = Array.from({ length: buf.numberOfChannels }, (_, c) => buf.getChannelData(c));

// 무거운 단계: 한 번만
const A = await Engine.analyze(
  { channels, sampleRate: buf.sampleRate },
  (progress, stage) => console.log(Math.round(progress * 100) + '%', stage ?? '')
);

// 가벼운 단계: 설정이 바뀔 때마다
const TL = Engine.buildTimeline(A, { vocab: 'extended', res: 2, slash: true });

console.log(Engine.chartText(TL, { title: '내 노래' }));
// # 내 노래  |  BPM 97.1  |  박자 4/4  |  조성 C 장조
//   1 | C . / . / . / . | G . / . / . / . | ...

// 카멜롯 표기로 내보내기: 조성은 C 장조(8B), 코드는 8B, 9A7 처럼
console.log(Engine.chartText(TL, { title: '내 노래', notation: 'camelot' }));

// 로마 숫자로 내보내기: I, V7/vi, ii65 처럼(자리바꿈 숫자는 이어서 적음)
console.log(Engine.chartText(TL, { title: '내 노래', notation: 'roman' }));

设置(Engine.defaultSettings())

键 值 默认值 含义
ops 'half'、'x2'、'd2'、'd3' 组成的数组 [] 按顺序应用节拍校正运算
meter 'auto' 或整数 'auto' 每小节拍数
downShift 整数 0 强拍移动(正数为推后)
vocab 'basic'、'standard'、'extended' 'standard' 和弦词汇
res 1 或 2 1 和弦变化单位(1 拍、½ 拍)
slash 真、假 true 低音(转位)标记

分析结果 A 的主要字段

字段 内容
duration, sampleRate 原始长度(秒)、原始采样率
fl, fm, fh, rms, O 低·中·高频通量、音量、起音包络(以节奏帧为单位)
nov, S, drum 和声变化、拍重音、底鼓·军鼓曲线
audible, startT, endT 有声区段
harm tuning(半音)、treble、bass 色度及各自的跳步
tempo bpm、tau、靠前的候选 cands
beats 自动节拍时刻(秒)
phase, level 相位验证、拍层级验证的判定值与依据
level43 4:3·3:2 拍层级确认:triple(拍是否看起来被分成三份)、applied;看起来分成三份时有 curBpm 和各候选的判定值 alts;更换时有 ratio(4/3 = 变慢,2/3 = 变快)、toBpm、flip(移动半拍)
elapsed 分析时间(秒)

时间线 TL 的主要字段

字段 内容
empty 是否因节拍不足而无法生成和弦谱
beats, pos, barOfBeat 节拍时刻、小节内位置(0 为强拍)、节拍所属的小节
bars[] num, pickup, t0, t1, startBeat, nBeats, events[]
events[] off、dur(以拍为单位)、tied、pcs(组成音,0 = C)、bassPc(要弹奏的低音)、label(root, base, ext, bass, text, ascii, nc, cam:Camelot code·ext·text·ascii,rn:罗马数字级数 acc·num·qual·fig·sus·sec·text·ascii)
bpm 用节拍间隔中位数计算的速度
meter m、triple、label(例:4/4、12/8)、contrast、alt
key 起始调性:tonic、mode、score、name(例:E♭ 장조,即 E♭ 大调)
keys[] 调性区段:name、cam(Camelot 代码)、tonic、mode、startBeat、barNum(小节编号)、beat(第几拍)、t0(秒)

三个时钟

时钟 基准 用途
采样时间 解码后缓冲区的采样编号 ÷ 采样率 分析、所有节拍时刻(秒)
音频时钟 AudioContext.currentTime 音乐播放与咔嗒声预约
画面时钟 requestAnimationFrame 当前和弦框与名称放大、指示灯、播放位置显示

节拍时刻按采样时间计算,再用 4-1 中 (15) 的公式换算到音频时钟。播放缓冲区和分析信号来自同一个解码结果,且重采样器延迟为 0,因此两条时间轴之间没有偏移。

当前和弦框与名称放大(1.10.1)。 在画面时钟的每一帧,根据播放位置所在的拍及其在拍内的位置(相对拍长的比例)找到小节内的和弦。播放中的位置减去输出延迟,但不早于开始播放的位置,因此移动位置后不会短暂闪现前面的和弦。和弦单位是新弹奏的和弦(黑字、N.C.)及其后延留的灰色和弦,可以跨小节延续。只在单位改变时更新画面。第一个名称在 0.1 秒内放大(CSS 过渡,离开时用 0.7 秒缩小);倍率不超过 1.3 倍(两行小节为 1.15 倍),且小于“到下一个名称为止的剩余空间 ÷ 名称宽度”,因此放大的名称不会碰到相邻名称。放大的基准轴是名称左侧(保持在拍的位置),高度在单行小节为 45%(上方一行的胶囊与播放时变长的斜线之间),在两行小节为上端。荧光笔框每行和弦谱一个;对单位所跨的每一行,从第一个名称前 7px(跨行延续时为行的左端)圈到下一个和弦名称前 2px(小节末尾时为小节线紧前,歌曲末尾为双小节线前,延续到下一行时为行的右端)。框的两端 $x$ 各自按阻尼振子 $\ddot x = \omega^2 (x^\ast - x) - 2\zeta\omega\dot x$ 移向目标 $x^\ast$。前进方向的一端以 $\omega = 24\ \mathrm{rad/s},\ \zeta = 0.42$ 越过目标约 23% 再返回,另一端以 $\omega = 16\ \mathrm{rad/s},\ \zeta = 1$(临界阻尼)不越过目标,跟在后面约 0.6 秒时停下。由于跟随的一端不会越过目标,即使移到短和弦上两端也不会交错。新出现的行上的框从和弦起点弹出;单位离开的行上的框向前进方向收拢并逐渐变淡,两端相遇时消失。越过歌曲末尾时向前收拢。重新绘制和弦谱之后(更改标记、画面宽度)以及在减少动态效果的设置下,框和名称会立即归位。框内的进度填充(橙色)从片段左端填到播放位置。播放位置的 x 是该小节的拍位置(斜线位置)按拍内位置(相对拍长的比例)分割的地方(1.10.2);如果单位是从前一行延续来的,前一行的片段会填满到底。填充以框内坐标绘制,因此即使框在以弹簧方式移动,填充末端也停留在播放位置。框的颜色为浅色主题 #ffed85、深色主题 #4b4219,填充为浅色主题 #ffc061、深色主题 #5e3110;包括灰色和弦和 N.C. 的灰色(浅色 #4e5662,深色 #aab1bb)在内,在填充、框和和弦谱底面上的任何位置都能以 4.5:1 以上的对比度读出。因此在框移动期间,或在名称比框长的两行小节中,也不改变文字颜色。

音频图

flowchart LR
  M["音乐缓冲区"] --> GM["音乐音量"]
  C["点击音缓冲区"] --> GC["点击音音量"]
  P["钢琴音<br/>每个音的力度、左右声像"] --> PB["钢琴总线"]
  PB --> MK["补偿增益 +6.4 dB"]
  PB --> RV["房间混响 1.6 秒"] --> W["混响量 0.22"] --> MK
  MK --> GP["和弦音量"]
  GM --> L["限幅器 −1 dBFS"]
  GC --> L
  GP --> L
  L --> O["输出"]

设置保存

节拍器开关、强拍重音、和弦名称的标记方式(音名、Camelot、罗马数字级数)、和弦演奏开关与弹奏时机、音乐/点击音/和弦音量、点击音时机、自动滚动、主题、和弦词汇、分辨率、斜线标记,都保存在 localStorage 的 chordity-prefs-v1 中(若存在旧名称下的保存值,会迁移过来)。即使在无法保存的环境中,应用也能照常运行。

4-4. 问题与解决

以下是开发过程中遇到的问题及其解决办法。数值通过合成曲目回归测试和真实浏览器测试(4-6)确认。

问题 原因 解决 结果
慢歌被识别为两倍速度,每个八分音符都响一次点击 密集的八分音符琶音,以及偏好 110 BPM 的先验分布 通过检查鼓声层奇偶拍的不对称来做减半校正 68 BPM 抒情曲:133 → 67.1 BPM
快歌被识别为一半速度 只把底鼓识别为拍 检查“拍上底鼓、拍间军鼓”的交替来做加倍校正 176 BPM 朋克:88 → 176 BPM
速度本来正确的钢琴曲有被错误减半的风险 左手低音只在第 1、3 拍出现,奇偶拍不对称 减半校正只在 110 BPM 以上进行 72 BPM 钢琴曲保持不变
真实的抒情曲被识别为两倍速度,和弦每半拍就变一次(「세월이 가면」(Sewori gamyeon,《岁月流逝》):135 对 68 BPM) 踩镲和键盘填满了反拍,奇偶强度比为 1.59,未达到阈值 2.4 增加一个条件:看底鼓和军鼓是否集中在同一侧(若拍子正确,由于反拍军鼓,两者应在相反一侧) 校正为 68.1 BPM,合成曲目的判定全部不变
减半校正后出现假的 3 拍、2 拍小节 八分音符跟踪在加花处滑了一格,而逐个隔拍剔除节拍会让之后的全部受影响 不再剔除,而是按新的拍周期重新跟踪 不规则小节 5 → 1
慢歌中拍子被切分音和加花拉扯而摇摆 间隔惩罚(固定 100)以百分比为基准,慢歌中超过 100 ms 的摇摆也被廉价地容许 与局部速度曲线比较,并把惩罚改为以绝对时间为基准(α = 2000·T²) 分段直线残差 RMS 96~222 → 6~23 ms,渐慢测试曲 80/80
快速摇滚被识别为一半速度,而且拍落在军鼓(第 2、4 拍)上,和弦以半拍错位混在一起(178 BPM 的曲子识别为 89 BPM) 贝斯的八分音符填满了低频,底鼓被检测得较弱,两个层级的速度得分又相近 根据“拍上军鼓、拍间底鼓”的形态以及两倍速度候选的强度来加倍(反拍型) 177.9 BPM,强拍 F 0 → 0.95,和弦根音 62% → 86%
半拍感(halftime)的快歌被识别为一半速度(170 BPM 的曲子识别为 85 BPM) 慢拍落在底鼓上,军鼓在拍间也出现 半拍型加倍。拍间几乎没有低音敲击的无鼓钢琴曲除外 170.1 BPM,合成钢琴曲不变
把 110~130 BPM 的曲子减半 只看奇偶强度差,就判定把八分音符当成了拍 检查拍间低音(应位于十六分音符位置)和反拍军鼓 纠正了 GTZAN 流行音乐 8 首,「세월이 가면」保持 68 BPM
把 4/4 拍的曲子记成 7/4、5/4、3/4 在 30 秒左右的曲子里,只挑出少数几个罕见的强烈强拍痕迹,导致对比度虚高 检查强拍证据的自相关,鼓的反拍明显时只取偶数拍号 GTZAN 中记为奇数拍号的曲目 221 → 104
切分 riff 中小节错位一拍,并夹入假的 5 拍、3 拍小节 强拍证据跟随了提前一个八分音符变化的和弦 鼓的奇偶先验(底鼓在第 1、3 拍,军鼓在第 2、4 拍) 不规则小节 9 → 3(「고민중독」(Gomin jungdok,《烦恼中毒》)),GTZAN 流行音乐强拍 F 0.557 → 0.608
曲子结尾被记成变为 3/4 拍 和弦每次提前一拍变化,看起来像 3 拍间隔 拍号变化概率 $10^{-6}$ → $10^{-7}$ 保持 4/4,合成曲目中 4 首拍号变化不变
大量出现 Am7/C、Em7/G 这类三音在低音的 m7 把由相同音构成的六和弦(C6、G6)读成了 m7 的转位 若低音正是最强的那个音,则改为低音上的三和弦 GuitarSet 中改变的 13 处里有 12 处与演奏正确答案一致
大调曲子中途被记成转到关系小调(G 大调 → E 小调) 只有频繁落到 vi(Em)的进行和 B7 → Em 进行较多的段落被单独判为小调 合并被分为关系大调/小调的相邻段落后重新判定,只有小调明显占优时才判为小调 「고민중독」G 大调 → A♭ 大调(与正确答案相同),其他测试曲目的调性全部不变
三音在响却记成 sus4 旋律或有音高的鼓(军鼓的分音)中混入的四音 三音达到四音的 0.7 倍以上即判为三和弦 「고민중독」sus 错误 10% → 2%
雷鬼这类 skank 很强的曲子,两个层级的判定会分歧 两倍速度的解读在音乐上也说得通 不自动强制,而是提供“÷2”按钮 有意不解决
节拍标记提前 6~8 ms 46 ms 的窗口提前包含了起音(attack),通量峰值偏前 6.5 ms 校正常数 平均误差 0 ± 2 ms
61% 的七和弦被识别为三和弦 七和弦的先验惩罚过大 用网格搜索重新调整 $\beta$、$\gamma$ 和惩罚 七和弦漏检 0,三和弦误判为七和弦 0
有转调的曲子也只显示一个调性,并把转调后的和弦按旧调的写法标记(E 大调段落中的 A♭m7 等) 只对全曲估计一次调性 用以调号为单位的调性 HMM 划分段落,各段分别应用 ♯/♭ 写法和调性加分 「세월이 가면」从第 45 小节第 1 拍起为 E 大调,之后标为 G♯m7、C♯7
和弦清晰的 A 小调进行(Am … E7)被判为 E 小调 仅凭音分布与调性轮廓的相关来定调,对调外音(F)惩罚不足 用识别出的和弦重新确定段落调性(音阶契合度、主和弦时长、V → I、首尾和弦),并与音分布相关混合 判为 A 小调,罗马数字级数 i v6 VI III6 …
短段落偏向关系小调(E 大调 → C♯ 小调) 段落以 vi 结束且有 V/vi → vi 时,和弦线索会偏向小调 增加“4 小节乐句第一拍的主和弦比例”作为线索,并用 18 首曲目的测试确定音分布的权重 调性明确的 18 首全部正确
曲子中途速度大幅变化时,漏掉后半段的拍,连拍号也出错 只在全曲速度 ±15% 范围内寻找局部速度 用速度状态维特比找出跳变,没有跳变的曲子沿用以前的曲线 节拍 64→93/96,反拍 16→1
把比波普识别为每小节一拍(65 BPM) 小节级重复很强,速度估计选中了慢层级,而加倍规则只看反拍军鼓 速度候选扩展到 300 BPM,并依据拍间低音(行走贝斯)逐级上调一个层级 260 BPM,节拍 96/96
低音是 C,却把 Cm7 记成 E♭ 或 E♭maj7 在音组成相同(Cm7 = E♭6)时,区分根音的线索只有低音,而其权重偏弱 尝试提高低音权重,但经过音导致别处出现 C♯maj7/C 之类的错误,于是撤回 未解决,记入局限
音乐开始前出现假的弱起小节 DP 路径延伸到了静音处,边缘整理中的邻域平滑又把它保留了下来 删除发声区间 ±¼ 拍以外的节拍后再外推 已消除
噪声底被判为声音 发声区间的阈值过松 −40 dB → −38 dB 已消除
有音高的底鼓可能被误认作贝斯音 底鼓的低频轰鸣每拍都累积到低音色度(chroma)中 减去全曲中位数的一半 去除了恒定成分
静音文件中出现 4 个假节拍 没有发声区间时也保留了原路径 没有发声区间则不输出节拍 空时间轴和提示文字
½ 拍移动后,曲子开始前出现点击音 中点计算在最前面附加了一拍 丢弃发声区间之前的拍 已消除
曲尾混响中出现只有 1 拍的小节 外推只检查了末端边界 只有在新拍之后仍有半拍以上的声音时才延长 已消除
分析过程中切换标签页可能会大幅变慢 在后台标签页中,setTimeout 可能被延迟到以 1 秒为单位 用不受计时器节流影响的 MessageChannel 让出控制权 不受计时器延迟影响
按住 → 键时不会跳到下一小节 播放调度(60 ms)期间,位置被读成在跳转点之前 调度前把位置固定为跳转点 连续 5 次 → 恰好 5 个小节
播放、暂停图标不切换 SVG 元素没有 .hidden 属性 直接切换 hidden 特性 正常
点击音时机滑块被压成一个点 复选框用的 CSS 也作用到了范围输入上 把选择器限定为复选框 正常
手机上固定播放栏变成四行 节拍器按钮和音量换行后全部被固定 节拍器改为图标按钮,音量移到固定区域之外 两行(播放行和波形),高度 125 px
分析与播放的时间轴可能错位 浏览器重采样器与单独解码带来的延迟 直接以零延迟对播放缓冲区重采样 偏移 0(脉冲测试 1.00000 s)
钢琴比录音低 18 cent 钢琴按 A4 = 440 Hz 合成 按分析估计的基准音合成,基准音变化时重建缓存 11 个音全部为 +17.7~+18.3 cent(正确答案 +18)
音乐、点击音、和弦重叠时有数字削波的风险 三种声音之和可能超过 0 dBFS 输出前加限幅器(−1 dBFS,20:1,起音 1 ms) 三者延迟相同,因此保持对齐,点击音测试结果不变
和弦演奏的声音太小 钢琴击键的最初瞬间比主体大 15~20 dB,按峰值设定的音量听起来偏小(默认音量下前 1 秒 RMS −28 dBFS) 在钢琴通路上加固定补偿增益,并对各泛音的起始相位随机化以降低初始峰值 最终值由下一行确定
补偿增益 +11.4 dB 过大 合适点位于过小值(−28 dBFS)与过大值(−18 dBFS)之间 在两者的 dB 中点二分,补偿增益定为 +6.4 dB 比以前约大 1.8~2.1 倍(+5.1~+6.5 dB)。即使在最大音量下钢琴峰值也在 −3 dBFS 左右,限幅器不会压低音乐
和弦演奏的余音太短 瞬时衰减成分比例高,击键后迅速变小 瞬时成分 72 → 60%,瞬时衰减时间比 0.12 → 0.15,余音衰减时间 ×1.13,音长 +0.8 秒 1–2 秒尾音 +2.7~+3.0 dB,10 dB 衰减时间 1.3~1.8 倍
用压缩器提高音量时,余音变长约 3 倍 释放缓慢的压缩器连尾音也抬高了,而快速限幅器抓不住最初几 ms 的峰值 去掉压缩器,改用合成参数和固定增益来调整 只延长了目标的量(+3 dB)
每根弦随机给相位时,余音过度变长 从同一相位出发的各弦逐渐错开正是双重衰减的成因,随机相位破坏了这一点 每个泛音相位不同,同一泛音的各弦相位相同 仅改相位带来的尾音变化 +5.9 → +0.6 dB

4-5. 优化

项目 方法 效果
阶段分离 繁重的信号处理只做一次,拍号、和弦推断按每次设置重新进行 校正生效约 40 ms(5 分钟的曲子)
实数 FFT 一次 $N/2$ 点复数 FFT + 后处理,旋转因子和位反转表预先计算 FFT 计算量约减半
不保存频谱图 节奏通量以 3 帧环形缓冲区流式计算,和声部分每帧只保存前几个峰值 以 4 分钟曲子计,原本需要三套完整频谱图(节奏、高音、低音各约 85 MB,合计约 254 MB),现在只需约 4 MB 的峰值
多相重采样器 把卷积核预先计算为相位表,整数累加器,无边界检查的快速路径 每个输出采样约 48 次乘法(48 kHz → 22.05 kHz)
累积和 响度均衡和峰值对比判定中的区间平均用累积和计算 与窗口长度无关的线性时间
O(T·K) 维特比 利用“保持或从最大值切换”的结构,回溯用 Int16Array 在扩展词汇(K = 289)下比 O(T·K²) 少 289 倍运算
协作式多任务 每 32 ms 通过 MessageChannel 让出控制权 不用 Worker 也能保持单文件,界面不卡顿
最小化画面更新 只在小节、拍、和弦变化时修改 DOM。波形是把画好一次的两张画布按 CSS 宽度裁剪来显示进度 不必每帧重绘画布
点击音 预先生成为缓冲区并复用,用调度列表一次性取消 无需为每次点击构建振荡器图
类型化数组 Float32Array、Float64Array、Int16Array、Int32Array、Uint8Array 内存连续、利于 JIT 优化
钢琴合成 用旋转递推生成正弦(不调用 Math.sin),低于 −100 dB 的部分省略计算,每个音只合成一次并缓存,按曲中首次出现的顺序预先合成 播放中超过 50 ms 的主线程任务 0 次
钢琴音内存 只合成曲中用到的音(测试曲目中为 15 个),2.6~5.6 秒单声道 约 13 MB

不使用 Web Worker 也有原因。Worker 需要单独的脚本文件或 blob: URL,在单文件分发和严格内容安全策略的环境中会受到限制。仅靠以 32 ms 为单位让出控制权,就能在分析 5 分钟曲子的过程中保持进度显示流畅。

4-6. 验证

单元测试

项目 方法 结果
实数 FFT 与直接计算 256 点随机信号的 DFT 比较 最大误差 3.8×10⁻¹³
重采样器精度 48 kHz 的 1 kHz 正弦 → 22,050 Hz,与理想正弦比较 RMS 误差 7.7×10⁻⁶
重采样器延迟 转换位于 1.0 秒处的脉冲后确认峰值位置 1.00000 秒(延迟 0)
采样率 22.05、44.1、96 kHz 输入 节拍命中 100%,平均误差 0.3 ms 以内
特殊输入 静音 5 秒、1.5 秒、0.2 秒片段 无错误,显示空时间轴

合成曲目回归测试

用 18 首已知正确答案(节拍、强拍、和弦)的合成曲目进行测试。曲目中加入了底鼓、军鼓、踩镲、带泛音的贝斯和键盘、混有 20% 非和弦音的旋律以及噪声底,也有把基准音偏移 18 cent 或让速度 ±2% 摇摆的曲目。节拍命中以 70 ms 以内为准,误差为检测时刻 − 正确时刻。和弦在拍的中点处,根音、性质、低音全部正确才算正确。

曲目 BPM(正确答案) 节拍命中 反拍 平均误差 / 平均绝对误差 拍号 强拍 和弦
流行,强反拍踩镲,+18 cent 97.1 (97) 96/96 0 0.1 / 1.5 ms 4/4 24/24 100%
流行 128.1 (128) 96/96 0 0.5 / 1.7 ms 4/4 24/24 100%
华尔兹 150.2 (150) 48/48 0 0.5 / 1.8 ms 3/4 16/16 100%
三连音抒情曲 58.0 (58) 48/48 0 −1.4 / 2.0 ms 12/8 12/12 100%
八分音符琶音抒情曲,速度摇摆 67.1 (68) 64/64 0 −1.0 / 1.9 ms 4/4 16/16 98.4%
弱军鼓摇滚 118.0 (118) 96/96 0 0.0 / 1.6 ms 4/4 24/24 100%
舞曲(4-on-the-floor,反拍开镲) 123.9 (124) 96/96 0 −0.4 / 1.5 ms 4/4 24/24 100%
嘻哈(十六分踩镲,切分底鼓) 87.9 (88) 64/64 0 0.7 / 2.0 ms 4/4 16/16 100%
朋克 176.2 (176) 96/96 0 0.4 / 1.7 ms 4/4 24/24 100%
钢琴独奏 72.0 (72) 64/64 0 −3.2 / 3.2 ms 4/4 16/16 98.4%
钢琴独奏,三拍子 84.0 (84) 48/48 0 −2.6 / 2.7 ms 3/4 16/16 100%
抒情曲,后半段放慢 15%(渐慢) 71.6 (72→63) 80/80 0 −0.8 / 2.0 ms 4/4 20/20 88.8%
转调 C → D(经 A7) 110.2 (110) 116/116 0 0.0 / 1.5 ms 4/4 29/29 100%
转调 E♭ → E(经 B7,抒情曲) 70.0 (70) 116/116 0 −1.1 / 2.0 ms 4/4 29/29 93.1%
转调 G → A♭(直接) 100.0 (100) 112/112 0 0.0 / 1.4 ms 4/4 28/28 100%
A 小调主歌 → C 大调副歌(非转调) 96.0 (96) 64/64 0 −0.2 / 1.5 ms 4/4 16/16 100%
只有鼓的前奏 + 弱起小节 104.0 (104) 57/57 0 −0.7 / 2.3 ms 4/4 14/14 100%,N.C. 9/9
雷鬼 one drop 151.7 (76) 48/48 47 −3.8 / 3.8 ms 4/4 两倍层级 100%

雷鬼被识别在两倍层级,多出的节拍全部落在八分音符位置。这就是前面所说的有意不解决,按一次“÷2”按钮即变为 76 BPM。和弦错误包括:Esus4 → E 在小节中间变化处,E 晚了一拍才被识别;以及两首抒情琶音曲(渐慢、E♭ → E)中,琶音里的七音较弱,maj7、m7 被读成了三和弦(开关局部速度或转调跟踪结果都相同,因此与拍和调性跟踪无关)。

转调测试

没有转调的 14 首合成曲目全部只得出一个调性(假转调 0)。转调位置的正确答案是新调主和弦开始的小节。

曲目 正确答案 结果
C 大调 → D 大调,经 A7 第 18 小节起为 D 大调 第 18 小节第 1 拍,误差 0 拍
E♭ 大调 → E 大调,经 B7(抒情曲) 第 18 小节起为 E 大调 第 18 小节第 1 拍,误差 0 拍
G 大调 → A♭ 大调,不经过渡直接转 第 17 小节起为 A♭ 大调 第 17 小节第 1 拍,误差 0 拍
A 小调主歌 → C 大调副歌 无转调(调号相同) 无转调
以弱起小节开始的曲子 起始调性位置 第 0 小节第 4 拍(弱起小节中的实际拍)

转调后的写法也已确认。D 大调段落写作 F♯m,E 大调段落写作 G♯m7、C♯m,A♭ 大调段落写作 D♭、B♭m7。

Camelot 标记测试

项目 结果
将 24 个调性的编号与标准 Camelot 轮表对照 24/24 一致
按和弦种类转换 C → 8B, Am → 8A, G7 → 9B7, Dm7 → 7A7, Cmaj7 → 8Bmaj7, Esus4 → 12Bsus4, Bdim → 10Adim, Bm7♭5 → 10A7♭5, Bdim7 → 10Adim7, G/B → 9B
真实录音(「세월이 가면」(Sewori gamyeon,《岁月流逝》))中的标记切换 调性 E♭ 大调(5B)→ E 大调(12B),转调标签为“12B로”(韩语助词 로 表示“转到”)。和弦谱、当前/下一个和弦、文本和弦谱、屏幕阅读器用的名称全部变为 Camelot,选择音名则恢复原状
分析结果 切换标记后,拍、强拍、和弦判定均不变(18 首合成曲目结果相同)

罗马数字级数测试

分析和弦清晰的合成音频,确认识别出的和弦的罗马数字级数与和声学教材的写法一致。

调性 和弦 结果
A 小调 Am, Em/G, F, C/E, Dm, G, C, E7 i, v6, VI, III6, iv, V/III, III, V7
C 大调(副属和弦、借用和弦) C, E7, Am, D7, G7, C, A7, Dm, G7, C, Fm, C, B♭, C I, V7/vi, vi, V7/V, V7, I, V7/ii, ii, V7, I, iv, I, ♭VII, I
E♭ 大调(副 ii、转位) E♭, B♭/D, Cm, B♭m7, E♭7, A♭maj7, B♭7/A♭, Gm7, Cm7, Fm7, B♭7, E♭ I, V6, vi, ii7/IV, V7/IV, IVM7, V⁴₂, iii7, vi7, ii7, V7, I

真实录音(「세월이 가면」)的 105 个和弦中,有 10 个被标为副属类和弦(ii7/IV、V7/IV、V7/ii 等),16 个被标为转位,转调前的 B7 被标为新调 E 大调的 V7。对 18 首调性明确的合成曲目,现在混合和弦与音分布的方式判定调性,18 首全部正确(以前只用音分布的方式把 A 小调进行判成了 E 小调,为 17 首)。

真实录音测试

用一首商业录音(기현(Kihyun)「세월이 가면」,응답하라 1988(《请回答1988》)OST,3 分 50 秒)进行了测试。由于没有标注正确拍位,利用其为速度恒定的录音室录音这一点,把各段节拍拟合直线后的残差作为跟踪误差的替代指标。

项目 以前 现在
拍的层级 135 BPM(把八分音符当作拍) 68.1 BPM
36~40 秒段落的和弦谱 A♭maj7 / Gm7 B♭/F │ Fsus4 / B♭ / (每半拍就变) A♭maj7 / / B♭/F │ Gm7 / E♭ /
长度不同的小节 3 个(3 拍、2 拍、3 拍)+ 结尾 1 个 + 结尾
分段直线残差 RMS 96~222 ms(固定惩罚 100,在正确速度下) 6~23 ms
33~48 秒段落的点击间隔(浏览器) 0.80~1.02 s 0.86~0.90 s
转调 不显示,转调后仍按 E♭ 写法标记(A♭m7 等) 从第 45 小节第 1 拍(2:37)起为 E 大调(B♭7 → B7 → E 中 E 的位置),之后标为 G♯m7、C♯7

真实浏览器端到端测试

在无头 Chromium 中实际操作页面。拦截 AudioBufferSourceNode.prototype.start,记录所有被调度的点击音,再用最近一次音乐开始的信息还原出曲中时刻( $o_s + t^\text{click} - t_s$ ),与正确节拍对照。测试中混合执行了播放、跳小节和停止。

文件 点击 正拍 反拍 平均 / 最大误差 强拍重音位置
流行 97 BPM 37 37 0 0.1 / 8.1 ms 22/22 位于实际强拍
弱起小节 104 BPM 29 29 0 1.9 / 9.7 ms 16/16
抒情曲 68 BPM(自动减半校正) 22 22 0 1.9 / 8.5 ms 13/13
校正操作 结果
移动 ½ 拍(“½박 옮기기”) 点击 4/4 移到正确拍的中点,再按一次则 5/5 回到正拍
“×2”,接着“÷2” 间隔 310 ms(拍 + 中点)→ 622 ms 回到正拍
推迟一拍(“한 박 늦추기”) 强拍重音移到第 2 拍
点击音时机 +30 ms 点击音被推迟 29.2 ms 调度
将拍号指定为 3 3/4,每小节 3 条斜线
关闭节拍器(M) 被调度的点击音为 0

此外还确认了控制台错误 0 条,桌面和手机宽度下横向溢出 0 px。63 秒的曲子从选择文件到显示和弦谱约需 1.6 秒(无头 Chromium),5 分钟曲子的分析约需 2.7 秒(Node 22)(在 Linux 环境下测量)。

和弦演奏测试

被调度的钢琴音也用同样方法拦截。对每个音的缓冲区估计音高(先用自相关粗略找出,再用 Goertzel 扫描精确测量基本分音频率),把同一时刻开始的音归为一次击键,与正确和弦对照。应用代码中没有加入任何测试专用标记。

项目 结果
和弦变化时 18 次击键全部构成音一致,低音 18/18 一致
击键时刻 与实际和弦变化时刻的误差平均 1.4 ms,最大 8.1 ms
从和弦中途开始播放 10 次重新击键全部为该时刻的正确和弦
每小节、每拍 3/3、5/5 正确,拍时刻误差平均 3.0 ms、0.6 ms
钢琴基准音 相对曲子调音 +18 cent,11 个音的基本分音为 +17.7~+18.3 cent
用 P 键关闭 此后钢琴音调度为 0,节拍器继续
对节拍器的影响 加入限幅器后,点击 37/37、29/29、22/22 仍在正拍,强拍重音全部准确
主线程 播放中超过 50 ms 的任务 0 次
音量与余音 用 OfflineAudioContext 渲染实际应用的合成函数和声音通路,在与旧版本相同的滑块位置下比较。前 1 秒 RMS +5.1~+6.5 dB(约 1.8~2.1 倍),1–2 秒尾音 +2.7~+3.0 dB,10 dB 衰减时间 1.3~1.8 倍(C、G7、Am)。峰值在默认音量下为 −7 dBFS,最大音量下为 −2.3~−3.0 dBFS

曲目类型扩展测试(1.5.0)

在回归 18 首之外,新增 17 首此前没有的曲目类型,与 1.4.2 比较。其中流行音乐型 10 首(速度突变、自由速度(rubato)钢琴、和弦外低音、磁带音高变化、K-pop·EDM、流行摇滚强力和弦、R&B 斜线和弦抒情曲、鼓声很大的嘻哈、鼓声极大的摇滚、原声扫弦流行),特殊风格 7 首(5/4、7/4、摇摆爵士、比波普、波萨诺瓦、金属、氛围音乐)。

验证的标准是流行音乐优先。会让流行音乐曲目(回归 18 首、流行音乐型曲目、真实录音)结果变差的修改,即使特殊风格变好也不采用;能让流行音乐曲目结果变好的修改,即使特殊风格略微变差也采用。

分组 项目 1.4.2 1.5.0
回归 18 首 节拍、强拍、和弦、调性 100%、100%、98.6%、18 首 相同
流行音乐型 10 首 节拍 95.7% 99.6%
  反拍点击 16 1
  强拍 95.7% 99.5%
  和弦(精确) 88.1% 97.0%
特殊风格 7 首 节拍 80.0% 93.8%
  强拍 74.4% 93.2%
  和弦(精确) 52.7% 56.0%
真实录音 与 1.4.2 一致   和弦 100%,强拍 66/66,节拍 257/257

按曲目看,速度突变曲目的节拍为 64→93/96(反拍 16→1),5/4 和 7/4 的拍号被准确判定(强拍 16/16、12/12),比波普的节拍为 24→96/96,和弦外低音曲目为 50→100%,R&B 斜线和弦抒情曲为 82→91%,流行摇滚强力和弦曲目为 50→88%。

比较过但未采用的方法

方法 流行音乐 特殊风格 判定
七和弦的 9·13 扩展音变体 真实录音中 18% 的和弦发生变化(A♭→A♭maj7 等) 摇摆爵士类一致率 28→32% 不采用
和声·打击乐分离(HPSS) 无变化,真实录音和弦变化 3%,分析时间 2 倍 和弦精确 56→61% 不采用
NNLS 音分解色度 回归和弦 98.6→97.4%,流行音乐型 97.0→95.7% 和弦精确 56→63% 不采用
Bar pointer 模型 回归反拍点击 47→304,强拍 100→56.7% 强拍 93→44% 不采用

Bar pointer 模型本来是与训练好的神经网络生成的拍、强拍激活一起使用的方法,用手工构造的起音·重音曲线无法区分拍的层级和拍号。这四种方法都作为关闭的设置保留在引擎中,可以重新进行同样的比较。

拍号变化与速度显示测试(1.6.0)

曲目 正确答案 结果
流行,4/4 → 3/4 → 4/4(各 8 小节) 第 9 小节起为 3/4,第 17 小节起为 4/4 相同,强拍 24/24
华尔兹主歌 → 4/4 副歌 第 9 小节起为 4/4 相同,强拍 20/20
速度突变 96 → 132 第 13 小节第 1 拍起为 132 相同
后半段放慢 15% 的抒情曲 后半段逐渐 72 → 63 从第 11 小节第 2 拍到第 20 小节第 4 拍逐渐 72 → 63(六边形框),第 20 小节第 4 拍显示白色药丸标签 63
拍号恒定的 35 首和真实录音 无拍号变化 假拍号变化 0
其余 36 首和真实录音 无渐变区间 六边形框 0

开启拍号变化模型后,与 1.5.0 比较的 37 首曲目的节拍、强拍、和弦、调性全部相同,真实录音与 1.4.2 也同样为和弦 100%、强拍 66/66、节拍 257/257。在速度随机摇摆至 ±12% 的自由速度钢琴曲中,只在一处突然加快超过 12% 的地方出现了标记。

真实录音基准测试(1.10.0)

1.10.0 修改了拍的层级、强拍和和弦名称规则,因此用带人工标注正确答案的真实录音与 1.9.4 进行了比较。GTZAN 是 10 种风格、998 首 30 秒节选,带有拍和强拍的正确答案;GuitarSet 是 180 首独奏吉他伴奏,带有和弦、拍和强拍的正确答案。节拍和强拍以 ±70 ms 以内的一一配对计算 F 值;和弦在正确拍的中点处统计根音和大小调类别(maj·7·maj7 为大,m·m7 为小)是否正确(GuitarSet 以 lead sheet 为正确答案)。流行音乐组为 GTZAN 的布鲁斯、乡村、迪斯科、金属、流行、摇滚,以及 GuitarSet 的摇滚、创作歌手、放克。

数据 项目 1.9.4 1.10.0
GTZAN 流行音乐 600 首 节拍 F / 强拍 F 0.829 / 0.557 0.837 / 0.608
  以正确速度且在正拍上识别的曲目 / 识别为一半速度的曲目 452 / 54 465 / 39
GTZAN 全部 998 首 节拍 F / 强拍 F 0.786 / 0.511 0.794 / 0.556
  记为 3·5·7 拍的曲目 221 104
GuitarSet 流行音乐 108 首 节拍 F / 强拍 F 0.656 / 0.550 0.663 / 0.568
  和弦根音 / 大小调 81.7% / 68.5% 81.9% / 69.0%
GuitarSet 全部 180 首 和弦根音 / 大小调 77.0% / 66.8% 77.8% / 67.9%

GTZAN 各风格的强拍 F 值为:布鲁斯 0.328 → 0.361,乡村 0.778 → 0.794,迪斯科 0.676 → 0.708,金属 0.363 → 0.476,流行 0.676 → 0.717,摇滚 0.519 → 0.593,嘻哈 0.482 → 0.541,雷鬼 0.634 → 0.679,爵士 0.349 → 0.360,古典 0.293 → 0.315,全部上升。按曲目看,流行音乐 600 首中强拍 F 上升超过 0.05 的有 61 首,下降的有 34 首。下降的曲目中,有 22 首是原本把 4/4 曲子错记成 7/4、5/4、3/4,纠正为 4/4 后失去了几个碰巧对上的强拍;拍号不变而强拍位置变差的有 8 首。在 GuitarSet 中,两首放克(114 BPM)从一半速度被纠正为正确速度,拍变密后和弦根音下降了 6~9 个百分点。

还用三首流行歌曲录音做了确认。「고민중독」(Gomin jungdok,《烦恼中毒》)以公开的和弦谱按音源的鼓和贝斯逐小节对齐后作为正确答案来测量(只取有把握的 62% 区间);「크리스마스의 기억」(Keuriseumaseu-ui gieok,《圣诞节的记忆》)只看已知的速度(170 BPM)和拍号。

曲目 项目 1.9.4 1.10.0
QWER 「고민중독」(正确答案 178 BPM,4/4 129 小节) 速度 89.0(落在军鼓上的一半速度) 177.9
  节拍 F / 强拍 F / 不规则小节 0.64 / 0 / 9 0.96 / 0.95 / 3
  和弦根音 / 大小调(每拍) 61.6% / 50.5% 85.5% / 83.5%
  小节级和弦 66.7% 91.3%
  带 7、sus 等修饰的名称所占比例 91% 67%
  调性 E 小调 → F 小调 G 大调 → A♭ 大调(从第 104 小节起,与正确答案相同)
Warak 「크리스마스의 기억」(170 BPM) 速度 / 不规则小节 85.0 / 3 170.1 / 2(曲子的第一小节和最后一小节)
기현 「세월이 가면」(Sewori gamyeon,《岁月流逝》) 与 1.4.2 一致(和弦、强拍、节拍) 97.7%,66/66,257/257 相同

37 首合成曲目(回归 18 首、流行音乐型 10 首、特殊风格 9 首)以及 37 首未见过的合成曲目,其节拍、强拍、和弦、调性与 1.9.4 逐曲相同。

5 首真实录音验证(1.10.3)

用风格和语言各不相同的五首流行歌曲,确认了速度、拍号、小节位置、调性和和弦。小节位置与独立的分析器(madmom:经过训练的拍·强拍跟踪和大小调和弦识别)比较;和弦则以如下方式得到的正确答案每 0.1 秒测量一次(根音、大小调类别):把公开的和弦谱按拍对齐到音源,和弦谱与音源不同之处(翻唱编曲等)则测量低音和和声频段来确定。由于版权原因,正确答案和 madmom 结果不予分发,只列出得分。「여래아」(Yeoraea)没有公开的和弦谱,因此只看了速度、拍号、小节和调性。

曲目 速度(公开值) 拍号 强拍一致(madmom) 调性 和弦根音 / 大小调 1.10.2 → 1.10.3 同一评分下的 madmom
여자친구 「시간을 달려서」(Sigan-eul dallyeoseo,《穿越时间》) 112.0 (112) 4/4 0.96 → 0.99 F 小调 89.0% / 86.2% → 89.5% / 86.8% 83.7% / 84.0%
「もう恋なんてしない」女声翻唱(比原曲高 7 个半音,快约 5%) 109.9 4/4 0.99 B 大调 90.1% / 92.9% → 90.4% / 92.9% 84.1% / 88.1%
あいみょん 「マリーゴールド」 106.0 (106) 4/4 0.96 D 大调 89.4% / 88.9% → 90.4% / 89.9% 92.9% / 92.9%
YOASOBI 「夜に駆ける」 130.0 (130) 4/4 1.00 C 小调 → B 小调 → D 小调 88.6% / 80.6% → 88.6% / 80.5% 83.4% / 82.7%
시드 사운드 「여래아」 90.0 4/4 1.00 C 小调 → D 小调(1.10.2 为 C 小调 → F 大调) 无和弦谱 -

五首曲子的速度层级和拍号全部正确,小节位置与 madmom 和和弦谱一致。「시간을 달려서」的前两小节在 1.10.3 中、「マリーゴールド」前奏的小节线在 1.10.4 中得到纠正。「マリーゴールド」的前奏 madmom 也识别得不同(1.78、4.78 秒),因此强拍一致仍为 0.96,小节线则通过前奏 riff 反复的周期和起点来确认。「夜に駆ける」中降半音、升半音的转调确实存在;降半音的调在 1.10.3 之前会提前两小节(第 102 小节)显示,从 1.10.4 起显示在 B 小调副歌开始的第 104 小节。剩余的和弦错误大多是:人声旋律或持续音混入和声频段,被读成共同音较多的相邻和弦(A → Bm7/A、G7 → Gm7、C → Cm7 等);漏掉半小节、一拍长的短和弦;以及把 sus4 解决到三和弦的过程合并为一个和弦。降低高音区权重的方法在这五首中使根音提高了 0.6~1.1 个百分点,但让合成 R&B 抒情曲、GuitarSet 和「고민중독」(Gomin jungdok,《烦恼中毒》)变差,因此未采用。

1.10.4(前奏网格、转调边界修整)没有改变五首曲子的和弦、其他小节和其他调性边界;37 首合成曲目、回归 18 首、调性·罗马数字级数测试、GuitarSet、「고민중독」与「세월이 가면」(Sewori gamyeon,《岁月流逝》)的和弦·强拍·节拍、Warak、GTZAN 的得分也都不变。调性边界在「고민중독」中向正确答案方向移动了一个小节;在没有调性正确答案的 GTZAN 30 秒节选 1000 首中有 9 处(古典 3、金属 4、爵士和摇滚各 1),以及 GuitarSet 中 1 首,转调标记向后移动了 1~2 小节(由于是短节选,大多看起来是本不存在的转调)。

1.10.3 的其他测试结果:37 首合成曲目逐曲相同;「고민중독」根音 85.5% → 85.9%,大小调 83.5% → 83.8%;GuitarSet 流行音乐根音不变(81.9%)、强拍 F 0.568 → 0.574;GTZAN 流行 6 种风格的节拍、强拍 F 不变,不规则小节 711 → 695 个;「세월이 가면」与 1.4.2 的一致仍为 97.7%、66/66、257/257。

10 首真实录音验证套件(1.11.0)

在 1.10.3 的五首基础上,加入风格和速度不同的另外五首(김종국 「이 사람이다」(I saramida,《就是这个人》)、015B 「이젠 안녕」(Ijen annyeong,《如今再见》)、박명수 「바다의 왕자」(Bada-ui wangja,《大海的王子》)现场翻唱、디셈버 「별이 될게」(Byeori doelge,《我会成为星星》)与 피노키오 「사랑과 우정 사이」(Saranggwa ujeong sai,《爱情与友情之间》)的女声翻唱)以及「세월이 가면」(기현,응답하라 1988(《请回答1988》)OST),做成了一套带和弦正确答案的 10 首真实录音验证套件。每首曲子把两份以上的公开和弦谱对齐到音源的拍,和弦谱与音源不同之处测量低音和和声频段来确定,再另行带着怀疑重新确认(每首修正 1~9 处),与拍、强拍、调性的正确答案一起制作。由于版权原因,正确答案不予分发。「여래아」因没有公开和弦谱而未纳入,「고민중독」另行评分。

规则和参数只根据前面四首、「고민중독」、GuitarSet 和合成曲目来选定;新增的五首和「세월이 가면」在选参数时没有看过,只在最后测量。

曲目 和弦根音 / 大小调 1.10.5 → 1.11.0 同一评分下的 madmom
여자친구 「시간을 달려서」 89.3% / 86.5% → 91.8% / 90.0% 83.7% / 84.0%
「もう恋なんてしない」翻唱 90.4% / 92.6% → 92.8% / 94.1% 84.1% / 88.1%
あいみょん 「マリーゴールド」 90.4% / 89.8% → 97.0% / 94.9% 92.9% / 92.9%
YOASOBI 「夜に駆ける」 88.6% / 80.5% → 90.7% / 85.0% 83.4% / 82.7%
김종국 「이 사람이다」 87.6% / 83.5% → 90.0% / 85.6% 83.3% / 80.9%
015B 「이젠 안녕」 85.8% / 83.2% → 91.0% / 88.8% 83.2% / 82.7%
박명수 「바다의 왕자」现场翻唱 94.0% / 93.6% → 99.3% / 99.3% 90.3% / 90.3%
디셈버 「별이 될게」翻唱 90.4% / 89.8% → 90.9% / 88.5% 88.6% / 88.5%
피노키오 「사랑과 우정 사이」翻唱 97.7% / 96.2% → 98.0% / 95.8% 91.1% / 91.9%
기현 「세월이 가면」 88.4% / 84.8% → 94.7% / 91.2% 90.8% / 89.8%
前 4 首(用于选参数的曲目) 89.7% / 87.2% → 93.3% / 91.0% 86.5% / 87.3%
后 6 首(选参数时未看的曲目) 90.8% / 88.6% → 94.0% / 91.5% 88.0% / 87.5%
全部 10 首 90.4% / 88.0% → 93.7% / 91.3% 87.4% / 87.4%

评分方式是:每 0.1 秒取一个样本,看引擎和弦的根音和大小调类别是否与正确答案标签之一相同,没有和弦的地方(强拍之前、结尾)也计为错误。1.10.3 一节的表格使用的是旧评分,在查找与时刻对应的和弦的方式上不同,并且会把这类样本从大小调的分母中排除,因此数值相差 0.1~0.7 个百分点。

10 首的根音全部上升,根音错误的样本从 9.6% 降到 6.3%(读成共同音较多的相邻和弦:5.0% → 3.6%)。大小调在「별이 될게」和「사랑과 우정 사이」中略有下降。根音和性质(大、小、7、maj7、m7、sus 等)都正确的比例从 64.3% 升到 66.0%(正确答案和弦谱只写三和弦的前 4 首,这一比例本来就低)。10 首的拍、强拍、调性全部不变。「이젠 안녕」(正确答案 71 BPM)和「사랑과 우정 사이」(64 BPM)被识别为把八分音符当作拍的两倍速度,但若放宽拍层级验证的条件,GTZAN 中会有更多正确速度的曲子被降为一半,因此没有修改(4-8)。

将「세월이 가면」的测试基准改为正确答案(1.11.0)。 此前这首曲子以是否与 1.4.2 的结果相同(和弦一致 97.7%、强拍 66/66、节拍 257/257)来检查退化,但 1.4.2 的结果本身与正确答案相比根音只有约 88%。从 1.11.0 起改为与正确答案比较,低于记录值(根音 94.7%、大小调 91.2%、性质 78.1%、节拍 F 0.948、强拍 F 0.937)即视为退化,与 1.4.2 的一致只作参考(1.11.0 中和弦 85.9%,强拍、节拍仍为 66/66、257/257)。

1.11.0 的其他测试结果:37 首合成曲目根音 96.52% → 96.81%,大小调类别 95.60% → 95.78%(比波普、摇摆爵士、R&B 抒情曲三首变好,其余相同,节拍、强拍不变);「고민중독」根音 85.9% → 93.3%,大小调 83.8% → 88.2%;GuitarSet 流行音乐根音 81.87% → 82.47%,大小调 68.97% → 69.72%;GuitarSet 全部根音 77.87% → 78.35%;回归 18 首、调性·罗马数字级数测试以及 GTZAN、Warak 的拍、强拍、拍号不变。GTZAN 1000 首中有 23 首的调性改变,与公开的 GTZAN 调性正确答案(Lerch)相比,7 首改对、5 首改错(其余无正确答案或两者都错),流行 6 种风格的调性准确率从 55.3% 升到 56.0%(对关系调、五度关系给部分分的 MIREX 得分 64.4% → 65.0%)。立体声文件的分析时间每首约增加 40%(套件 10 首平均 2.9 秒 → 4.0 秒,同一台电脑)。

1.11.8 验证(小节第一拍上一拍长的和弦)

「해질녘 예스터데이」(Haejilnyeok yeseuteodei,《黄昏的 Yesterday》)第二小节在第一拍弹 A♯m7,从第二拍起弹 D♯m7。只看第一拍,A♯m7 比 D♯m7 领先 3.79,但这小于在第二拍换和弦的惩罚(4.0),因此在 1.11.7 中整个小节都显示为 D♯m7。

起初只设了一个得分条件:“第一拍上另一个和弦领先超过 3.5,而第二拍上当前和弦更好,就把第一拍改为那个和弦”。结果 1,268 首测试曲目中有 102 首发生变化,在带和弦正确答案的 GuitarSet 中,得分有变动的 6 首全部变差。对照正确答案,那些位置都是新和弦从第一拍就开始的地方,第一拍上出现的另一个和弦,其实是前一和弦的余响与新和弦混在一起的声音。仅凭得分差无法区分这首曲子和那些曲子(GuitarSet 00_Jazz3-150-C 相差 3.94,比这首曲子还大)。把第一拍分成两半、看后半拍是否也领先的条件,反而因为这首曲子的 A♯m7 在后半拍较弱(0.88)而没有采用。

两个和弦是否真的分别弹奏,是靠声音的起始(起音(attack))区分的。这首曲子中,只属于 A♯m7 的音在第一拍以 2.25 倍、只属于 D♯m7 的音在第二拍以 2.03 倍重新响起,并且两个和弦的低音不同。GuitarSet 中出错的 6 处,要么第一拍的起音较弱(低于 1.2 倍的 3 处),要么第二拍没有重新弹原来的和弦(2 处),要么两个和弦的低音相同(1 处)。起音阈值直接沿用了延续(灰色)判定中的 1.2 倍。

条件 发生变化的曲目(1,268 首中) GuitarSet 和弦评分
得分差 3.5 以上 102 6 首变差
+ 起音·低音条件,得分差 3.0 以上 51 2 首变差,1 首变好
+ 起音·低音条件,得分差 3.25 以上 34 无变化曲目
+ 起音·低音条件,得分差 3.5 以上(采用) 23 无变化曲目

3.25 在带正确答案的曲目中也没有出现变差之处,但它离会出现变差曲目的 3.0 较近,因此留出余量定为 3.5。最终条件下发生变化的曲目为 23 首(每首一两个小节)。「해질녘 예스터데이」第二小节恢复为 A♯m7 D♯m7,结尾部分第 93 小节第一拍增加了 D♭(A♭sus4 Fm7/C B♭m7 → D♭ A♭sus4 Fm7/C B♭m7)。真实录音套件中的「고민중독」在 2 分 21 秒左右某一小节的第一拍增加了 B7/F♯,但那是正确答案和弦谱中未写和弦的格子,因此得分不变(同一首曲子的 m4a 文件也相同)。其余 20 首是没有和弦正确答案的 GTZAN 曲目。回归 18 首、调性·罗马数字级数测试、37 首合成曲目评分、「세월이 가면」正确答案测试、GuitarSet 180 首和弦·拍评分都与 1.11.7 相同,发生变化的曲目其拍、强拍、拍号、调性也都不变。

1.11.7 验证(前奏的局部错位)

用户告知的「해질녘 예스터데이」(Haejilnyeok yeseuteodei,《黄昏的 Yesterday》)在鼓进入的 10.88 秒之前是钢琴前奏,但拍以 0.40~0.53 秒的间隔参差不齐地被识别,从第一小节起和弦就错位了。把正曲段落的拍周期(0.455 秒,132 BPM)往前延伸,正好落在第一个声音(3.14 秒)上,前奏钢琴的起音几乎全部落在该网格的十六分音符位置(58 个起音峰中有 51 个在 ±25 ms 以内),由此确认这首曲子从一开始就是以 132 BPM 固定速度制作的。此前,拍会跟随十六分音符的提前(5.31、9.40 秒等),最多提前了 0.12 秒。在 1.11.7 中,前奏的拍以 0.455 秒间隔排列,第三小节由 G♯m G♯m7 变为 G♯m7 C♯7(逐五度下行的进行)。不过第二小节由 A♯m7 D♯m7 变成了 D♯m7:在正确的网格上,A♯m7 只在第一拍(加一个十六分音符)上响,这个一拍长的短和弦被合并进了相邻和弦。这一点在 1.11.8 中修正(见下方 1.11.8 验证)。第一小节由 B G♯7/F♯ 变为 G♯m7/B;在前奏之外,汇总反复段落之间的和弦得分时,改变后的前奏拍被用作配对,第 44 小节第四拍的一个拍由 Em7 变为 Cmaj7(就该拍的音而言,Em7 更合理)。其余小节不变。

在 1,268 首测试曲目上逐个增加条件并统计发生变化的曲目。只靠均匀速度、局部偏离、十六分音符契合这几个条件时,有 4 首发生变化(GTZAN 2 首的拍、强拍得分不变);GuitarSet 05_Funk2-119-G 的拍、强拍得分相同,但多出一个长度不同的小节,和弦根音 0.254 → 0.222,大小调 0.316 → 0.263 下降,因此增加固定速度依据条件(起音峰 30 个以上、十六分音符契合 0.85 以上)将其排除。这首曲子从头到尾只有吉他独奏,前奏起音只有 19 个,契合度也只有 0.84。最终条件下发生变化的只有「해질녘 예스터데이」一首,其余 1,267 首以及回归 18 首、调性·罗马数字级数测试、37 首合成曲目评分、真实录音套件、「세월이 가면」(Sewori gamyeon,《岁月流逝》)正确答案测试、GTZAN 和 GuitarSet 评分均与 1.11.6 相同。测试曲目中除这首外没有同样形态前奏(无鼓开始的固定速度曲目中出现十六分音符提前)的曲子,因此这条规则能改善多少其他曲目,目前还未能测量。

1.11.6 验证(3:2 拍层级确认)

采用与 1.11.5 相同的方法,对看起来拍被三分的 125 首中、4:3 确认后不变的 119 首,逐首预先生成以 3/2 倍快速度重新识别的结果,然后逐个增加条件。

条件 发生变化的曲目 变好 变差(拍或强拍)
无条件全部改变 119 34 84
新拍不是三连音且在 60~240 BPM 107 32 75
+ 和声变化分组 $G \ge 0.05$ 48 26 22
+ 无先验分布的得分比 0.93 以上 12 9 3
+ 新节拍位置的重音比 1.05 以上 9 8 1(吉他伴奏 1 首,强拍)
+ 新拍明显分为两个八分音符(1.4 倍) 8 8 0(以拍、强拍为准)
+ 新速度 90 BPM 以上(采用) 7 7 0

在 8 首阶段运行 GuitarSet 和弦评分,发现 02_BN2-166-Ab(12/8,56 BPM → 4/4,83 BPM,为正确答案 166 的一半)虽然拍、强拍变好,但和弦大小调准确率从 0.44 降到 0.22,于是把新速度下限从 60 BPM 提高到 90 BPM 将其排除。发生变化的是以下 7 首。

曲目 变化内容 与正确答案比较
GTZAN metal.00017 12/8,144 BPM → 4/4,216 BPM(正确答案 217) 拍 F 0.38 → 0.76,强拍 F 0.04 → 0.70
GTZAN metal.00034、metal.00094(同一音源出现两次的曲目) 12/8,107 BPM → 4/4,160 BPM(正确答案 159) 拍 F 0.38 → 0.88、0.37 → 1.00 / 强拍 F 0.12 → 0.35、0 → 0
GTZAN rock.00064 12/8,102 BPM → 5/4,152 BPM(正确答案 152,正确答案也是 5 拍小节) 拍 F 0.39 → 1.00,强拍 F 0.21 → 1.00
GuitarSet 01_Rock1-130-A 12/8,87 BPM → 4/4,130 BPM 拍 F 0.40 → 0.99,强拍 F 0.29 → 0.96,和弦根音 0.83 → 0.96,大小调 0.81 → 0.96
GuitarSet 02_Rock3-148-C 6/8,98 BPM → 4/4,150 BPM 拍 F 0.40 → 0.60,强拍 F 0.28 → 0.63,和弦根音 0.87 → 0.97,大小调 0.76 → 0.87
GTZAN jazz.00086 12/8,137 BPM → 5/4,206 BPM(正确答案 104 的两倍) 拍 F 0.28 → 0.59,强拍 F 0.12 → 0.17

前 6 首变成了正确速度;jazz.00086(实际上是存在 4:3 错误的曲目)变成正确答案的两倍,可以用校正中的“÷2”按钮修正(以前是 12/8,无法用按钮修正)。存在同样错误(记为 12/8、6/8,速度为正确答案的 2/3 倍)的测试曲目有 GTZAN 10 首、GuitarSet 18 首,共 28 首,其中 6 首变成了正确速度。其余曲目至少有一项条件未通过(主要是得分比 0.93)。降低这一阈值后,速度虽对但节拍落在反拍上的吉他伴奏曲(00_BN1-129 等)也会一起改变而变差,因此没有放宽。把全部 1,268 首与 1.11.5 分别分析比较时(下限 60 BPM),除上述 7 首和 02_BN2-166 外全部相同;把下限提高到 90 后,重新运行看起来拍被三分的 125 首,确认只有 02_BN2-166 回到了 1.11.5 的结果。GuitarSet 180 首和弦评分中只有这 2 首变好,其余相同;回归 18 首、调性·罗马数字级数测试、37 首合成曲目评分、真实录音套件、「세월이 가면」正确答案测试、GTZAN 调性均与 1.11.5 相同。

1.11.5 验证(4:3 拍层级确认)

在与 1.11.4 相同的 1,268 首测试曲目中,对看起来拍被三分(记为 12/8、6/8)的 125 首,逐首预先生成以 4/3 倍慢速度重新识别的结果,再逐个增加条件并统计发生变化的曲目。对有正确答案的曲目,只要拍 F 或强拍 F 中任一下降,即计为变差。

条件 发生变化的曲目 变好 变差(拍或强拍)
无条件全部改变 124 26 96
新拍不是三连音且在 50~140 BPM 97 25 70
+ 无先验分布的得分比 0.93 以上 14 5 + 无正确答案的 1 首 8(吉他伴奏 7 首,极快的曲目 1 首)
+ 新拍明显分为两个八分音符(1.4 倍) 9 5 + 无正确答案的 1 首 3(吉他伴奏 2 首,极快的曲目 1 首)
+ 最初估计中也几乎持平(0.85) 6 5 + 无正确答案的 1 首 0
+ 和声变化分组 $G \ge 0$(采用) 6 5 + 无正确答案的 1 首 0

发生变化的是以下 6 首。

曲目 变化内容 与正确答案比较
「아주 먼 옛날」(Aju meon yennal,《很久很久以前》)(用户反馈) 12/8,92 BPM → 4/4,69 BPM,长度不同的小节 4 个 → 0(结尾小节除外) 无正确答案文件(和弦落在小节第一拍、第三拍,像 C–Em–Dm7–G7 那样均匀进行)
GTZAN pop.00004 6/8,107 BPM → 4/4,80 BPM 拍 F 0.30 → 1.00,强拍 F 0.17 → 1.00
GTZAN pop.00062 6/8,107 BPM → 2/4,81 BPM 拍 F 0.30 → 1.00,强拍 F 0.11 → 0.67
GTZAN hiphop.00035 12/8,112 BPM → 4/4,84 BPM 拍 F 0.28 → 1.00,强拍 F 0.15 → 1.00
GTZAN rock.00049 12/8,111 BPM → 4/4,84 BPM 拍 F 0.27 → 0.98,强拍 F 0 → 0(相同)
GuitarSet 00_Rock2-85-F 12/8,113 BPM → 4/4,85 BPM 拍 F 0.30 → 0.99,强拍 F 0.21 → 0.97

pop.00004 和 pop.00062 重新识别后的节拍跟随底鼓落在反拍上(拍 F 0),需要半拍移动规则才变好。存在同样错误(记为 12/8、6/8,速度为正确答案的 4/3 倍)的曲目有 GTZAN 11 首、GuitarSet 7 首和「아주 먼 옛날」,共 19 首,其中 6 首被修正。其余 13 首至少有一项上述条件未通过;若进一步放宽条件,正确速度为 3/2 倍快的吉他伴奏曲或速度极快的曲目会变成半速、反拍,因此没有放宽。看起来拍未被三分的曲目不做这一确认,因此结果与 1.11.4 相同。用 1.11.4 引擎和本引擎分别分析全部 1,268 首并比较,结果除上述 6 首外也全部相同;回归 18 首、调性·罗马数字级数测试、37 首合成曲目评分、真实录音套件、「세월이 가면」正确答案测试与 1.11.4 相同。

1.11.4 验证(不让任何现有曲目变差)

1.11.4 的三条规则(拍号变化确认、重新审视长度不同的小节、m7 与六和弦重叠时的调名)的条件,是按照在 1,268 首测试曲目(37 首合成曲目和 37 首未见过的合成曲目、15 首真实录音(包括 11 首带正确答案的流行歌曲)、GTZAN 999 首、GuitarSet 180 首)中一首也不变差来确定的。对每首曲子分析一次,把规则开、关两种设置都应用到结果上,比较小节、和弦、调性、拍号和速度。关闭规则时所有曲目与 1.11.3 相同;开启后结果发生变化的只有以下 7 首。

曲目 变化内容 与正确答案比较
最后的副歌被记成 5/4、每段副歌都夹入 2 拍小节的 4/4 曲子(用户反馈) 无拍号变化,长度不同的小节 27 个 → 0,调名小调 → 大调 无正确答案文件(通过反复和低音分析确认为 4/4、大调)
GTZAN hiphop.00067、reggae.00022、reggae.00077 长度不同的小节减少 强拍 F 0.56、0.89、0.81 → 1.00
GTZAN country.00039、jazz.00074 调名 F♯ 小调 → A 大调,E 小调 → G 大调 错 → 对
GTZAN blues.00061 调名 F 小调 → A♭ 大调 正确答案 G 小调,前后都错(MIREX 得分相同)

拍、强拍、和弦、调性中没有变差的曲目。调性方面,也没有与正确答案距离(MIREX 加权得分)变大的曲目(阈值设为 0.1 时,有一首会从正确答案的五度相邻调变为较远的调,因此定为 0.15)。回归 18 首、调性·罗马数字级数测试、37 首合成曲目评分、11 首真实录音套件、「세월이 가면」正确答案测试、GuitarSet 与 1.11.3 相同。

无条件时未采用的内容。 无条件使用三条规则时,发生变化的曲目很多:拍号变化确认 8 首(强拍 3 首变差),重新审视小节 159 首(强拍 72 首变好、34 首变差),调名 118 首(调性 26 首改对、29 首改错)。为保护现有曲目,只在依据明确时才使用,同时也放弃了大多数原本会变好的曲目。还测试了只把低音不是根音的 m7 计为大调的方法(76 首,调性 17 首改对、13 首改错),以及把调内 m7 的七音低音写成大三和弦/低音的命名规则(D♯m7/C♯ → F♯/C♯,「바다의 왕자」(Bada-ui wangja,《大海的王子》)根音 99.3% → 99.1%),但都没有采用。

1.11.1 验证(和弦时机、性质、速度检查)

分析速度。 在计算结果一位不变(相同运算、相同顺序)的前提下,把分析提速约 1.8 倍。采样率转换并行计算同一相位的 8 个输出,FFT 一次执行两级并只求用到的频率格。在 Chrome 中,5 首真实录音的分析加生成和弦谱所用时间合计从 20.2 秒降到 11.3 秒(抵消了 1.11.0 增加的部分,甚至比 1.10.5 还快),分析过程中的最大内存与 1.11.0 相同。结果是否相同,通过比较引擎在真实录音(包括单声道、3 声道、48 kHz 版本)、GuitarSet 90 首、合成曲目 74 首、多种采样率·长度·静音输入以及关闭的开关组合下输出的全部数值来确认。

修正调性退化。 在 1.11.0 中,「고민중독」(Gomin jungdok,《烦恼中毒》)的调性被错记为 B 小调 → E 小调 → F 小调(1.10.5 为与正确答案相同的 G 大调 → A♭ 大调)。由于套件中没有这首曲子的调性正确答案文件,1.11.0 的测试漏掉了这一点。原因是人声旋律削减留下的一个七和弦名称(前奏的 C 变成 C7),使前奏调性以 0.007 的得分差变成了 D 大调;通过过滤几乎持平的转调修正了这个问题。这条规则改变的只有「고민중독」以及 GTZAN 1000 首中 1 首嘻哈曲目的假转调(那首曲子前后都与正确答案不同);套件 10 首、GuitarSet 180 首、合成曲目、调性·罗马数字级数测试连调性都不变。

性质名称。 加入半减七和弦名称后,以 GuitarSet 演奏为准的性质从 59.37% 升到 60.28%,套件 10 首的性质从 66.0% 升到 66.2%(「이 사람이다」(I saramida,《就是这个人》)、「もう恋なんてしない」中的 m7♭5),每首曲子的分析时间在同一台电脑的 node 上平均从 4.0 秒降到 2.1 秒。根音、大小调、拍、调性在所有测试组中都不变。

调查过但未采用的内容。

1.11.1 的其他测试结果:37 首合成曲目和 37 首未见过的合成曲目、回归 18 首、调性·罗马数字级数测试、Warak,以及 GTZAN 和 GuitarSet 的拍、强拍、根音、大小调与 1.11.0 相同。「세월이 가면」正确答案测试为根音 94.7%、大小调 91.2%、性质 78.1%,保持不变。

延续与变化位置测试(1.8.4)

项目 1.8.3 1.8.4
合成曲目小节开头的灰色(实际都是重新弹奏处) 49 处 8 处,全部是曲子结束后最后一个和弦的余韵
和弦变化与正确答案同拍 / 早一拍 / 晚一拍 / 正确答案中没有的变化 772 / 50 / 19 / 35 776 / 46 / 19 / 35
回归 18 首和弦精确 / 类别 98.6% / 99.8% 98.9% / 100%
流行音乐型曲目和弦精确 / 类别 97.0% / 99.2% 97.2% / 99.3%
节拍、强拍、调性、特殊风格、罗马数字级数测试   相同
真实录音   和弦、强拍、节拍相同。6 处灰色中只有重新弹奏的 1 处(第 24 小节)变为黑色字
混有延续和弦的 pad 曲目(同一和弦相连的 8 处)   7 处正确

还确认了画面上的绘制位置。在桌面和移动设备宽度下打开 6 首曲子,306 个和弦名称全部与各自所在拍的斜线保持相同距离。

4-7. 特点与优势

4-8. 局限与未来

局限

今后值得尝试的方向

4-9. 参考文献

↑ 返回目录

更新记录

版本规则

版本号写作 主版本.次版本.构建 三个数字。当前版本显示在应用和帮助页面的标题旁。

位置 何时递增 例
主版本 应用名称或使用方式发生重大变化时。0 表示确定正式名称之前的开发版 1.0.0 名称定为 Chordity
次版本 新增功能时。构建号从 0 重新计数 1.3.0 罗马数字级数标记
构建 功能不变,修正错误、调整声音或数值、修改文字和文档时 1.4.1 润色提示文字

Chordity 1.x

1.11.10 (2026-10-21)

1.11.9 (2026-10-18)

1.11.8 (2026-10-18)

1.11.7 (2026-10-15)

1.11.6 (2026-10-14)

1.11.5 (2026-10-14)

1.11.4 (2026-10-14)

1.11.3 (2026-10-14)

1.11.2 (2026-10-13)

1.11.1 (2026-10-13)

1.11.0 (2026-10-13)

1.10.5 (2026-10-11)

1.10.4 (2026-10-11)

1.10.3 (2026-10-11)

1.10.2 (2026-10-11)

1.10.1 (2026-10-11)

1.10.0 (2026-10-11)

1.9.4 (2026-10-10)

1.9.3 (2026-10-10)

1.9.2 (2026-10-10)

1.9.1 (2026-09-30)

1.9.0 (2026-09-30)

1.8.4 (2026-09-30)

1.8.3 (2026-09-30)

1.8.2 (2026-09-30)

1.8.1 (2026-09-30)

1.8.0 (2026-09-30)

1.7.0 (2026-09-30)

1.6.0 (2026-09-30)

1.5.0 (2026-09-30)

1.4.2 (2026-09-27)

1.4.1 (2026-09-27)

1.4.0 (2026-09-26)

1.3.1 (2026-09-26)

1.3.0 (2026-09-26)

1.2.0 (2026-09-26)

1.1.1 (2026-09-26)

1.1.0 (2026-09-26)

1.0.0 (2026-09-26)

开发版 0.x(마디 코드)

在确定正式名称之前,以“마디 코드”(意为“小节和弦”)这一名称开发。

0.4.1 (2026-09-26)

0.4.0 (2026-09-25)

0.3.1 (2026-09-25)

0.3.0 (2026-09-25)

0.2.1 (2026-09-25)

0.2.0 (2026-09-25)

0.1.0 (2026-09-25)

↑ 返回目录