本文针对计划或开始使用 MIDI 文件的人员。这种格式在音乐界被广泛使用,由于数据集的可用性,它引起了计算机音乐研究人员的注意。
不过,MIDI 文件可以编码不同类型的信息。特别是,MIDI 乐谱和 MIDI 演奏之间有很大区别。如果没有意识到这一点,就会在无用的任务上浪费时间,或者错误地选择训练数据和方法。
我将介绍这两种格式的基本知识,并举例说明如何在 Python 中使用它们。
什么是 MIDI?
MIDI 是合成器之间的实时通信协议。其主要思想是在 MIDI 键盘上每次按下音符(音符打开)时发送一条信息,而在音符释放(音符关闭)时发送另一条信息。这样,接收端的合成器就知道该发出什么声音了。
MIDI
如果我们收集并保存所有这些信息(确保加上它们的时间位置),我们就有了一个可以用来重现乐曲的 MIDI 文件。除了音符开和关,还有许多其他类型的信息,例如指定踏板信息或其他控制器。
你可以考虑用钢琴滚轴来绘制这些信息。
请注意,这不是 MIDI 文件,而只是其内容的一种可能的表现形式!有些软件(本例中为 Reaper)会在 pianoroll 旁边添加一个小钢琴键盘,以便更直观地解读信息。
如何创建 MIDI 文件?
MIDI 文件主要通过两种方式制作: 1)用 MIDI 乐器演奏;2)手动写入音序器(Reaper、Cubase、GarageBand、Logic)或乐谱编辑器(如 MuseScore)。
每种制作 MIDI 文件的方法都会产生不同类型的文件:
在开始之前,强调一下:我不会特别关注信息是如何编码的,而是关注可以从文件中提取哪些信息。例如,当我说 “时间用秒表示 ”时,意思是我们可以得到秒,尽管编码本身更为复杂。
MIDI 演奏
我们可以在 MIDI 演出中找到 4 种信息:
音符的起始和偏移(以及持续时间)以秒为单位,与演奏 MIDI 乐器的人按下和松开音符的秒数相对应。
音符音高用从 0(最低)到 127(最高)的整数编码;注意可表示的音符比钢琴可演奏的音符要多;钢琴的范围相当于 21-108。
音符速度也用从 0(静音)到 127(最大强度)的整数编码。
绝大多数 MIDI 演奏都是钢琴演奏,因为大多数 MIDI 乐器都是 MIDI 键盘。其他 MIDI 乐器(如 MIDI 萨克斯、MIDI 鼓和吉他的 MIDI 传感器)也存在,但并不常见。
最大的人类 MIDI 表演(古典钢琴音乐)数据集是谷歌 Magenta 的 Maestro 数据集。
MIDI 演奏的主要特性
MIDI 演奏的一个基本特征是,从来没有起音或持续时间完全相同的音符(理论上有可能,但实际上可能性极小)。
事实上,即使演奏者真的尝试,也不可能在同一时间按下两个(或更多)音符,因为人类所能获得的精确度是有限的。音符的持续时间也是如此。此外,对于大多数音乐家来说,这甚至都不是优先考虑的问题,因为时间上的偏差有助于产生更具表现力或更有节奏感的感觉。最后,连续的音符之间会有一些静音或部分重叠。
因此,MIDI 演奏有时也被称为非量化 MIDI。时间位置分布在一个连续的时间刻度上,而不是量化为离散的位置。
实际例子
让我们来看一段 MIDI 演出。我们将使用 GitHub 上的 ASAP 数据集。
在你最喜欢的终端(我在 Windows 上使用的是 PowerShell)中,转到一个方便的位置并克隆该版本库。
git clone https://github.com/fosfrancesco/asap-datasetclone https://github.com/fosfrancesco/asap-dataset
我们还将使用 Python 库 Partitura 打开 MIDI 文件,因此你可以在 Python 环境中安装该库。
pip install partitura
一切就绪后,让我们打开 MIDI 文件,打印前 10 个音符。由于这是 MIDI 演奏,我们将使用 load_midi_performance 函数。
from pathlib import Path
import partitura as pt
# set the path to the asap dataset (change it to your local path!)
asap_basepath = Path('../asap-dataset/')
# select a performance, here we use Bach Prelude BWV 848 in C#
performance_path = Path("Bach/Prelude/bwv_848/Denisova06M.mid")
print("Loading midi file: ", asap_basepath/performance_path)
# load the performance
performance = pt.load_performance_midi(asap_basepath/performance_path)
# extract the note array
note_array = performance.note_array()
# print the dtype of the note array (helpful to know how to interpret it)
print("Numpy dtype:")
print(note_array.dtype)
# print the first 10 notes in the note array
print("First 10 notes:")
print(performance.note_array()[:10])
这个 Python 程序的输出应该是这样的:
Numpy dtype:
[('onset_sec', '<f4'), ('duration_sec', '<f4'), ('onset_tick', '<i4'), ('duration_tick', '<i4'), ('pitch', '<i4'), ('velocity', '<i4'), ('track', '<i4'), ('channel', '<i4'), ('id', '<U256')]
First 10 notes:
[(1.0286459, 0.21354167, 790, 164, 49, 53, 0, 0, 'n0')
(1.03125 , 0.09765625, 792, 75, 77, 69, 0, 0, 'n1')
(1.1302084, 0.046875 , 868, 36, 73, 64, 0, 0, 'n2')
(1.21875 , 0.07942709, 936, 61, 68, 66, 0, 0, 'n3')
(1.3541666, 0.04166667, 1040, 32, 73, 34, 0, 0, 'n4')
(1.4361979, 0.0390625 , 1103, 30, 61, 62, 0, 0, 'n5')
(1.4361979, 0.04296875, 1103, 33, 77, 48, 0, 0, 'n6')
(1.5143229, 0.07421875, 1163, 57, 73, 69, 0, 0, 'n7')
(1.6380209, 0.06380209, 1258, 49, 78, 75, 0, 0, 'n8')
(1.6393229, 0.21484375, 1259, 165, 51, 54, 0, 0, 'n9')]
你可以看到,我们有以秒为单位的起始和持续时间、音高和速度。其他字段与 MIDI 演奏关系不大。
起音和持续时间也用刻度表示。这更接近 MIDI 文件对这些信息的实际编码方式:选择一个很短的时间长度(= 1 tick),所有时间信息都以这个时间长度的倍数编码。在处理音乐演奏时,通常可以忽略这些信息,直接使用以秒为单位的信息。
MIDI 乐谱
MIDI 乐谱使用更丰富的 MIDI 信息来编码时间符号、调号、小节和节拍位置等信息。
因此,它们类似于乐谱(乐谱),尽管它们仍然缺少一些重要信息,如音高拼写、连接、点、休止符、梁等。
时间信息不是以秒为单位编码,而是以更抽象的音乐单位(如四分音符)编码。
MIDI 乐谱的主要特性
MIDI 乐谱的一个基本特征是,所有音符的起始位置都与量化网格对齐,首先由小节位置定义,然后由递归整数分割(主要是 2 和 3,但也有其他分割,如 5、7、11 等......)用于小音符。
实践示例
现在我们来看看巴赫 C# 前奏曲 BWV 848 的乐谱,这就是我们之前加载的演奏乐谱。Partitura 有一个专门的 load_score_midi 函数。
from pathlib import Path
import partitura as pt
# set the path to the asap dataset (change it to your local path!)
asap_basepath = Path('../asap-dataset/')
# select a score, here we use Bach Prelude BWV 848 in C#
score_path = Path("Bach/Prelude/bwv_848/midi_score.mid")
print("Loading midi file: ", asap_basepath/score_path)
# load the score
score = pt.load_score_midi(asap_basepath/score_path)
# extract the note array
note_array = score.note_array()
# print the dtype of the note array (helpful to know how to interpret it)
print("Numpy dtype:")
print(note_array.dtype)
# print the first 10 notes in the note array
print("First 10 notes:")
print(score.note_array()[:10])
这个 Python 程序的输出应该是这样的:
Numpy dtype:
[('onset_beat', '<f4'), ('duration_beat', '<f4'), ('onset_quarter', '<f4'), ('duration_quarter', '<f4'), ('onset_div', '<i4'), ('duration_div', '<i4'), ('pitch', '<i4'), ('voice', '<i4'), ('id', '<U256'), ('divs_pq', '<i4')]
First 10 notes:
[(0. , 1.9958333 , 0. , 0.99791664, 0, 479, 49, 1, 'P01_n425', 480)
(0. , 0.49583334, 0. , 0.24791667, 0, 119, 77, 1, 'P00_n0', 480)
(0.5, 0.49583334, 0.25, 0.24791667, 120, 119, 73, 1, 'P00_n1', 480)
(1. , 0.49583334, 0.5 , 0.24791667, 240, 119, 68, 1, 'P00_n2', 480)
(1.5, 0.49583334, 0.75, 0.24791667, 360, 119, 73, 1, 'P00_n3', 480)
(2. , 0.99583334, 1. , 0.49791667, 480, 239, 61, 1, 'P01_n426', 480)
(2. , 0.49583334, 1. , 0.24791667, 480, 119, 77, 1, 'P00_n4', 480)
(2.5, 0.49583334, 1.25, 0.24791667, 600, 119, 73, 1, 'P00_n5', 480)
(3. , 1.9958333 , 1.5 , 0.99791664, 720, 479, 51, 1, 'P01_n427', 480)
(3. , 0.49583334, 1.5 , 0.24791667, 720, 119, 78, 1, 'P00_n6', 480)]
可以看到,音符的起音都正好落在一个网格上。如果我们考虑 onset_quarter(第 3 列),就会发现 16 分音符每 0.25 个四分音符出现一次,正如预期的那样。
时长的问题比较大。例如,在这个乐谱中,16 分音符的持续时间应为 0.25。然而,我们可以从 Python 输出中看到,持续时间实际上是 0.24791667。这是因为用来生成 MIDI 文件的 MuseScore 将每个音符缩短了一点。为什么?为了让 MIDI 文件的音频效果更好一些。事实的确如此,但代价是给使用这些文件进行计算机音乐研究的人带来了许多问题。类似的问题也存在于广泛使用的数据集中,如拉克 MIDI 数据集。
MIDI 乐谱与 MIDI 演奏
鉴于我们看到的 MIDI 乐谱与 MIDI 表演之间的差异,让我为你提供一些通用指南,帮助你正确设置深度学习系统。
音乐生成系统首选 MIDI 乐谱,因为量化的音符位置可以用相当小的词汇量来表示,而且还可以进行其他简化,比如只考虑单音旋律。
对于以人类演奏和感知音乐的方式为目标的系统,例如节拍跟踪系统、节奏估算器和情感识别系统(侧重于富有表现力的演奏),使用 MIDI 演奏。
将这两种数据用于乐谱跟踪(输入:演奏,输出:乐谱)和表现性演奏生成(输入:乐谱,输出:演奏)等任务。
结论
MIDI 文件非常出色,因为它们明确提供了每个音符的音高、起音和持续时间等信息。这就意味着,与音频文件相比,针对 MIDI 数据的模型可以更小,并使用更小的数据集进行训练。