Python编曲实践(八):我,乔鲁诺·乔巴那,能用两百行代码写出JOJO黄金之风里我自己的出场曲!

15 篇文章 16 订阅
3 篇文章 0 订阅

前言

前些天笔者写的文章 Python编曲实践(七):整整一百行Python代码写出黑人抬棺梗曲《Astronomia》的旋律 受到了大家的许多支持和好评,本篇文章挑战更复杂、更有挑战性,同时也很有梗的一首音乐,那就是《JOJO的奇妙冒险第五部:黄金之风》的主要OST之一:Giorno’s Theme。这首音乐时常在主人公乔鲁诺·乔巴那召唤替身使者“黄金体验”(如下图)的时候响起,强烈的律动感使人过耳不忘。
在这里插入图片描述

B站有很多这首音乐的翻唱作品,其中下面这个比较有特色(为了更容易实现,本篇文章暂时忽略了中间的复杂部分,而保留了最有标志性的内容):

那不勒斯街头用萨克斯吹JoJo黄金之风的BGM-GIORNO'S THEME

与黑人抬棺曲《Astronimia》的相比,这篇音乐的结构更复杂,主要在以下几点:

  • 使用了两个音轨来模拟钢琴演奏时的左手低音区部分右手高音区部分
  • 出现了和弦,即在同一时间点多个音符同时奏响的织体结构
  • 使用了升降号,包括升号♯与降号♭,通过这两个标记来使得音符在原本的音高上升高或降低一个半音

后两种变化使得之前使用的MidiFileExtended类变得無駄無駄無駄了,为了适应这些变化,笔者对其进行了更新和升级。大家可以在文章头部👆👆下载,也可以通过百度网盘链接下载(提取码zyyc),并务必在运行代码前完成以下几项简单工作:

  • 使用pip指令安装好 mido 库和 PyGame 库,MidiExtended类中的相关功能依赖于这两个库;
  • 将midi_extended.zip中的内容解压,并将整个文件夹拷贝到工程根目录下;
  • 确保歌曲的保存路径是存在的

做好准备工作之后,我们就开始欧拉欧拉欧拉吧!如果这一过程中遇到任何问题请及时评论或私信反映给我!

二百行代码

同上一篇一样,如果你是个急性子,已经迫不及待地想刮起黄金之风,那么就可以在确保上述三项准备工作已经就绪的前提下直接在你的电脑上运行这二百行代码(或者可以在Github中参考这一文件):

from midi_extended.MidiFileExtended import MidiFileExtended

