背景

笔者什么乐器也不会,乐理知识也只有中小学音乐课学的一点点。不过借助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:\Apps\fluidsynth-x64\GeneralUserSoftSynth\GeneralUserSoftSynth.sf2",则我们在程序中调用就用

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

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

分析

乐谱格式

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

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

  • 一是1234567那些数字,注意我用键盘数字上方的特殊符号表示这个音升了半音;
  • 二是这些数字所在的八度,即这些数字头顶和脚下有没有点,通常没有点的是第四个八度,头顶有点的是第五个八度,脚下有点的是第三个八度;
  • 三是某个音的时值,即它占几拍,注意为了声音的连贯,延音线相连的两个音符如果音高相等,我就把它们的时值加起来了,同时为了计算方便,我定义⅛拍为0,¼拍为1,½拍为2,一拍为4,以此类推,如果大于9则用a、b、c这些代替;
  • 四是各乐句的先后顺序,我们可以把每条乐句的音符描述出来,然后用一个序列记录依次出现的乐句的序号;
  • 五是乐句的首调,即左上角的1=D,因为有些歌中间会突然升降调,所以我们必须建一个序列存储依次出现的乐句的调性,出于简单考虑,直接记录首调和C调差多少个半音就行了,比如D调和C调相差2(中间隔个C#),就记录2即可。

因此我们的程序只要有这五个数据就可以弹奏出整首乐曲了。比方说这首歌前奏的前四个小节,第一部分就可以表示为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)
  • track在这个库中表示音轨,bar表示的应该是小节,但是我偷懒了,把bar直接存储乐句了。在track的开头,我添加了一个2拍的休止符,因为这个库不知道是bug还是什么,如果track开头没有休止符,则乐曲的第一个音会被吞掉。

  • bar的构造函数有两个参数,前者随便填无影响,可能只是元信息,后者比较重要,它描述了这个小节的时长,如果小节里放的音符总时长超过了这个小节的时长最后一个音符会被扔掉,所以一定要计算好。这个时长用分数表示,但它的计算方式很奇怪,(4, 4)表示2拍,以此类推(8, 4)表示4拍,和正常情况完全不一样。之后对于每个乐句,我首先把时长转化成数,然后计算乐句的时长,因为我的乐谱8为2拍,所以要除8再乘4。

  • 接着就该填什么音就填什么音。但要注意两点,一是库里1234567分别用CDEFGAB代替;二是库中对时值的描述和我们的描述是倒数关系,它是8为¼拍,4为½拍,2为1拍,以此类推,所以在传入place_notes和place_rest我们的时值要用8除。

  • note.transpose用来转调,它能够把音符提升一定的半音数。

弹奏音乐

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

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音频如下:

完整程序

import json
import threading
from mingus.containers import *
from mingus.midi import fluidsynth
import mingus.core.chords as chords
from mingus.midi.midi_track import MidiTrack
from mingus.midi.midi_file_out import MidiFile 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)
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) fluidsynth.init(r'D:\Apps\fluidsynth-x64\GeneralUserSoftSynth\GeneralUserSoftSynth.sf2')
fluidsynth.set_instrument(1, 11)
fluidsynth.set_instrument(2, 115)
fluidsynth.set_instrument(3, 100)
fluidsynth.main_volume(2, 50)
fluidsynth.main_volume(3, 40)
thread1 = threading.Thread(target=lambda : fluidsynth.play_Track(t2, channel=2, bpm=150))
thread2 = threading.Thread(target=lambda : fluidsynth.play_Track(t, channel=3, bpm=150))
thread1.start()
thread2.start()
fluidsynth.play_Track(t, channel=1, 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)

