发起:唐里 校对:敬爱的勇哥 审核:唐里
参与翻译(1)人:路双宁
原文:Visualizing Musical Performance

作为一个音乐家兼数据科学家,我对音乐表演可视化的想法很感兴趣。在这篇文章中,我简述了如何使用MAESTRO数据集对钢琴演奏的录音可视化。本文提供了一些例子。下面,我使用Python的Mido,Seaborn和Pandas包,用代码一步一步地说明,打开、清理和可视化MAESTRO数据集中的钢琴表演。文章最后以弗朗兹·李斯特《匈牙利狂想曲第2号》的可视化作结,并录下了这首曲子,让读者可以通过可视化来观看这首曲子的展开。还提供了用于创建可视化的完整Python脚本的链接。

《巴赫D小调前奏曲与赋格》散点图

数据

MAESTRO数据集包含超过200小时的国际钢琴电子竞赛的钢琴表演。参赛者使用雅马哈自动演奏钢琴演奏,这种原声钢琴还可以捕捉和回放乐器数字接口(MIDI)数据。MAESTRO数据集包含参赛者演奏的MIDI数据和演奏的录音。

MIDI是一种允许数码乐器通过“消息”相互通信的协议。这些消息存储着用于回放的乐器类型、要播放的音符、音符何时开始、何时结束等信息。

简介:在Python中打开MIDI文件

在Python中打开一个MIDI文件,要安装mido包。从mido包里引入MidiFile。下面的代码是一个在Python中用MidiFile打开一个文件的例子。对于MAESTRO数据,这个过程会创建一个包含两个轨道的MidiFile对象。第一个轨道包含演奏的元数据(例如时间、作品签名)。第二个轨道包含演奏的信息。

1
2
3
4
5
6
from mido import MidiFile
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

mid = MidiFile(file_name)

《莫扎特D大调奏鸣曲》散点图

阐述:处理数据

在本文中,我使用以下步骤来处理数据以创建可视化。

步骤1:提取第二个轨道,通过对轨道进行遍历来提取数据。其结果应该是一个消息对象的list。遍历的时候,忽略第一个和最后一个消息。第一个消息是程序消息,最后一个是元消息,表示轨道的结束。这两条消息都没有提供所播放的音符的数据。

1
2
3
4
message_list = []

for i in mid.tracks[1][1:-1]:
message_list.append(i)

步骤2:遍历消息的list,将消息转换成字符串。可以用python的str()函数或者mido包提供的format_as_string()方法完成到字符换的转换。

1
2
3
4
message_strings = []

for x in message_list:
message_strings.append(str(x))

步骤3:使用split(‘’)把消息字符串分割成包含键和值的子串

1
2
3
4
5
message_strings_split = []

for message in message_strings:
split_str = message.split(" ")
message_strings_split.append(split_str)

这将从列表中创造一个列表,单个列表如下所示:

1
['control_change', 'channel=0', 'control=64', 'value=43', 'time=0']

列表的第一个子字符串只包含一个值(消息类型)。为了将数据转换为字典,必须删除这个子字符串。

步骤4:删除第一个子字符串,将其存储在列表中,并将列表转换为dataframe。

1
2
3
4
5
6
7
message_type = []

for item in message_strings_split:
message_type.append(item[0])

df1 = pd.DataFrame(message_type)
df1.columns = ['message_type']

步骤5:把列表中的其它子串转换成字典,再转换成dataframe。=号将子串的键和值分隔开。下面的代码遍历列表和每个子字符串,以创建键值对。然后,它将这些键值对存储在字典中(每个子字符串列表对应一个字典)。最后,它将字典转换为一个dataframe。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
attributes = []

for item in message_strings_split:
attributes.append(item[1:])

attributes_dict = [{}]

for item in attributes:
for i in item:
key, val = i.split("=")
if key in attributes_dict[-1]:
attributes_dict.append({})
attributes_dict[-1][key] = val

df2 = pd.DataFrame.from_dict(attributes_dict)