class GoldenWind(object):
    def __init__(self):
        self.bpm = 127
        self.time_signature = '4/4'
        self.key = 'Am'
        self.file_path = '../data/midi/write/golden_wind.mid'
        self.mid = MidiFileExtended(self.file_path, type=1, mode='w')

    def write(self):
        self.mid.add_new_track('Piano1', self.time_signature, self.bpm, self.key, {'0': 0})
        self.mid.add_new_track('Piano2', self.time_signature, self.bpm, self.key, {'0': 0})

        for i in [1, 2, 1, 3]:
            self.intro_outro(i, False)
        for i in [1, 2, 1, 4]:
            self.intro_outro(i, True)
        for i in [1, 2, 1, 3,
                  1, 2, 1, 3,
                  1, 2, 1, 3,
                  1, 2, 1, 3]:
            self.piano2_pattern(i)
        for i in [1, 2, 3, 4]:
            self.piano1_parapraph(i)
        for i in [1, 2, 1, 3]:
            self.intro_outro(i, False)
        for i in [1, 2, 1, 4]:
            self.intro_outro(i, True)

    def intro_outro(self, pattern, both):
        tracks = [self.mid.get_extended_track('Piano1'), self.mid.get_extended_track('Piano2')]
        for i in range(2):
            track = tracks[i]
            if not both and i == 1:
                track.wait(1)
                continue
            if pattern == 4:
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(6, 1/16, base_num=-1-i)
                track.add_note(7, 1/8, base_num=-1-i)
                track.wait(7/16)
            else:
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(7, 1/16, base_num=-1-i)
                track.add_note(6, 1/16, base_num=-1-i)
                track.wait(1/16)
                track.add_note(7, 1/16, base_num=-1-i)
                track.wait(1/16)
                if pattern == 1:
                    track.add_note(2, 1/16, base_num=-i)
                    track.wait(1/16)
                    track.add_note(7, 1/16, base_num=-1-i)
                    track.wait(1/16)
                    track.add_note(4, 1/16, base_num=-1-i, alt=1)
                elif pattern == 2:
                    track.add_note(4, 1/16, base_num=-i)
                    track.wait(1 / 16)
                    track.add_note(3, 1/16, base_num=-i)
                    track.wait(1 / 16)
                    track.add_note(2, 1/16, base_num=-i)
                elif pattern == 3:
                    track.add_note(4, 1/16, base_num=-i)
                    track.wait(1 / 16)
                    track.add_note(3, 1/16, base_num=-i)
                    track.wait(1 / 16)
                    track.add_note(7, 1/16, base_num=-1-i)
                track.add_note(6, 1/8, base_num=-1-i)

    def piano2_pattern(self, pattern):
        track = self.mid.get_extended_track('Piano2')
        if pattern == 1:
            track.add_note([7, 4, 7], 1/4+1/8, alt=[0, 1, 0], base_num=[-1, -1, -2])
            track.add_note([4, 2, 5], 1/8+1/4, alt=[0, 0, 1], base_num=[-1, -1, -2])
            track.wait(1/4)
        elif pattern == 2:
            track.add_note([7, 7], 1/2, base_num=[-1, -2])
            track.add_note([4, 4], 1/2, base_num=[-1, -2], alt=[1, 1])
        elif pattern == 3:
            track.add_note([1, 1], 1/2, base_num=[0, -1], alt=[1, 1])
            track.add_note([4, 4], 1/2, base_num=[-1, -2], alt=[1, 1])
        elif pattern == 4:
            track.add_note([4, 7], 1/4, base_num=[-1, -2], alt=[1, 0])
            track.wait(3/4)

    def piano1_parapraph(self, paragraph):
        track = self.mid.get_extended_track('Piano1')
        if paragraph == 1:
            track.add_note(4, 1/4+1/8, base_num=1, alt=1)
            track.add_note(4, 1/8+1/4, base_num=1)
            track.wait(1/8)
            track.add_note(2, 1/16, base_num=1)
            track.add_note(3, 1/16, base_num=1)

            track.add_note(4, 1/8+1/16, base_num=1)
            track.add_note(3, 1/16+1/8, base_num=1)
            track.add_note(2, 1/8, base_num=1)
            track.add_note(1, 1/8+1/16, base_num=1, alt=1)
            track.add_note(2, 1/16+1/8, base_num=1)
            track.add_note(3, 1/8, base_num=1)

            track.add_note(4, 1/4+1/8, base_num=1, alt=1)
            track.add_note(7, 1/8+1/4, base_num=1)
            track.add_note(7, 1/8)
            track.add_note(1, 1/8, base_num=1, alt=1)

            track.add_note(2, 1/8+1/16, base_num=1)
            track.add_note(3, 1/16+1/8, base_num=1)
            track.add_note(2, 1/8, base_num=1)
            track.add_note(1, 1/8+1/16, base_num=1, alt=1)
            track.add_note(6, 1/16+1/8, base_num=1)
            track.add_note(5, 1/8, base_num=1)

        if paragraph == 2:
            track.add_note([4, 2, 7], 1/4+1/8, alt=[1, 0, 0], base_num=[1, 1, 0])
            track.add_note([4, 2, 7], 1/8+1/4, base_num=[1, 1, 0])
            track.wait(1/8)
            track.add_note(2, 1/16, base_num=1)
            track.add_note(3, 1/16, base_num=1)

            track.add_note([4, 1, 5], 1/8+1/16, alt=[0, 1, 0], base_num=[1, 1, 0])
            track.add_note(3, 1/16+1/8, base_num=1)
            track.add_note(2, 1/8, base_num=1)
            track.add_note([1, 6], 1/8+1/16, alt=[1, 1], base_num=[1, 0])
            track.add_note(2, 1/16+1/8, base_num=1)
            track.add_note(3, 1/8, base_num=1)

            track.add_note([4, 7], 1/4+1/8, alt=[1, 0], base_num=[1, 0])
            track.add_note([7, 4], 1/8+1/4, base_num=[1, 1])
            track.add_note(7, 1/8, base_num=1)
            track.add_note(1, 1/8, base_num=2, alt=1)

            track.add_note([2, 7], 1/8+1/16, base_num=[1, 0])
            track.add_note(3, 1/16+1/8, base_num=1)
            track.add_note(5, 1/8)
            track.add_note(4, 1/8+1/16, alt=1)
            track.add_note(2, 1/16+1/8, base_num=1)
            track.add_note(3, 1/8, base_num=1)

        if paragraph == 3:
            track.add_note([4, 2, 7], 1/4+1/8, alt=[1, 0, 0], base_num=[1, 1, 0])
            track.add_note([4, 2, 7], 1/8+1/4, base_num=[1, 1, 0])
            track.wait(1/8)
            track.add_note(2, 1/16, base_num=1)
            track.add_note(3, 1/16, base_num=1)

            track.add_note([4, 7, 5], 1/8+1/16, base_num=[1, 0, 0])
            track.add_note(3, 1/16+1/8, base_num=1)
            track.add_note(2, 1/8, base_num=1)
            track.add_note([1, 5], 1/8+1/16, alt=[1, 0], base_num=[1, 0])
            track.add_note(2, 1/16+1/8, base_num=1)
            track.add_note(3, 1/8, base_num=1)

            track.add_note([4, 2, 7], 1/4+1/8, alt=[1, 0, 0], base_num=[1, 1, 0])
            track.add_note([7, 4, 1], 1/8+1/4, base_num=[1, 1, 1], alt=[0, 0, 1])
            track.add_note(7, 1/8, base_num=1)
            track.add_note(1, 1/8, base_num=2, alt=1)

            track.add_note(2, 1/8+1/16, base_num=1)
            track.add_note(3, 1/16+1/8, base_num=1)
            track.add_note(2, 1/8, base_num=1)
            track.add_note(1, 1/8+1/16, base_num=1, alt=1)
            track.add_note(6, 1/16+1/8, base_num=1)
            track.add_note(5, 1/8, base_num=1)

        if paragraph == 4:
            track.add_note([4, 2, 7], 1/4+1/8, alt=[1, 0, 0], base_num=[1, 1, 0])
            track.add_note([4, 2, 7], 1/8+1/4, base_num=[1, 1, 0])
            track.wait(1 / 8)
            track.add_note(2, 1/16, base_num=1)
            track.add_note(3, 1/16, base_num=1)

            track.add_note([4, 1, 5], 1/8+1/16, alt=[0, 1, 0], base_num=[1, 1, 0])
            track.add_note(3, 1/16+1/8, base_num=1)
            track.add_note(2, 1/8, base_num=1)
            track.add_note([1, 6], 1/8+1/16, alt=[1, 1], base_num=[1, 0])
            track.add_note(2, 1/16+1/8, base_num=1)
            track.add_note(3, 1/8, base_num=1)

            track.add_note([4, 2], 1/4+1/8, base_num=1, alt=[1, 0])
            track.add_note([7, 4], 1/8+1/4, base_num=1)
            track.add_note(7, 1/8)
            track.add_note(1, 1/8, base_num=1, alt=1)

            track.add_note(2, 1/8+1/16, base_num=1)
            track.add_note(5, 1/16+1/8, base_num=1)
            track.add_note(4, 1/8, base_num=1, alt=1)
            track.add_note(4, 1/8+1/16, base_num=1)
            track.add_note(2, 1/16+1/8, base_num=2)
            track.add_note(6, 1/8, base_num=1, alt=1)