用Python演奏音乐的更多相关文章

  1. python实现音乐播放器

    python实现音乐播放器 模块:pygame 模块:time Python 布尔循环实例: import time import pygame muxi_k = """ ...

  2. Arduino入门笔记(4):用蜂鸣器演奏音乐并配有LED闪烁

    转载请注明:@小五义 http://www.cnblogs.com/xiaowuyi 欢迎加入讨论群 64770604 一.本次实验所需器材 1.Arduino板 https://item.taoba ...

  3. python播放音乐

    最近一直想实现使用Python播放音乐的功能,找了百度上的好多博客,要不就只能播放wav格式的,要不播放mp3格式的但无法在Linux系统下使用的,或者只能在Python2的情况下播放的,写的都不符合 ...

  4. Python 的音乐库

    前言 其实处理这个用 Matlab 最方便,之前把 guitar-synthesizer 从 Matlab 移植到 Python,过程中更是体会到了这一点. 不过 Matlab 安装包又大,启动又慢, ...

  5. python 简易音乐盒子

    #!/usr/bin/env python#-*- coding:utf-8 -*- from Tkinter import *import tkMessageBoximport urllib def ...

  6. Python实现音乐的剪辑

    一.读取音频文件 from scipy.io import wavfile import numpy as np like = wavfile.read('./嘤嘤嘤.wav') print (lik ...

  7. 使用python播放音乐

    1.首先安装pygame,pip install pygame 2.上代码: import time import pygame #音乐路径 filepath=r"C:\Users\1473 ...

  8. python 喜马拉雅 音乐下载 演示代码

    1.主程序文件 import os import json import requests from contextlib import closing from progressbar import ...

  9. Scratch少儿编程系列:(八)演奏简单音乐

    一.程序说明 本程序,用来演奏简单音乐. 二.制作过程 1. 场景和角色的选择 场景选择“音乐和舞蹈”主题下的“party root”,角色沿用默认角色,如下图: 选择后效果如下图: 2. 切换到“脚 ...

随机推荐

  1. @Results用法总结

    MyBatis中使用@Results注解来映射查询结果集到实体类属性. (1)@Results的基本用法.当数据库字段名与实体类对应的属性名不一致时,可以使用@Results映射来将其对应起来.col ...

  2. 在群晖NAS上运行URLOS之后竟然能安装Discuz! Q!!

    如果我们手头上有1台群晖NAS时,有没有考虑过把群晖NAS当成服务器来使用,这样会不会很有意思呢? 现在,我们终于可以尝试一番了,把群晖NAS变成一台实实在在的服务器,在上面跑各种运行环境!其实很简单 ...

  3. 06.DRF-第一个demo

    一.环境安装与配置 DRF需要以下依赖: Python (2.7, 3.2, 3.3, 3.4, 3.5, 3.6) Django (1.10, 1.11, 2.0) DRF是以Django扩展应用的 ...

  4. awk 命令使用入门

    Linux 下处理和分析文本文件内容,AWK 命令是一个强有力的工具.特别是文件内容是以行和列的形式排版的时候,AWK 就是命令行界的 Excel 啊! 简单的 awk 命令可以直接在命令行中使用,复 ...

  5. vue环境配置脚手架搭建,生命周期,钩子

    Vue项目环境搭建 """ node ~~ python:node是用c++编写用来运行js代码的 npm(cnpm) ~~ pip:npm是一个终端应用商城,可以换国内 ...

  6. 【转载】张一鸣:为什么 BAT 挖不走我们的人才?

    很多企业失败的时候,总说是政策的变化.市场的变化.消费者需求的变化.技术发展的变化等等,导致了失败,其实这都是瞎说,归根结底还是人的原因,都是因为你招的人不行.可见,人才对于一个企业的重要性. 互联网 ...

  7. Dubbo想要个网关怎么办?试试整合Spring Cloud Gateway

    一.背景 在微服务架构中 API网关 非常重要,网关作为全局流量入口并不单单是一个反向路由,更多的是把各个边缘服务(Web层)的各种共性需求抽取出来放在一个公共的"服务"(网关)中 ...

  8. Python自学——pygame安装

    本片文章介绍pygame的安装方法.一则跟广大初学者分享经历,二则做个自我总结. pygame是python的库文件,跟一般的应用软件安装方法不太一样.我电脑上的python版本是python3.7, ...

  9. hive sql 解析json

    在hive中会有很多数据是用json格式来存储的,而我们用数据的时候又必须要将json格式的数据解析成为正常的数据,今天我们就来聊聊hive中是如何解析json数据的. 下面这张表就是json格式的表 ...

  10. BZOJ3242 快餐店

    原题传送门 题意 给定一个n条边n个点的连通图,求该图的某一点在该图距离最远的点距离它的距离的最小值. 题解 显然,答案是\(\frac {原图直径}{2}\). 本体的图有 \(n\) 个点 \(n ...