步骤6:将属性(df2)的dataframe与包含消息类型(df1)的dataframe连接起来。

1
df_complete = pd.concat([df1, df2], axis=1)

步骤7:dataframe中的time变量表示自上次消息以来经过的时间。note变量表示弹奏的钢琴键。为了绘制数据,需要测量经过的时间来绘制数据随时间的变化。可以使用Python的.cumsum()方法创建time_elapsed变量。绘制时需要将变量从string类型转换为float类型。

1
2
3
4
5
6
7
8
# 直接将Nan转化成int会报错,因此先将Nan填充为0
df_complete.note = df_complete.note.fillna(0)
#Transform the time and note attributes from strings to floats
df_complete.time = df_complete.time.astype(float)
df_complete.note = df_complete.note.astype(int)

#Engineer a time elapsed attribute equal to the cumulative sum of time.
df_complete['time_elapsed'] = df_complete.time.cumsum()

步骤8:过滤掉控制消息和note_off消息,因为这些消息对可视化来说不会被用到。控制消息表示何时按下和释放延音踏板与弱音踏板。控制消息的消息类型等于“control”。note_off消息由速度为0的note_on类型的消息表示。

1
2
df_filtered = df_complete[df_complete['message_type']=='note_on']    
df_filtered = df_filtered.loc[df_filtered['velocity'] != '0']

步骤9:删除dataframe中不必要的列

1
2
3
4
5
df_filtered.drop(['channel', 'value', 'control', 'time'], axis=1, inplace=True)    
try:
df_filtered.drop('program', axis=1, inplace=True)
except:
pass

步骤10:处理过程的最后一步是分别在dataframe开始和结尾添加一行。添加这些行将在可视化的边缘和数据点之间提供一些空间。新的第一行被分配了音符值0(钢琴的较低范围之外的值),并且经过的时间值等于最大经过时间值的-0.05倍。(第一个note_on消息的time_elapsed值大于0)。新的最后一行被分配了一个音符值127(钢琴上限范围之外的值),经过的时间值等于最大经过时间值的1.5倍。添加这些行可以使生成的可视化具有平滑的边缘。

1
2
3
4
5
6
7
8
9
10
11
add_first_row = []    
add_first_row.insert(0, {'message_type': 'note_on', 'note': 0, 'time': 0,
'velocity': 0, 'time_elapsed':-df_filtered.iloc[-1]['time_elapsed']*0.05})
df_final = pd.concat([pd.DataFrame(add_first_row), df_filtered], ignore_index=True)

last_time_elapsed = df_final.iloc[-1]['time_elapsed']*1.05

add_last_row = []
add_last_row.insert(0, {'message_type': 'note_on', 'note': 127, 'time': 0,
'velocity': 0, 'time_elapsed':last_time_elapsed})
df_final = pd.concat([df_final, pd.DataFrame(add_last_row)], ignore_index=True)

爱德华·格里格小调抒情作品《华尔兹》作品12第2部分的散点图

更进一步:可视化

为了将演奏可视化,我用了Python的Seaborn包。x轴表示经过的时间(时间从左往右递增),y轴表示弹奏的音符的音高,从低(下)到高(上)。图中显示了使用六边形时,随时间的推移音符的散点图。颜色用来表示六边形中包含的音高范围和一个时间段内的音高频率。在上面的图中,较高的频率用较暗的绿色表示。

散点图显示了六边形所定义的范围内音高的频率,以及这些频率如何随着演奏的进行而变化。其结果是一个可以随着时间(x轴)可视化音高(y轴)和音高频率(颜色)的图。该图包含了所有必要的信息,是所弹奏片段的可视化(参见下面的匈牙利狂想曲示例中的演示)。

下面的步骤详细说明了本文中的可视化是如何绘制的。

步骤1:将Seaborn的样式设置为“white”。这一步提供了一个干净的白色背景,在此基础上构建可视化。

1
sns.set_style('white')