if __name__ == '__main__':
    golden_wind = GoldenWind()
    golden_wind.write()
    golden_wind.mid.save_midi()
    golden_wind.mid.play_it()

大家可能已经发现,这两百行代码的复杂程度较上一篇《Astronomia》而言有较大的提升,不过不用担心,下面的内容我将带大家梳理好音乐结构与代码安排的对应关系,并让大家熟悉一下MidiExtended这个类的基础使用方法。

实现过程

寻找曲谱资源

看过JOJO的朋友们都知道,其中的音乐不论是插曲还是片头片尾曲,质量都一点不含糊,而这一首《Giorno’s Theme》则因为其复杂的结构和大量的即兴演绎空间而受到大量音乐UP主的喜爱,因而变得超级火爆。经过大量搜索,最终笔者找到了既能体现原版音乐旋律精髓,又不会因太过冗长而难以实现的一个最佳版本,大家可以去试听一下,其中包含的两页曲谱如下图(倒数第三小节出现了明显的问题,笔者在本文中进行了修正,同时对结尾部分使用了前奏部分的结构):
在这里插入图片描述
可以发现这一曲谱包含两个声部,上面的五线谱有一个高音谱号,下面的五线谱有一个低音谱号,分别对应于钢琴的高音区与低音区:
在这里插入图片描述
通过这一对照表,我们就可以对五线谱中表示的旋律进行辨识,并开始正式编程:

