Pythonでシンセサイザーを作って演奏する。

#Pythonでシンセサイザーを作って演奏する。
##Python実行環境
~~AWS cloud9 EC2 Ubuntu t2.small~~
~~Python 3.6.9~~
windws10
Visual Studio 2019
Python 3.7

##ソースコード
###MakeMusic.py
```python: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
```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
```python
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は定数じゃないけど、スコアファイルからも途中で変更できるようにグローバル変数にしました。

```python
#main関数
def main():
    readScore()
    saveWav()
    
if __name__ is "__main__":
    main()
```
　今後の拡張性等を兼ねてmain関数とif文。今後拡張していくかはわかりませんが、オープンソースなので誰かやりたい人いたら笑。

```python
#音の周波数を計算して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を返します。引数は(キー, オクターブ, 秒数)となっています。

```python
#和音や長さから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波生成関数を用いて出力用の変数に格納していきます。やってることはコンパイラみたいなことです笑。

```python
#保存
def saveWav():
    name = ""
    for s in filename:
        if s is ".":
            break
        name += s
    wavfile.write(name + ".wav", rate, scoreWave)
```
　スコアファイルの名前をもとにwavファイルの名前を付け、書き出します。

```python
#標準の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等では","や全角文字も英数字として評価してしまって面倒だったので作りました。ほとんど完コピですが、参考にしたサイトが[こちら](https://qiita.com/fujiy/items/f738aa9d0bb7427e07a4)です。私もまったく同じ状況にハマってしまって、試しにisalnumやisalpha等で色々な文字列を評価したところ、":"や";"まで英数字として評価されていたようでした・・・。これで小一時間悩まされました泣。

```python
#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波をグラフ化したり様々な音源を重ね合わせで再現したり、もしくは人工音声で歌詞を入れて歌わせたりできると思いますので、ぜひコピペでもして使ってみてください。