Skip to content

从一首完整的钢琴曲里提取主旋律以及所有和弦的算法

Rainbow Dreamer edited this page Apr 7, 2022 · 12 revisions

最近在写一些musicpy的功能的时候,遇到了需要把一首曲子的主旋律和和弦部分分离出来的需求。比如有个功能是对曲子进行智能作曲分析。比如现在我们读取了一个MIDI文件,这是一首钢琴曲,主旋律和和弦部分全部都是放在一个轨道的。比如说这首钢琴曲总共有1400个音符,包括主旋律和和弦部分。现在我们需要对这1400个音符进行调性分析,(暂时不考虑曲子的转调和离调)比如判断这首曲子是大调还是小调。

这里我们需要提一下关系大小调的乐理概念。关系大小调就是同样一组音,但是一个是大调,另一个是小调。比如C大调和A小调互为关系大小调,他们的构成音都是CDEFGAB,只是A小调的音阶顺序为ABCDEFG。比如现在这1400个音符的绝大部分的音都来自CDEFGAB,暂时不考虑中古调式的话,那么这首曲子可能是C大调,也可能是A小调。

一般来说,一首大调的曲子,开始和结束的和弦都很可能是大和弦,尤其是如果结束在大调的1级大和弦,那么基本可以判定为大调。比如在C大调中,一共七个音,CDEFGAB,一级和弦就是C和弦(种类可以有很多,比如一级自然三和弦就是C大三和弦,还有一级自然七和弦是C大七和弦等等)。如果是小调的曲子,那么就是开始和结束都很可能是小和弦,尤其是如果结束在小调的1级和弦(小调的1级和弦也就是其关系大调的6级和弦,是个小和弦),那么基本可以判定为小调。不过这种判定方法其实非常容易出现差错,因为其实例外太多了。比如我自己写的音乐就有很多不能这样判断出来调性的。一个相对来说靠谱很多的方法是,寻找这首曲子围绕着的那个音,也就是找主音。除了找主音之外,因为C大调的最重要的主和弦是其1级和弦(比如C大三和弦,构成音C,E,G),A小调的最重要的主和弦是其1级和弦(比如A小三和弦,构成音A,C,E),那么我们可以比较C大三和弦的音在主旋律中出现的频率和A小三和弦的音在主旋律中出现的频率哪个更高。因为C,E这两个音是C大三和弦和A小三和弦共有的,因此只需要比较在主旋律中G这个音出现的多还是A这个音出现的多。(经过测试,这种判断主旋律大小调倾向性的方法非常实用,判断准确率很高,前提是曲子出现的音名只有7个并且构成一个大调音阶或者其衍生调式,小调是大调的第6个衍生调式)这种方法再结合之前说到的看第一个和弦和最后一个和弦的方法,也能达成更理想的效果。

除了判断调性之外,还需要对一首曲子的和弦走向进行分析,因此和弦部分也需要单独提取出来(包括之前要看第一个和最后一个和弦也有这个需求)。可能有人会觉得,提取一首曲子的主旋律不就是把最高的那部分音抓下来吗,不过其实还真的没有这么简单。首先,在MIDI文件读取出来后,音符的先后顺序取决于每个音符出现的时间(读到这个音符的note_on信息)。musicpy里的和弦类的数据结构设计是把音符存放在一个列表里,然后每两个相邻音符之间的间隔存放在另一个列表里,每个音符自己有个参数duration,记录着这个音符的持续长度。音符在列表里的先后顺序也是和MIDI文件里的信息是一样的,哪个音符先出现开始演奏的信息就在列表里先出现。这首钢琴曲里的主旋律与和弦部分是放在一起的,难点在于,很多和弦音,尤其是分解和弦的最高音可能很高,弹到只比当前的主旋律低一点点。这些弹到主旋律范围内的和弦音是为了给主旋律进行和声装饰以及丰富曲子的层次感与律动感。但是我们人耳在听的时候,还是能分辨出那些弹到主旋律的和弦音,因为这些音并没有和主旋律一样形成“连贯的旋律感”,所以一般情况下不会被听众归类为主旋律。(不过也不是完全没有例外,偶尔有时候有些钢琴曲就是把一部分主旋律同时也当做当前的和弦的最高音的)

这首钢琴曲的音域跨度很大,主旋律的音域横跨三个八度左右,和弦部分也横跨三个八度左右,因此找一个单独的音作为分界线来划分主旋律与和弦部分很明显是不现实的。