初始化

在加入音符前需要对GoldenWind类进行初始化,分别对音乐的速度(BPM)、节拍、调性、MIDI文件保存地址和使用的MidiFileExtended对象进行初始化(根据五线谱的调号来看,这一段音乐的调性可能是C大调或者是A小调,笔者通过音符的出现范围来看初步判断它是A小调,如果有错误请及时指正):

    def __init__(self):
        self.bpm = 127
        self.time_signature = '4/4'
        self.key = 'Am'
        self.file_path = './golden_wind.mid' 
        self.mid = MidiFileExtended(self.file_path, type=1, mode='w')

下面是用来调用负责不同部分的函数来实现整篇音乐的write函数,其中声明了两个Track来负责不同的钢琴部分。由于整篇音乐第一部分与最后一部分相同(笔者修改过,与原乐谱不同),故可以通过调用intro_outro函数来统一实现;而中间部分的高音区部分使用piano1_paragraph函数来实现,低音区部分使用piano2_pattern函数实现:

    def write(self):
        self.mid.add_new_track('Piano1', self.time_signature, self.bpm, self.key, {'0': 0})
        self.mid.add_new_track('Piano2', self.time_signature, self.bpm, self.key, {'0': 0})

        for i in [1, 2, 1, 3]:
            self.intro_outro(i, False)
        for i in [1, 2, 1, 4]:
            self.intro_outro(i, True)
        for i in [1, 2, 1, 3,
                  1, 2, 1, 3,
                  1, 2, 1, 3,
                  1, 2, 1, 3]:
            self.piano2_pattern(i)
        for i in [1, 2, 3, 4]:
            self.piano1_parapraph(i)
        for i in [1, 2, 1, 3]:
            self.intro_outro(i, False)
        for i in [1, 2, 1, 4]:
            self.intro_outro(i, True)

前奏/尾奏:intro_outro函数

前奏/尾奏包括八小节的长度,对应乐谱前八小节的内容,通过intro_outro函数来实现。
改进后的add_note函数中第一个参数表示一个八度内的音高,第二个参数表示以全音符为单位长度下音符的时值,base_num表示以中央C的音区为基准的降低/升高八度数,alt用于表示升/降音高

    def intro_outro(self, pattern, both):
        tracks = [self.mid.get_extended_track('Piano1'), self.mid.get_extended_track('Piano2')]
        for i in range(2):
            track = tracks[i]
            if not both and i == 1:
                track.wait(1)
                continue
            if pattern == 4:
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(6, 1/16, base_num=-1-i)
                track.add_note(7, 1/8, base_num=-1-i)
                track.wait(7/16)
            else:
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(7, 1/16, base_num=-1-i)
                track.add_note(6, 1/16, base_num=-1-i)
                track.wait(1/16)
                track.add_note(7, 1/16, base_num=-1-i)
                track.wait(1/16)
                if pattern == 1:
                    track.add_note(2, 1/16, base_num=-i)
                    track.wait(1/16)
                    track.add_note(7, 1/16, base_num=-1-i)
                    track.wait(1/16)
                    track.add_note(4, 1/16, base_num=-1-i, alt=1)
                elif pattern == 2:
                    track.add_note(4, 1/16, base_num=-i)
                    track.wait(1 / 16)
                    track.add_note(3, 1/16, base_num=-i)
                    track.wait(1 / 16)
                    track.add_note(2, 1/16, base_num=-i)
                elif pattern == 3:
                    track.add_note(4, 1/16, base_num=-i)
                    track.wait(1 / 16)
                    track.add_note(3, 1/16, base_num=-i)
                    track.wait(1 / 16)
                    track.add_note(7, 1/16, base_num=-1-i)
                track.add_note(6, 1/8, base_num=-1-i)

