用Python演奏音乐


[TOC]

背景

笔者什么乐器也不会,乐理知识也只有中小学音乐课学的一点点。不过借助Python,调用编曲家常用的MIDI程序库,也能弹奏出一些简单的音乐,以下是笔者的一些心得。

准备

安装mingus

首先是安装Python库,我选择的是mingus,它的优点是教程写的很详细,而且和实际的乐理,像调性、节拍这些结合的较好,而不是像同类库通过发送“按下按键”、“释放按键”这些指令来播放声音,另一方面它可以在运行的时候播放制作出的音乐,不用先导出MIDI文件再渲染音频。这个库安装很简单,直接

pip install mingus

即可。

下载并配置fluidsynth

mingus这个库只是提供了调用的接口,接下来需要安装实际处理MIDI格式的程序fluidsynth。首先在github下载对应的版本,下载后解压,在文件夹中找到libfluidsynth-2.dll,把这个文件夹添加到环境变量path。然后……比较坑的一点来了,我们下载的这个库是libfluidsynth-2,但是mingus只认libfluidsynth和libfluidsynth-1,所以需要把mingus的代码改一下,找到mingus所在文件夹(通常是Python安装文件夹/Lib/site-packages/mingus),打开/midi/pyfluidsynth.py,将里面第35行起

lib = (
    find_library("fluidsynth")
    or find_library("libfluidsynth")
    or find_library("libfluidsynth-1")
)

改成

lib = (
    find_library("fluidsynth")
    or find_library("libfluidsynth")
    or find_library("libfluidsynth-1")
    or find_library("libfluidsynth-2")
)

之后运行python,尝试

from mingus.midi import fluidsynth

没有报错则此步完成。

下载soundfont文件

soundfont文件一般用来存储乐器的声音。网上很多资源因为年代久远都凉了,找了很久才找到一个。下载以后解压,然后把文件夹的名字和文件夹里所有文件的名字里的空格和除扩展名之外的点全部去掉,之后找到后缀名为sf2的文件,这个就是我们要找的,假设它的路径为“D:-x64.sf2”,则我们在程序中调用就用

fluidsynth.init(r'D:\Apps\fluidsynth-x64\GeneralUserSoftSynth\GeneralUserSoftSynth.sf2')

即可。注意那个r,有它字符串里的反斜杠就不用转义了。这句话没有报错则此步完成。

分析

乐谱格式

以郭静的《每一天都不同》为例,简谱是这样的(来自简谱网):

《每一天都不同》简谱

我们可以看到乐谱基本上可以用五部分描述:

因此我们的程序只要有这五个数据就可以弹奏出整首乐曲了。比方说这首歌前奏的前四个小节,第一部分就可以表示为12317716,第二部分就可以表示为44443454,第三部分就可以表示为3111244g。

我将整个乐谱用json文件改写如下:

{
    "音符": [
        "12317716",
        "031200123316012155152523000123152067137017606711233200",
        "031200123316012155152523000123152023277105671234352110554",
        "3054325103453160565224330665355332552201236",
        "5433431212345617156^143211177",
        "505112523210231234327125077125231067167176101122343455554",
        "54334312554",
        "50511252321"
    ],
    "音高": [
        "44443454",
        "444444444443444433434344444444444433443443343344444444",
        "444444444443444433434344444444444444433443334444434444444",
        "4434444444444444444444444444444444444444444",
        "44444444444444545444544444433",
        "444444444444444444443444433443444433433433444444444444444",
        "44444444444",
        "44444444444"
    ],
    "节拍": [
        "3111244g",
        "421542112114211112262118442112226211224211421121122844",
        "421542111214211112262118442112226211112422311211312184211",
        "4111142a211222621111211a1111211222112221122",
        "8314222i22222211a222a22224444",
        "4211833211a211211222112211112221121121121142112111111c211",
        "8314222e211",
        "4211833211e"
    ],
    "组成": [0, 0, 1, 2, 3, 4, 2, 3, 5, 3, 6, 3, 7],
    "调性": "2222222222222"
}

乐谱解析

这样我们就可以在程序中解析它了。解析的代码如下:

def tran(x):
    if x >= 'a':
        return ord(x) - 87
    elif x == '0':
        return 0.5
    else:
        return float(x)

f = open('每一天都不同.json', 'rb')
data = json.loads(f.read(), encoding='utf8')
f.close()

n = data['音符']
h = data['音高']
r = data['节拍']
l = data['组成']
k = data['调性']
t = Track()
b = Bar('C', (4, 4))
b.place_rest(1)
t.add_bar(b)
name = 'CDEFGAB'
symbol = '!@#$%^&'
for i in range(len(l)):
    rn = list(map(tran, r[l[i]]))
    b = Bar('C', (4 * sum(rn) / 8, 4))
    for j in range(len(n[l[i]])):
        if n[l[i]][j] == '0':
            b.place_rest(8 / rn[j])
        else:
            x = symbol.find(n[l[i]][j])
            if x == -1:
                x = int(n[l[i]][j]) - 1
                y = name[x]
            else:
                y = name[x] + '#'
                print(y)
            note = Note(y, int(h[l[i]][j]))
            note.transpose(k[i])
            b.place_notes(note, 8 / rn[j])
    t.add_bar(b)

