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

本文主要是介绍Python编曲实践(八):我,乔鲁诺·乔巴那,能用两百行代码写出JOJO黄金之风里我自己的出场曲!,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

前些天笔者写的文章 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 MidiFileExtendedclass GoldenWind(object):def __init__(self):self.bpm = 127self.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)continueif 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 = 127self.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)continueif 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》的旋律

这篇关于Python编曲实践(八):我,乔鲁诺·乔巴那,能用两百行代码写出JOJO黄金之风里我自己的出场曲!的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/927550

相关文章

Python判断for循环最后一次的6种方法

《Python判断for循环最后一次的6种方法》在Python中,通常我们不会直接判断for循环是否正在执行最后一次迭代,因为Python的for循环是基于可迭代对象的,它不知道也不关心迭代的内部状态... 目录1.使用enuhttp://www.chinasem.cnmerate()和len()来判断for

使用Python实现高效的端口扫描器

《使用Python实现高效的端口扫描器》在网络安全领域,端口扫描是一项基本而重要的技能,通过端口扫描,可以发现目标主机上开放的服务和端口,这对于安全评估、渗透测试等有着不可忽视的作用,本文将介绍如何使... 目录1. 端口扫描的基本原理2. 使用python实现端口扫描2.1 安装必要的库2.2 编写端口扫

使用Python实现操作mongodb详解

《使用Python实现操作mongodb详解》这篇文章主要为大家详细介绍了使用Python实现操作mongodb的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、示例二、常用指令三、遇到的问题一、示例from pymongo import MongoClientf

SQL Server使用SELECT INTO实现表备份的代码示例

《SQLServer使用SELECTINTO实现表备份的代码示例》在数据库管理过程中,有时我们需要对表进行备份,以防数据丢失或修改错误,在SQLServer中,可以使用SELECTINT... 在数据库管理过程中,有时我们需要对表进行备份,以防数据丢失或修改错误。在 SQL Server 中,可以使用 SE

使用Python合并 Excel单元格指定行列或单元格范围

《使用Python合并Excel单元格指定行列或单元格范围》合并Excel单元格是Excel数据处理和表格设计中的一项常用操作,本文将介绍如何通过Python合并Excel中的指定行列或单... 目录python Excel库安装Python合并Excel 中的指定行Python合并Excel 中的指定列P

一文详解Python中数据清洗与处理的常用方法

《一文详解Python中数据清洗与处理的常用方法》在数据处理与分析过程中,缺失值、重复值、异常值等问题是常见的挑战,本文总结了多种数据清洗与处理方法,文中的示例代码简洁易懂,有需要的小伙伴可以参考下... 目录缺失值处理重复值处理异常值处理数据类型转换文本清洗数据分组统计数据分箱数据标准化在数据处理与分析过

SpringBoot项目中Maven剔除无用Jar引用的最佳实践

《SpringBoot项目中Maven剔除无用Jar引用的最佳实践》在SpringBoot项目开发中,Maven是最常用的构建工具之一,通过Maven,我们可以轻松地管理项目所需的依赖,而,... 目录1、引言2、Maven 依赖管理的基础概念2.1 什么是 Maven 依赖2.2 Maven 的依赖传递机

Python调用另一个py文件并传递参数常见的方法及其应用场景

《Python调用另一个py文件并传递参数常见的方法及其应用场景》:本文主要介绍在Python中调用另一个py文件并传递参数的几种常见方法,包括使用import语句、exec函数、subproce... 目录前言1. 使用import语句1.1 基本用法1.2 导入特定函数1.3 处理文件路径2. 使用ex

Oracle查询优化之高效实现仅查询前10条记录的方法与实践

《Oracle查询优化之高效实现仅查询前10条记录的方法与实践》:本文主要介绍Oracle查询优化之高效实现仅查询前10条记录的相关资料,包括使用ROWNUM、ROW_NUMBER()函数、FET... 目录1. 使用 ROWNUM 查询2. 使用 ROW_NUMBER() 函数3. 使用 FETCH FI

Python脚本实现自动删除C盘临时文件夹

《Python脚本实现自动删除C盘临时文件夹》在日常使用电脑的过程中,临时文件夹往往会积累大量的无用数据,占用宝贵的磁盘空间,下面我们就来看看Python如何通过脚本实现自动删除C盘临时文件夹吧... 目录一、准备工作二、python脚本编写三、脚本解析四、运行脚本五、案例演示六、注意事项七、总结在日常使用