现在来说说我的算法。首先,把所有音符里间隔为0的音符组找出来,(也就是同时一起弹的音符)然后只保留最高的音。因为如果是一个主旋律,下面同时弹了一些音,那么这些音一般来说就是和弦音或者是丰富和声用的装饰音。接下来,开始一个循环,遍历整个音符列表。这里需要提到我这个算法里的两个原创概念:旋律归类容许阈值(melody tolerance),和弦归类容许阈值(chord tolerance),这两个值经过多次测试之后,默认值为小七度(minor seventh)和大六度(major sixth)。首先,对于第一个音符,如果第二个音符比第一个音符的音高差要大于或等于和弦归类容许阈值,则第一个音符被判定为和弦音,第二个音符为第一个被判定为旋律的音符,否则就是第二个音符为和弦音符,第一个音符为主旋律音符。现在我们已经有了第一个主旋律音符。在遍历整个音符列表的过程中,计算最近一个小节的长度之内(8个单位音符长度)被归类为旋律的音符的平均音高,如果当前被归类为旋律的音符总长度还不满一个小节,则计算所有当前被归类为音符的平均音高。如果当前音符的音高要高于这个平均音高,则被判定为主旋律音符。如果比平均音高低,但是音高差要小于或等于旋律归类容许阈值,则当前的音符很有可能是主旋律音符。在这个条件成立的基础上,如果最近的一个被判定为主旋律的音符与当前音符的音高差小于和弦归类容许阈值,则当前的音符被判定为主旋律音符。(因为如果最近的主旋律的音符需要突然跳很远下来到当前音符,那么即使作曲者本意是想把当前音符也当做主旋律,听众也很难形成旋律上的连贯感,因为旋律本质上就是一组音高比较相近的音符,与音高差距大的和弦音形成对比而被当做旋律的。)如果当前音符比平均音高低,而且音高差超过旋律归类容许阈值,当且仅当最近的一个主旋律音符与当前音符的音高差和下一个音符与当前音符的音高差都小于和弦容许归类阈值,当前的音符被判定为主旋律音符。这个算法到这里就讲完了。我用这个自己想的主旋律分离算法给自己的很多音乐作品进行了测试,效果出奇的好,可以几乎100%地提取出完整的正确的主旋律音,无论主旋律的音域跨度有多大。主旋律提纯算法成功了之后,和弦提取就很简单了,其实也就是主旋律都找出来后,剩下的音就是和弦音或者主旋律装饰和声了。把和弦部分分解为一个个单独的和弦的算法我也想出来了,这个留到下期再说。还有对于一首曲子的调性判断的算法,这两天我构思了很多不同的算法,今天上午终于写出来了一个准确率较高的算法,包括对于有转调和离调的曲子,各种中古调式等等都可以判断出来。这些算法我会在其他的wiki条目给大家讲解。

在musicpy里我把这个原创的分离一首曲子的主旋律与和弦音的算法写成了一个函数,叫做split_melody,可以把一个和弦类型的曲子分离出主旋律作为新的和弦类型。还有split_chord函数可以分离出和弦音作为新的和弦类型,split_all函数会返回2个和弦类型,分别是曲子的主旋律与和弦音。

split_melody函数的参数按照顺序为:

split_melody(current_chord,
             mode='index',
             melody_tol=minor_seventh,
             chord_tol=major_sixth,
             get_off_overlap_notes=True,
             average_degree_length=8,
             melody_degree_tol=toNote('B4'))
  • current_chord: 想要进行分离主旋律与和弦音的和弦类型(一个和弦类型本身可以储存一个单乐器的完整的曲子,比如钢琴曲)

  • mode: 分离主旋律后返回的具体内容的表达形式,mode='index'返回的是所有的主旋律音符在x中的位置(第几个音),mode='notes'返回的是所有的主旋律音符的列表,mode='hold'返回的是所有的主旋律音符组成的新的和弦类型,其音符间隔都会经过重新计算,以达到和实际上把和弦音去掉之后剩下的音符的间隔一样。

  • melody_tol: 这个参数就是上面我在描述我的算法的时候说的旋律归类容许阈值(melody tolerance),可以自己设定音程大小,默认值为minor_seventh(小七度)。

  • chord_tol: 这个参数就是上面我在描述我的算法的时候说的和弦归类容许阈值(chord tolerance),可以自己设定音程大小,默认值为major_sixth(大六度)。

  • get_off_overlap_notes: 这个参数是为了应对有些MIDI文件音符大量重叠(同音重叠)可能造成算法无法达到预期效果的情况,设置为True的时候会检测MIDI文件中有无重叠音符的情况,并且把重叠的音符全部去重。

  • average_degree_length: 算法中的每过几个小节计算当前平均的音高。默认值为8小节。

  • melody_degree_tol: 这个是最近我改进了这个算法之后新加入的第3个容许阈值,叫做旋律音级归类容许阈值(melody degree tolerance),主要用来判断和弦音有来回转折的情况,比如阿尔贝蒂低音这种情况,可以让主旋律与和弦音更加灵活地分离,能够更加提高分离主旋律与和弦音的纯度。默认值为B4这个音。

