Python编曲实践(九):如何计算并估计音乐的调性(大/小调+主音)?Krumhansl-Schmuckler调性分析算法的原理与实现

前言

之前,我在 Python编曲实践(五)中记录了构建MIDI数据集Free MIDI Library的过程,其中预处理阶段十分重要的一个步骤是移调,即把所有音乐的调性调整为C大调或A小调,这样会使得音乐数据一致性更强,进而提高生成音乐的质量。为了节省时间,当时我使用的是music21库提供的调性分析函数,并没有对其算法和实现方式进行深层探究,之后通过认真阅读其用户手册,发现其使用的是Krumhansl-Schmuckler调性分析算法这一古老但有效的调性分析算法。考虑到如今尚无对这一算法的中文介绍,本篇博文的主题即对这一经典的调性分析算法进行介绍,并通过Python来设法实现它。

算法原理

Krumhansl-Schmuckler调性分析算法的核心思想是计算当前音乐的音符总时长与24种(12个主音×大/小调)不同调性特征权重(Key-profile weights)的皮尔逊积矩相关系数(Pearson correlation coefficient),并比较得到这24个值之中的绝对值最大的数值,其对应的调性即为通过这一算法得到的调性信息。

下面是该算法的逐步分析,具体过程可以参考keycor官网:

  1. 计算出音乐中12种不同音高的音符累计总时长,在此处的音高可以忽略八度的区分;
  2. 选择一种调性特征权重,常用的有Krumhansl-Kessler、Aarden-Essen、Simple pitch、Bellman-Budge、Temperley-Kostka-Payne五种,每一种调性特征权重有各自的优劣,需要通过具体情况来选择;
  3. 对音符累计总时长进行调整,将当前假设的主音排在列表的首位,与调性分析权重计算得到皮尔逊积矩相关系数,总共得到24个数值;
  4. 通过argmax函数得到绝对值最大的皮尔逊积矩相关系数对应的调性,即为预测的调性

Python实现

完整代码详见github:MusicCritique/util/tonal.py

1. 计算音符累计时长

这一过程通过get_note_lengths函数实现:

def get_note_lengths(path):
    notes_length = [0 for _ in range(12)]
    pm = pretty_midi.PrettyMIDI(path)
    for instr in pm.instruments:
        if not instr.is_drum:
            for note in instr.notes:
                length = note.end - note.start
                pitch = note.pitch
                notes_length[pitch % 12] += length

    return notes_length

这一函数使用了pretty_midi库打开一个MIDI文件,并对除鼓之外的所有乐器的音符进行遍历,用结束时间减去开始时间便得到了该音符的时长。由于音符时长的累计过程不需要考虑当前音高所在八度,故可以直接将音高的数值模12,并累加到对应的位置。
此函数返回的数组格式如下,其中包含12个数值,对应的是十二种音高累加的总时长,单位是秒(s):

[100.71053470833331, 26.91579171666669, 598.2579445916629, 11.24736935833333, 189.86317371666692, 1.8631580500000098, 183.55264687500002, 460.25266993333423, 5.0105267333333074, 248.26317858333346, 0.22105264999999985, 425.22108806666756]
2. 获得调性特征权重

调性特征权重也是一个长度为12的数组,衡量的是在大调或小调中,不同音高的稳定性与重要性,其中主音和属音(五音)的数值最大,三音次之,不在调内的音高数值最小。常用的调性特征权重有5种,分别是Krumhansl-Kessler、Aarden-Essen、Simple pitch、Bellman-Budge和Temperley-Kostka-Payne,具体数值可以参考keycor官网。

这五种权重的对比图如下,其中KK对应Krumhansl-Kessler,S对应Simple pitch,KP对应Temperley-Kostka-Payne,BB对应Bellman-Budge,AE对应Aarden-Essen,上方是大调的情况,下方是小调的情况:
Python编曲实践(九):如何计算并估计音乐的调性(大/小调+主音)?Krumhansl-Schmuckler调性分析算法的原理与实现_第1张图片
根据图像可以看到,不同的权重取值对于不同位置的音高重视程度均有不同,而keycor官网对它们的评价如下:

  • Krumhansl-Kessler:强烈倾向于将属音(五音)判断成主音
  • Aarden-Essen:微弱倾向于将下属音(四音)判断成主音
  • Bellman-Budge:没有明显混淆倾向
  • Temperley-Kostka-Payne:倾向于把关系大/小调的主音判断成当前的主音
  • Simple:在大部分音乐之上表现较好,在小部分音乐之上表现不好

我使用了get_weights函数来取得调性特征权重,由于music21提供了比较完善的接口,在此处主要是调用music21的函数来取得:

