#Pythonでシンセサイザーを作って演奏する。
##Python実行環境
AWS cloud9 EC2 Ubuntu t2.small
Python 3.6.9
windws10
Visual Studio 2019
Python 3.7
##ソースコード
###MakeMusic.py
import numpy as np
from scipy.io import wavfile
import sys
import re
import os
amp = 0.5 #音量
A = 440.0 #A=440Hz
rate = 44100 #サンプリングレート
bpm = 120 #BPM
scoreWave = None
fileName = ""
vibratoDepth = 1
vibratoRate = 1
isDone = False
#音の周波数を計算してsin波を生成する関数
def sinWave_from_key(k, o, v, s, m):
"""
parameters
----------
k : string
key in scale
o : int
octave
v : float
volume
s : int
seconds
t : string
type
Returns
----------
wave : ndarray
"""
t = np.arange(0, s, 1 / rate) #時間パラメータ
kArray = ["C", "Cs", "D", "Ds", "E", "F", "Fs", "G", "Gs", "A", "As", "B"]
wave = 0 * t
if k in kArray:
kNum = kArray.index(k)
kFreq = A * (2 ** ((kNum - kArray.index("A")) / 12)) * (2 ** (o - 3))
loudness = (amp * v)
param = 2 * np.pi * kFreq * t
if "vibrato" in m:
param += vibratoDepth * np.sin(2 * np.pi * vibratoRate * t)
if "fadein" in m:
loudness *= t / s
elif "fadeout" in m:
loudness *= (s - t) / s
wave = loudness * np.sin(param)
return wave
#和音や長さからsin波を生成する
def note(k, v, l, t):
"""
parameters
----------
k : string array
key and scale [example-"C4, G5, ..."]
v : int
volume
l : string
fraction
t : string
noteType
-default : normal sin wave
-vibrato(~) : sin wave with vibrato
-fadein(<) : fadein
-fadeout(>) : fadeout
"""
numeratorBuff = l.split("/")[0]
denominatorBuff = l.split("/")[1]
numerator = int(numeratorBuff)
denominator = int(denominatorBuff)
fraction = numerator / denominator
s = 60 / bpm * fraction * 4 #1/4音符4つで1小節
wave = None
vol = []
for vElement in v:
vol.append(int(re.search("\d+", vElement).group()) / 100)
i = 0
for key in k:
keyName = re.search("[A-Za-z]*", key).group()
keyOctave = int(re.search("\d", key).group())
if wave is None:
wave = sinWave_from_key(keyName, keyOctave, vol[i], s, t[i])
else:
wave += sinWave_from_key(keyName, keyOctave, vol[i], s, t[i])
i += 1
global scoreWave
if scoreWave is None:
scoreWave = wave
else:
scoreWave = np.append(scoreWave, wave)
#保存
def saveWav():
name = ""
for s in fileName:
if s is ".":
break
name += s
if not fileName is "":
wavfile.write(name + ".wav", rate, scoreWave)
else:
wavfile.write("untitled.wav", rate, scoreWave)
#標準のisalnum等では不十分だったため半角英数のみのものを作った
def checkAlnum(s):
"""
parameters
----------
s : string
Returns
----------
result : bool
"""
alnum = re.compile(r'^[a-zA-Z0-9]+$')
result = alnum.match(s) is not None
return result
#txt形式のスコアを読み込む
def readScore():
if not fileName.endswith(".txt"):
print("!ERROR!---Please check the file extension.")
return None
if not os.path.exists(fileName):
print("Oops. Maybe wrong file name")
return None
with open(fileName) as f:
global amp
lines = f.readlines()
for line in lines:
if line.startswith("#"):
#コメントアウト用
continue
elif line.startswith("<") and (line.endswith(">") or line.endswith(">\n")):
#BPM設定タグ用
global bpm
bpm = int(re.search("\d+", line).group())
elif line.startswith("~") and (line.endswith("~") or line.endswith("~\n")):
#ビブラート設定タグ用
global vibratoDepth
global vibratoRate
depthBuff = ""
rateBuff = ""
depthIndex = line.find("depth" or "Depth")
rateIndex = line.find("rate" or "Rate")
if depthIndex == -1 and rateIndex == -1:
if ":" in line:
depthBuff = line.split(":")[0]
rateBuff = line.split(":")[1]
vibratoDepth = int(re.search("\d+", depthBuff).group())
vibratoRate = int(re.search("\d+", rateBuff).group())
else:
depthBuff = line
vibratoDepth = int(re.search("\d+", depthBuff).group())
elif depthIndex == -1:
rateBuff = line
vibratoRate = int(re.search("\d+", rateBuff).group())
elif rateIndex == -1:
depthBuff = line
vibratoDepth = int(re.search("\d+", depthBuff).group())
elif depthIndex < rateIndex:
depthBuff = line[depthIndex : rateIndex]
vibratoDepth = int(re.search("\d+", depthBuff).group())
rateBuff = line[rateIndex : len(line)]
vibratoRate = int(re.search("\d+", rateBuff).group())
elif rateIndex < depthIndex:
rateBuff = line[rateIndex : depthIndex]
vibratoRate = int(re.search("\d+", rateBuff).group())
depthBuff = line[depthIndex : len(line)]
vibratoDepth = int(re.search("\d+", depthBuff).group())
elif line.startswith("(") and (line.endswith(")") or line.endswith(")\n")):
amp = 0.5 * (int(re.search("\d+", line).group())/ 100)
elif line.startswith("[") and (line.endswith("]") or line.endswith("]\n")):
#音源ノート用
#[key octave (vol) {type}, length]
key = []
lineList = list(line)
keyBuff = ""
length = ""
lengthBuff = ""
mode = ""
noteType = []
noteTypeBuff = ""
vol = []
volBuff = ""
for l in lineList:
#ノート内のキーと長さの要素に分ける
if l is "[":
mode = "key"
elif checkAlnum(l) or l is "/":
if mode is "key":
keyBuff += l
elif mode is "length":
lengthBuff += l
elif mode is "vol":
volBuff += l
elif l is "+":
if not keyBuff is "":
key.append(keyBuff)
keyBuff = ""
if volBuff is "":
volBuff = "100"
vol.append(volBuff)
volBuff = ""
if noteTypeBuff is "":
noteTypeBuff = "default"
noteType.append(noteTypeBuff)
noteTypeBuff = ""
elif l is "(":
mode = "vol"
if not keyBuff is "":
key.append(keyBuff)
keyBuff = ""
elif l is ")":
mode = "key"
elif l is "{":
mode = "type"
if not keyBuff is "":
key.append(keyBuff)
keyBuff = ""
elif l is "}":
mode = "key"
elif l is "~":
if mode is "type":
noteTypeBuff += "vibrato"
elif l is "<":
if mode is "type":
noteTypeBuff += "fadein"
elif l is ">":
if mode is "type":
noteTypeBuff += "fadeout"
elif l is ",":
mode = "length"
if not keyBuff is "":
key.append(keyBuff)
keyBuff = ""
if volBuff is "":
volBuff = "100"
vol.append(volBuff)
volBuff = ""
if noteTypeBuff is "":
noteTypeBuff = "default"
noteType.append(noteTypeBuff)
noteTypeBuff = ""
elif l is "]":
length = lengthBuff
break
elif l is "#":
if not keyBuff is "":
key.append(keyBuff)
keyBuff = ""
if volBuff is "":
volBuff = "100"
vol.append(volBuff)
volBuff = ""
if noteTypeBuff is "":
noteTypeBuff = "default"
noteType.append(noteTypeBuff)
noteTypeBuff = ""
break
note(key, vol, length, noteType)
print(amp, key, vol, length, noteType)
global isDone
isDone = True
#main関数
def main():
while(not isDone):
print("Enter file name")
lineIn = input()
if lineIn is "exit":
break
else:
global fileName
fileName = lineIn
readScore()
saveWav()
if isDone:
print("Done!")
if __name__ is "__main__":
main()
###sample.txt
#bpm設定
<bpm=120>
#<120> 数値のみ
#単音
[C3, 1/1]
#休符
[R0, 1/1]
#[R1, 1/1] Rのオクターブは任意で可能
#和音
[C3, 1/4]
[C3+E3, 1/4]
[C3+E3+G3, 1/4]
[C3+E3+G3+C4, 1/4]
#音量
[C3, 1/4]
(vol=75)
#(75) 数値のみ
#[C3(50), 1/4]
#[C3+E3(25), 1/2] ノート内で単音に設定可能
#ビブラート設定
~depth:1, rate:5~
#~1~ depthのみ
#~1:5~ :で区切ってdepth:rate
#~depth, 1~ depthのみ
#~rate, 5~ rateのみ
#~rate:5, depth:1~ 文字で明記する際は反対でも可能
#ビブラート
[C3{~}, 1/1]
#[C3{~}+G3, 1/1] ノート内で単音に設定可能
#フェード
[C3{<}, 1/1]
[C3{>}, 1/1]
#[C3+G3{<}, 1/1] ノート内で単音に設定可能
#[C3{<~}, 1/1] ビブラートとフェードは併用可能、フェードインとフェードアウトは併用不可
##解説
###MakeMusic.py
amp = 0.5 #音量
A = 440.0 #A=440Hz
C = A * (2 ** (3 / 12)) #C=440*2^(3/12)H
rate = 44100 #サンプリングレート
bpm = 120 #BPM
scoreWave = None
filename = ""
グローバル変数。書き出しに必要だったり定数だったり。BPMは定数じゃないけど、スコアファイルからも途中で変更できるようにグローバル変数にしました。
#main関数
def main():
readScore()
saveWav()
if __name__ is "__main__":
main()
今後の拡張性等を兼ねてmain関数とif文。今後拡張していくかはわかりませんが、オープンソースなので誰かやりたい人いたら笑。
#音の周波数を計算してsin波を生成する関数
def sinWave_from_key(k, o, s):
"""
parameters
----------
k : string
key in scale
o : int
octave
s : int
seconds
Returns
----------
wave : ndarray
"""
t = np.arange(0, s, 1 / rate) #時間パラメータ
kArray = ['C', 'Cs', 'D', 'Ds', 'E', 'F', 'Fs', 'G', 'Gs', 'A', 'As', 'B']
wave = 0 * t
if k in kArray:
kNum = kArray.index(k)
kFreq = C * (2 ** (kNum / 12)) * (2 ** (o - 4))
wave = amp * np.sin(2 * np.pi * kFreq * t)
return wave
sin波を引数をもとに生成し、ndarray型のwaveを返します。引数は(キー, オクターブ, 秒数)となっています。
#和音や長さからsin波を生成する
def note(k, l):
"""
parameters
----------
k : string array
key and scale [example-"C4, G5, ..."]
l : string
fraction
"""
numeratorBuff = l.split("/")[0]
denominatorBuff = l.split("/")[1]
numerator = int(numeratorBuff)
denominator = int(denominatorBuff)
fraction = numerator / denominator
s = 60 / bpm * fraction * 4 #1/4音符4つで1小節
wave = None
for key in k:
keyName = re.search("[A-Za-z]*", key).group()
keyOctave = int(re.search("\d", key).group())
if wave is None:
wave = sinWave_from_key(keyName, keyOctave, s)
else:
wave += sinWave_from_key(keyName, keyOctave, s)
global scoreWave
if scoreWave is None:
scoreWave = wave
else:
scoreWave = np.append(scoreWave, wave)
スコアファイルから読み込んだノートをそれぞれ適切にsin波生成関数を用いて出力用の変数に格納していきます。やってることはコンパイラみたいなことです笑。
#保存
def saveWav():
name = ""
for s in filename:
if s is ".":
break
name += s
wavfile.write(name + ".wav", rate, scoreWave)
スコアファイルの名前をもとにwavファイルの名前を付け、書き出します。
#標準のisalnum等では不十分だったため半角英数のみのものを作った
def checkAlnum(s):
"""
parameters
----------
s : string
Returns
----------
result : bool
"""
alnum = re.compile(r'^[a-zA-Z0-9]+$')
result = alnum.match(s) is not None
return result
Python標準のisalnum等では","や全角文字も英数字として評価してしまって面倒だったので作りました。ほとんど完コピですが、参考にしたサイトがこちらです。私もまったく同じ状況にハマってしまって、試しにisalnumやisalpha等で色々な文字列を評価したところ、":"や";"まで英数字として評価されていたようでした・・・。これで小一時間悩まされました泣。
#txt形式のスコアを読み込む
def readScore():
global filename
filename = sys.argv[1]
if not filename.endswith(".txt"):
print("!ERROR!---Please check the file extension.")
return None
with open(filename) as f:
lines = f.readlines()
for line in lines:
if line.startswith("#"):
#コメントアウト用
continue
elif line.startswith("<") and (line.endswith(">") or line.endswith(">\n")):
#BPM設定タグ用
global bpm
bpm = int(re.search("\d+", line).group())
elif line.startswith("[") and (line.endswith("]") or line.endswith("]\n")):
#音源ノート用
key = []
lineList = list(line)
keyBuff = ""
length = ""
lengthBuff = ""
mode = ""
for l in lineList:
#ノート内のキーと長さの要素に分ける
if l is "[":
mode = "note"
elif checkAlnum(l) or l is "/":
if mode is "note":
keyBuff += l
elif mode is "length":
lengthBuff += l
elif l is "+":
key.append(keyBuff)
keyBuff = ""
elif l is ",":
key.append(keyBuff)
keyBuff = ""
mode = "length"
elif l is "]":
length = lengthBuff
note(key, length)
コマンドライン引数から読み込むファイルの名前を参照し、txt形式のスコアファイルから1行ずつ読み込んでBPMタグやnoteタグ等を割り振って処理しています。今後他のタグに対応させる時のために一応コメントアウト用のタグを明記してます。今のところは飾りなのでわざわざコメントタグを明記しなくても<>か[]でなければコメントとして扱われます。
##使い方
<bpm = 120>
[C2, 1/4]
[D2, 1/4]
[E2, 1/4]
[F2, 1/4]
[G2, 1/4]
[A2, 1/4]
[B2, 1/4]
[C3, 1/4]
###BPMタグ
<bpm = 120>
<
>
で囲まれた文字列はBPMタグとして扱われます。
数値のみを参照しBPMとして処理しているため、
<120>
<bpm=120>
<BPM120>
<びーぴーえむ120>
上記のどれでも半角で数値さえ入っていれば実行可能です。
###noteタグ
[C2, 1/4]
[D2, 1/4]
[E2, 1/4]
[F2, 1/4]
[G2, 1/4]
[A2, 1/4]
[B2, 1/4]
[C3, 1/4]
[
]
で囲まれた文字列はnoteタグとして扱われます。
noteタグでは["キー""オクターブ", "長さ"]
のように記述する必要があります。
また、和音を鳴らしたい場合、["キー1""オクターブ1"+"キー2""オクターブ2"+"キー3""オクターブ3", "長さ"]
のように音を+
で重ねることができますが、長さは1つのみになります。例として、
[C2+E2+G2, 1/4]
これで四分音符の長さでC2``E2``G2``(ドミソ)
の和音が生成されます。
また、
[C2, 1/4]
[C2+E2, 1/4]
[C2+E2+G2, 1/4]
[C2+E2+G2+C3, 1/4]
こうすることで順に音を重ねていくことも可能です。
また、休符の設定も可能で、
[R0, 1/4]
[R1, 1/4]
このように["キー以外の半角英字""オクターブ", "長さ"]
と入力することで休符を設定できます。R
以外でも問題はありませんが、R
を使った方が分かりやすいかと思います。オクターブも整数であればどうせ休符なので100
でも可能です。
###コメントタグ
#コメントタグです。
#
で開始した文字列は改行までコメントとして扱われます。上記のタグ以外の文字列であればコメントとして扱われますが、今後機能の拡張等を考え、#
を予約しておきました。
##まとめ
Pythonをあまり使ってこなかったのですが、やはり楽でいいですね。普段はC#やJavaを使うので定数がないことやクラス関数周りで不思議に思うことがいくつもあって面白かったです。
GUIで多機能な無料DAW等もありますが、CUIでシンセサイザーを動かして曲を作るのも独特で面白いので、今後これで曲を作ってみようと思います。
他にもmatplotlib
等入れて楽曲のsin波をグラフ化したり様々な音源を重ね合わせで再現したり、もしくは人工音声で歌詞を入れて歌わせたりできると思いますので、ぜひコピペでもして使ってみてください。