接下来我把具体的操作描述一下。

首先我们把曲子准备好,可以是任意的MIDI文件,然后使用read函数读取(read函数的使用我在《如何使用musicpy》的章节里有写)你想要分离的音轨。(记得把MIDI文件的文件路径写对才能正确读取)比如

bpm, a, start_time = read('example.mid').merge()

read函数返回的是一个乐曲类型,使用merge函数可以得到1个有着3个元素的tuple,分别为曲子的速度(BPM),曲子的和弦类型,曲子开始的时间(单位为小节)

然后我们可以使用

example_melody = a.split_melody(mode='hold')

得到example这首曲子的主旋律组成的新的和弦类型,然后我们可以使用play函数或者write函数把主旋律写进新的MIDI文件,(play函数的name可以不写,输出的MIDI文件的名字默认为temp.mid)

play(melody, bpm, name='example melody.mid')

或者

write(melody, bpm, name='example melody.mid')

然后example的主旋律的MIDI文件就会生成在musicpy文件夹里了,大家可以拿去编曲宿主或者其他需要的场合使用。

使用split_chord函数可以提取一首曲子的和弦音,其实就是做和split_melody互补的事情,具体操作和上面的差不多,只是输出的MIDI文件变成了一首曲子的和弦音,比如叫做example_chords。

使用split_all函数可以同时得到一首曲子的主旋律与和弦音的2个和弦类型,大家都可以用一样的方法分别输出为新的MIDI文件。但是split_all这个函数在mode='hold'的时候, 除了返回主旋律与和弦音的2个和弦类型之外,还会返回第3个返回值shift,这个值表示的是和弦音开始的小节比主旋律开始的小节往后了多少小节(如果和弦音是在主旋律之前开始,shift的值就是负数),shift这个返回值很有用,当你对主旋律或者和弦音作出修改之后,想要在musicpy里重新合并为一首新的曲子,又想要和之前最开始的曲子的开始位置一样的时候,就可以用

example_melody & (example_chords, shift)

来得到跟之前的曲子开始位置一样的新的曲子。

请注意:split_all函数返回的主旋律与和弦音的2个和弦类型的音符列表都含有和分离前的曲子相同的pitch_bend和tempo类型,因为非音符类型不参与分离算法,但是会在分离结果中进行保留。因此当你合并主旋律与和弦音的时候,请清除其中一个和弦类型的pitch_bend和tempo类型再进行合并,以避免pitch_bend和tempo类型重复出现,推荐使用和弦类型自带的only_notes函数直接获得一个只含有音符类型的和弦类型。比如:

example_melody & (example_chords.only_notes(), shift)

主旋律与和弦音保留的pitch_bend和tempo类型的开始时间都是相对于原曲的,因此在主旋律或者和弦音不是从原曲最开始出现的情况下,pitch_bend和tempo类型的开始时间在主旋律或者和弦音单独播放的时候可能会有偏差,如果想要获得准确的pitch_bend和tempo类型相对于主旋律或者和弦音的开始时间,可以使用split_all函数的返回值的第3个值shift来将pitch_bend和tempo类型的开始时间进行移位 (如果要合并为原曲就不需要改变开始时间),使用和弦类型自带的apply_start_time_to_changes函数,参数值为start_time,为一个数值,可以将和弦类型里的非音符类型的开始时间加上start_time。如果shift为正数,那么表示主旋律从头开始,和弦音在主旋律开始之后一段时间开始,因此主旋律不需要修改非音符类型的开始时间,和弦音的非音符类型的开始时间全部减去shift的值,就可以在和弦音单独播放的时候与原曲的弯音和速度变化的开始时间同步。如果shift为负数,那么和弦音不用做出修改,主旋律的非音符类型的开始时间全部减去-shift的值。比如:

example_melody_individual = copy(example_melody)
example_chords_individual = copy(example_chords)
if shift >= 0:
    example_chords_individual.apply_start_time_to_changes(-shift)
else:
    example_melody_individual.apply_start_time_to_changes(shift)

对于不同的曲子,设置不同的3个容许阈值可能会让效果更好,默认的3个容许阈值可以适用于大部分流行或者爵士钢琴曲。

这个我在musicpy的基础上开发的原创的从一首完整的曲子里提取主旋律以及所有和弦的算法的效果超级好,已经先后被开发编曲宿主软件的大佬和研究人工智能音乐作曲的开发团队看上并且找我授权拿去实际使用了。在我写的一个智能钢琴软件Ideal Piano中,我也有把这个算法作为其中一项特色的功能,大家选择MIDI文件之后可以选择分离主旋律,只听和弦音。

Clone this wiki locally