这一函数的关键设置如下:

  • 整个八小节是四种不同小节样式的组合。故使用参数中的pattern用于区分不同的小节样式,并根据pattern参数的值来进行不同小节样式的音符输入,以避免冗余代码;
  • 高音区与低音区的音高差了整整一个八度。这样就可以采用数组的形式来存储两个音轨,并通过循环变量i来辅助对低音区音轨降低一个八度;
  • 为了使得音乐结构复杂度的渐进性体现的更好,我对这八小节的内容进行了改编,使得前四小节仅有高音部分而删去了低音部分,函数内通过both参数来判断是否使用两个声部

中间部分的低音区样式:piano2_pattern函数

通过观察可以发现,在中间十六小节的音乐部分中,低音区仅仅有四种不同的样式,故可以通过以下函数来实现,其中pattern参数用于区分不同的样式:

    def piano2_pattern(self, pattern):
        track = self.mid.get_extended_track('Piano2')
        if pattern == 1:
            track.add_note([7, 4, 7], 1/4+1/8, alt=[0, 1, 0], base_num=[-1, -1, -2])
            track.add_note([4, 2, 5], 1/8+1/4, alt=[0, 0, 1], base_num=[-1, -1, -2])
            track.wait(1/4)
        elif pattern == 2:
            track.add_note([7, 7], 1/2, base_num=[-1, -2])
            track.add_note([4, 4], 1/2, base_num=[-1, -2], alt=[1, 1])
        elif pattern == 3:
            track.add_note([1, 1], 1/2, base_num=[0, -1], alt=[1, 1])
            track.add_note([4, 4], 1/2, base_num=[-1, -2], alt=[1, 1])
        elif pattern == 4:
            track.add_note([4, 7], 1/4, base_num=[-1, -2], alt=[1, 0])
            track.wait(3/4)

这一段代码包含了四个和弦,这一功能可以通过将add_note函数的第一个参数设置为数组来实现,若每个组成音的长度、所在八度区域、升降号不同的话,也可以将后面的参数设置成数组形式,并保证数组中参数的顺序相互对应。

中间部分的高音区段落:piano1_paragraph函数

中间十六小节的高音部分在整篇音乐中是最为复杂的,也是最难以实现的一部分,博主是一个一个音符来。听过之后可以发现,这一段音乐是四小节为单位进行划分的,即每四小节音乐的大体行进结构是相同的,故我们使用paragraph这个参数来区分不同的段落(由于最后四小节的内容明显出现了不协调,我对其进行了微调,故与原曲谱有一定的出入)。
函数整体由于体量太大,在此不单独给出,如果想参考的话请移步二百行代码板块,或者参考Github上的文件

主函数

同上一篇文章一样,主函数用于调用write函数来编写音乐,并对音乐进行保存和播放。若运行成功则会立即播放音乐,并在之前选择的保存目录中找到该MIDI文件。

if __name__ == '__main__':
    golden_wind = GoldenWind()
    golden_wind.write()
    golden_wind.mid.save_midi()
    golden_wind.mid.play_it()

生成的 golden_wind.mid文件MidiEditor 中打开界面如下:
在这里插入图片描述

结语

如果您对本文使用的 MidiFileExtended 类感兴趣,或者想进一步了解该类的话,请在下方评论或私信联系我,也欢迎大家对的文章提出质疑和改进方法。如您在实现本文章中提到的任何内容时遇到任何困难,请及时在下方评论或私信联系我!
最后,欢迎大家查看本专题下其他博文内容,十分感谢您的耐心阅读!

Python编曲实践(一):通过Mido和PyGame来编写和播放单轨MIDI文件
Python编曲实践(二):和弦的实现和进行
Python编曲实践(三):如何模拟“弯音轮”实现滑音和颤音效果
Python编曲实践(四):向MIDI文件中添加鼓组音轨
Python编曲实践(五):通过编写爬虫来爬取海量MIDI文件,预备构建数据集(附有百度云下载链接)
Python编曲实践(六):将MIDI文件转化成矩阵,继承PyTorch的Dataset类来构建数据集(附数据集网盘下载链接)
Python编曲实践(七):整整一百行Python代码写出黑人抬棺梗曲《Astronomia》的旋律

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值