def get_weights(mode, name='ks'):
    if name == 'kk':
        a = analysis.discrete.KrumhanslKessler()
        # Strong tendancy to identify the dominant key as the tonic.
    elif name == 'ks':
        a = analysis.discrete.KrumhanslSchmuckler()
    elif name == 'ae':
        a = analysis.discrete.AardenEssen()
        # Weak tendancy to identify the subdominant key as the tonic.
    elif name == 'bb':
        a = analysis.discrete.BellmanBudge()
        # No particular tendancies for confusions with neighboring keys.
    elif name == 'tkp':
        a = analysis.discrete.TemperleyKostkaPayne()
        # Strong tendancy to identify the relative major as the tonic in minor keys. Well-balanced for major keys.
    else:
        assert name == 's'
        a = analysis.discrete.SimpleWeights()
        # Performs most consistently with large regions of music, becomes noiser with smaller regions of music.
    return a.getWeights(mode)
3. 计算皮尔逊积矩相关系数并得到最大绝对值

通过krumhansl_schmuckler函数实现:

def krumhansl_schmuckler(path):
    note_lengths = get_note_lengths(path)
    key_profiles = [0 for _ in range(24)]

    for key_index in range(24):

        if key_index // 12 == 0:
            mode = 'major'
        else:
            mode = 'minor'
        weights = get_weights(mode, 'kk')

        current_note_length = note_lengths[key_index:] + note_lengths[:key_index]

        pearson = stats.pearsonr(current_note_length, weights)[0]

        key_profiles[key_index] = math.fabs(pearson)

    key_name = get_key_name(np.argmax(key_profiles))
    print(key_name)
    # print(key_profiles, '\n', note_lengths)

此函数首先调用了get_note_lengths,用以取得音符累计时长,之后对24种调性分别进行调整,得到current_note_length,即对应当前调性的音符累计时长,之后调用scipy库提供的pearsonr函数计算出皮尔逊积矩相关系数,添加到key_profiles数组中,循环之后使用argmax函数得到绝对值最大的数值对应index,即为预测得到的调性。

4. 测试

为了测试我们实现的调性预测算法,我们使用test函数来进行测试:

def test():
    path = '../data/midi/read/All You Need Is Love - Beatles.mid'

    find_meta(path)

    s = converter.parse(path)
    s.plot('histogram', 'pitch')
    p = analysis.discrete.KrumhanslKessler()
    print(p.getSolution(s))
    
    krumhansl_schmuckler(path)

测试使用的是披头士乐队的《All You Need Is Love》,首先进行的操作是在MIDI文件的元信息中查找调性信息,使用的是find_meta函数,通过这一函数可以知道此MIDI文件的真实调性,作为我们调性预测的最终参考:

def find_meta(path):
    pm = pretty_midi.PrettyMIDI(path)
    for ks in pm.key_signature_changes:
        print(ks)

之后,通过music21提供的plot函数得到音符分布条形图,并使用music21库提供的Krumhansl Kessler算法分析接口来进行调性分析,最后调用的是我们我们之前编写的krumhansl_schmuckler函数,为了统一变量,使用的都是Krumhansl-Kessler调性特征权重,其运行结果如下:

G Major at 0.00 seconds
D major
G major

其中第一个值是在元数据中查找到的调性信息,第二个值是music21库提供的调性分析函数得到的结果,第三个值是使用我们编写的函数预测得到的结果,可以看到我们自己实现的Krumhansl Kessler算法比music21更加准确,同时耗费的时间也明显减少。虽然这一个例并无法证明我们的实现优于music21库的实现,但是可以证明其自身是有效的。

再看一下生成的音符分布条形图,可以看到作为属音的D累计时长数值比主音G更大,而music21库提供的算法接口也是将D音错误判断成了主音,这证明了keycor官网对Krumhansl Kessler算法的评价是有一定道理的,即倾向于把属音判断成主音。
Python编曲实践(九):如何计算并估计音乐的调性(大/小调+主音)?Krumhansl-Schmuckler调性分析算法的原理与实现_第2张图片

总结

虽然通过Krumhansl-Schmuckler调性分析算法这个二十余年前的算法仍然能够较为可靠地预测音乐的调性,但是若想侦测到音乐中的变调现象,还需要对其进行进一步的改进,而且它仅仅考虑到了大调和小调的情况,而忽略了其他七声音阶和五声音阶。希望本篇博文介绍的Krumhansl-Schmuckler调性分析算法能够作为此领域比较优秀的一个基础算法,为之后探索更优的调性分析算法提供灵感与思路。最后,感谢您的耐心阅读,若有任何问题欢迎评论或与我私信交流!Rock on brother!

你可能感兴趣的:(编曲,Python,音乐,机器学习,python,算法,MIDI,MIR)