步骤2:定义图中x轴和y轴的范围。在第10步处理过程中添加的第一行和最后一行有助于可视化。x轴的范围由经过的最小时间和最大时间来决定。这两个的值都是由额外的行设置的。y轴的范围设置在16和113之间。这样做是为了使图产生平滑的边缘。MIDI音符值(在y轴上绘制)在0到127之间。然而,钢琴只有88个键,它们的MIDI音符值在21到108之间。第一行和最后一行的音符值设置在钢琴的可能值范围之外。因此,通过在第一行和最后一行设置的最小和最大音符值设置y轴范围,可以得到:

  1. 显示钢琴所有可能的值
  2. 不显示第一行和最后一行的人工音符值
  3. 在每个可视化的边缘和最低和最高音符之间有一个空间,并且
  4. 该空间的颜色与画布的背景相同

关于绘图函数的另外一个注意事项:网格大小表示x方向上六边形的数量。更改网格大小会更改六边形区域的大小,以及随后的图形中六边形的大小。下图绘制的是里姆斯基-科萨柯夫的《野蜂飞舞》。

1
2
3
4
5
6
7
g = sns.jointplot(df_final.time_elapsed, df_final.note, cmap=palette,
kind='hex', xlim=(min(df_final.time_elapsed),max(df_final.time_elapsed)),
ylim=(16,113),
joint_kws=dict(gridsize=88)
)
g.fig.set_figwidth(30)
g.fig.set_figheight(15)

里姆斯基-科萨柯夫《野蜂飞舞》的散点图(带标签和边缘)

步骤3:删除图中不需要的元素。为了提供一个艺术性的散点图,我删除了图的边缘、轴线、轴标签和坐标轴(注意:删除这些元素不利于提供一个有意义的可视化)。下面的代码删除了轴线、图边缘、轴标签和刻度标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sns.set_style('white')
g = sns.jointplot(df_final.time_elapsed, df_final.note, cmap=palette,
kind='hex', xlim=(min(df_final.time_elapsed),max(df_final.time_elapsed)),
ylim=(16,113),
joint_kws=dict(gridsize=88)
)
g.fig.set_figwidth(30)
g.fig.set_figheight(15)

sns.despine(left=True, bottom=True) #Remove x and y axes
plt.setp(g.ax_marg_x, visible=False) #Remove marginal plot of x
plt.setp(g.ax_marg_y, visible=False) #Remove marginal plot of y
g.set_axis_labels('', '') #Remove axis labels
plt.setp(g.ax_joint.get_xticklabels(), visible=False) #Remove x-axis ticks
plt.setp(g.ax_joint.get_yticklabels(), visible=False) #Remove y-axis ticks

里姆斯基-科萨柯夫《野蜂飞舞》的散点图

概述:

本文介绍了从MAESTRO数据集中打开、清理和可视化MIDI演奏数据的步骤。提供了代码片段来展示这些步骤。利用Seaborn的散点图的方法对演奏的数据进行可视化。这些图显示了音高的密度(或所弹奏音符的频率)随时间的变化。其结果是一种艺术的描绘,把一段音乐捕获在一个单一的图像中。

对于那些有兴趣了解可视化如何与音乐作品的表现相一致的读者,下面是一个例子。这是弗朗兹·李斯特《匈牙利狂想曲第2号》的可视化图。在可视化之后,YouTube上有一段Adam Gyorgy演奏该作品的视频。对于有兴趣创建自己的可视化的读者,可以在GitHub上找到我的脚本和文档。

阿尔班·伯格《奏鸣曲》的散点图

结尾:匈牙利狂想曲第2号

下图是弗朗兹·李斯特《匈牙利狂想曲第2号》的可视化图以及Adam Gyorgy表演这首曲子的视频。你可以一边听视频,一边通过可视化图感受他的演奏。

视频地址:https://youtu.be/7H99FM6S8rU

弗朗兹·李斯特《匈牙利狂想曲第2号》的散点图