背景

笔者什么乐器也不会,乐理知识也只有中小学音乐课学的一点点。不过借助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 背景音乐程序代码_用Python演奏音乐相关推荐

  1. python 背景音乐程序代码_【Python开源】抖音热门BGM爬虫下载~~~~

    [Python] 纯文本查看 复制代码#!/usr/bin/env python # -*- coding: utf-8 -*- # Time : 2018/7/22 18:04 # Author : ...

  2. python 运行程序代码_一些python程序

    <从问题到程序:用Python学编程和计算>--1.2 Python语言简介 本节书摘来自华章计算机<从问题到程序:用Python学编程和计算>一书中的第1章,第1.2节,作者 ...

  3. python重启程序代码_重启python程序

    跑程序跑到后面就越跑越慢了,就学习了一下重启程序的命令. 这是远程服务器跑的程序,亲测有用. import os import sys def restart_program(): print(&qu ...

  4. python画图程序代码_少儿python编程(7)海龟画图(拓展1)

    我们继续用Python的海龟库来画图吧! 上图是画一朵花的程序,重点是6-12行,使用了函数来定义drawleaf:每一掰叶子由两条弧线组成,每一条弧线重复画15次,每次前进5步,右转6度. 看图形化 ...

  5. 用python开发程序代码_用Python开发一款王者荣耀的“脚本”!上王者轻轻松!

    王者荣耀 -很火的手游-简直老少通吃-令人发指-虽然操作简单-但为什么你还是会被虐, 其实 是有技巧的--本文Python大神带你研究王者荣耀各类英雄的出装小技巧,让你成为大神般的存在 前期准备 环境 ...

  6. 用python写名字代码_用python编写一个批量修改文件名的小程序

    1.问题描述: 原有的视频文件按序排列,但是文件名没有对内容的说明,如下图所示: 原视频文件列表.jpg 想将其批量修改成如下文件名: 图2:要改成的文件名.jpg 最终想要的效果: ok.jpg 不 ...

  7. python好玩的代码_一行 Python 能实现什么丧心病狂的功能?

    能够把自身代码打印出来的程序,叫做Quine.下面是python的一行quine: ​有人说有分号不算一行,无分号版: 其实,如果你用程序语言的名字+quine作为关键字去搜索,你能找到各种语言实现的 ...

  8. python应用程序发布_关于Python的应用发布技术

    收集如何 将Py应用打包发布的各种技巧: 1.1. 工具 工欲善其事,必先利其器.python是解释型的语言,但是在windows下如果要执行程序的话还得加个python shell的话,未免也太麻烦 ...

  9. python设计选择题代码_《Python程序设计》试题库

    WORD 完美格式 < Python 程序设计>题库 一.填空题 第一章 基础知识 1 . Python 安装扩展库常用的是 _______ 工具.( pip ) 2 . Python 标 ...

最新文章

  1. zabbix_get 无法获取值(解决思路)
  2. 2009年8月26日,用于win2003上的MSN不能正常使用
  3. 分享Kali Linux 2017年第二周镜像文件
  4. dijkstra邻接表_掌握算法-图论-最短路径算法-Dijkstra算法
  5. VTK:量化多数据点用法实战
  6. Oracle 10g客户端的安装和配置
  7. 惠普照片打印软件_被看错的打印机?原来打印机还可以这么玩
  8. Zabbix3.2.6之通过JMX监控Tomcat
  9. windows守护进程_在Linux的Windows子系统上(WSL)使用Docker(Ubuntu)
  10. OpenCV 实现颜色直方图
  11. zabbix的Discovery功能
  12. 二维光子晶体带隙仿真Matlab完全程序_平面波展开法
  13. 有道词典java下载电脑版下载手机版下载安装_【有道词典官方下载】有道词典PC版下载_多特软件站...
  14. Android 安卓益智休闲源码
  15. Unity3D 鼠标点击切换图片
  16. 一次提交却发起了多次请求的一种可能的原因
  17. 计算机常用键的作用,键盘功能键大全2017 电脑键盘常用按键功能详解
  18. Sqlserver中的日期类型值不能小于1753年
  19. 详解ShellShock 漏洞复现原理,内附ShellShock的修复方法
  20. golang办公工作流workflow js-ojus/flow包介绍——系列一

热门文章

  1. 简述代理服务器及网络服务端口分类
  2. 便携式30W太阳能发电板光伏板设计移动电源充电器
  3. Jeston Xavier NX刷机笔记
  4. Python-Numpy详解
  5. java URL 转码和解码
  6. 平面设计PS基础入门图层教程
  7. 如何使用SpringBoot写一个属于自己的Starter
  8. 记录实际邓白氏申请(可加急)
  9. DOS-通过shutdown指令实现关机和重启
  10. Debezium报错处理系列之二十九:Make sure that an instance of SQL Server is running on the host and accepting TCP