弹奏音乐

然后我们就可以听听弹奏出来的音乐了,播放的代码如下:

fluidsynth.init(r'D:\Apps\fluidsynth-x64\GeneralUserSoftSynth\GeneralUserSoftSynth.sf2')
fluidsynth.set_instrument(1, 11)
fluidsynth.play_Track(t, channel=1, bpm=150)

set_instrument方法可以用来改变某个频道使用的乐器,比如上面的代码把第一个频道的乐器改成编号为11的乐器,如果不执行这段代码则默认使用第一个乐器即钢琴。各编号对应的乐器可以在这里查看。play_Track方法第一个参数是要播放的track,第二个是在哪个频道播放,第三个是播放的速度,默认是120,个人感觉调到150速度比较合适。

添加伴奏

音乐是听到了,但是有点单调,我们希望加入鼓点、合奏之类的。不过我怀疑这个库的编写者没有对这个库进行完善的测试,所以原来用于播放多个track的方法play_Tracks有bug。笔者使用了多线程的方式来同时播放,但是库中还有一个无法调节播放使用的channel的bug,我已向项目提了pull request,截至本文撰写的时候,项目维护者还没有回应,所以在这里给出修改方法:打开库所在文件夹/containers/note.py,将第47行起

    channel = 1 
    velocity = 64

这两行删掉。

然后,我们给歌曲添上鼓点。为了方便,我就设置半拍敲一下,每两拍为一个周期,按照强,弱,次强,弱来,当乐器被设置成鼓的时候,声音越高,鼓点越弱,声音越低,鼓点越强,所以我们可以写出这样的代码:

t2 = Track()
b = Bar('C', (4, 4))
b.place_rest(1)
t2.add_bar(b)
for i in range(int(sum(map(sum, map(lambda x: map(tran, r[x]), l)))) // 8):
    b = Bar('C', (4, 4))
    b.place_notes('C-3', 4)
    b.place_notes('C-7', 4)
    b.place_notes('C-5', 4)
    b.place_notes('C-7', 4)
    t2.add_bar(b)

i的范围是通过对每个乐句的时值求和得到的。接下来是播放,为了让声音更好听,我除了歌曲track、鼓点track再加上一个用另一种乐器演奏的歌曲track,播放的代码如下:

fluidsynth.init(r'D:\Apps\fluidsynth-x64\GeneralUserSoftSynth\GeneralUserSoftSynth.sf2')
fluidsynth.set_instrument(0, 11)
fluidsynth.set_instrument(1, 115)
fluidsynth.set_instrument(2, 100)
fluidsynth.main_volume(1, 50)
fluidsynth.main_volume(2, 40)
thread1 = threading.Thread(target=lambda : fluidsynth.play_Track(t2, channel=1, bpm=150))
thread2 = threading.Thread(target=lambda : fluidsynth.play_Track(t, channel=2, bpm=150))
thread1.start()
thread2.start()
fluidsynth.play_Track(t, channel=0, bpm=150)

保存音乐

得到音乐以后,我们希望将它保存下来,保存代码如下:

m = MidiFile()
mt = MidiTrack(150)
mt2 = MidiTrack(150)
mt3 = MidiTrack(150)
m.tracks = [mt, mt2, mt3]
mt.set_instrument(1, 11)
mt.play_Track(t)
for _, _, i in t2.get_notes():
    if i is not None:
        i[0].set_channel(2)
mt2.set_instrument(2, 115)
mt2.play_Track(t2)
for _, _, i in t.get_notes():
    if i is not None:
        i[0].set_channel(3)
mt3.set_instrument(3, 100)
mt3.track_data += mt3.controller_event(3, 7, 30)
mt3.play_Track(t)
m.write_file('D:/test.midi', False)

首先建立MidiFile对象表示一个Midi文件,然后创建3个速度为150的Midi音轨,之后分别是设置乐器和播放频道,坑的是这个库里MidiTrack.play_Track方法无法传入播放频道,所以需要手动设置track里所有的note的频道,mt3.controller_event(3, 7, 30)这个方法是为了设置第三个midi音轨的音量,3表示频道,7表示修改音量这个事件的编号,30是音量,注意是controller_event不是midi_event,我被这个坑了好久,直到看了CMU的MIDI教程,才幡然醒悟,这个库的基础设施还是太差了,如果不是它的对象结构和实时播放,真的一无是处。

得到midi文件,我们就可以将其渲染成wav文件了,直接用上之前下载的fluidsynth程序,执行

fluidsynth -F output.wav D:/Apps/fluidsynth-x64/GeneralUserSoftSynth/GeneralUserSoftSynth.sf2 D:/test.midi

得到的output.wav就是我们要的音频文件。我用ffmpeg转码后得到的mp3音频如下: