ギターのチューナーを作ってみたい【後編】

# 後編でやること
　前回の記事はこちらから→[ギターのチューナーを作ってみたい【前編】](https://qiita.com/NicoTheDog/items/d5612e8a915deac99c43)
　ソースコードはこちらから→[GitHub](https://github.com/NicoTheDog/GuitarTuner/blob/main/getSound.py)
　後編では前編で抽出したピークの周波数を元に、Tkinterを用いて実際にチューナを作ってみようと思います。Tkinterは見やすくするために用いるので、最小限の表示に留めています。
　前回の要件定義から、前編から残された機能としては太文字の部分ですので、こちらを仕上げていこうと思います。
1. ~~音をリアルタイムで録る~~
1. **録った音の音程を表示する**
1. **チューニングするべき音程からどれくらい離れているかを表示する**

# 2. 録った音の音程を表示する
## 2-3.検知したピークをフィルタリングする
　ギターの開放弦の音程はそれぞれ以下の周波数らしいです。（簡略化のため整数値で表してます）
- 1弦：330Hz（E）
- 2弦：247Hz（B）
- 3弦：196Hz（G）
- 4弦：147Hz（D）
- 5弦：110Hz（A）
- 6弦：82Hz（E）
　前回までのコードでは、この周波数領域以上の周波数までピークに含まれているので、この周波数領域内に収まるようにフィルタリングをかけてあげます。
```python
def Detect_Peak(x,y):
    # 録ったデータのピークを全て調べる
    peaks,index =  scipy.signal.find_peaks(y,prominence=0.01,height=(0,440))
    list_peaks = []
    # 基音（440Hz未満のピーク値だけを抽出する）
    for p in peaks:
        if(x[p] > 440):
            break
        list_peaks.append(p)
    return list_peaks

```
かなりゴリ押しですが、これで440Hz未満のピークをフィルタリングことができるようになりました。
## 2-4 録った音の音程を表示する
　ようやく本題の音程を表示するところまでたどり着けました。表示に関してはCMD上でもよかったのですが、味気無さを感じたので`Tkinter`を用いて表示させます。`mainloop()`を用いる際に一番下に置かないとGUI上に何も表示されない問題が生じたので、`mainloop()`を一番下に配置しています。以下のコードを追加することで、GUI上の表示＋現在の音程を表すことができるようになります。
```python
Regular_Tuner = {
    "E" : 82,
    "A" : 110,
    "D" : 147,
    "G" : 196,
    "B" : 247,
    "E" : 330
}

root = tk.Tk()
root.geometry('600x400')
root.title('サンプル画面')

label = tk.Label(
    root,
    width=20,
    height=1
)
label.pack()
def GetNearestValue(values):
    # 差が最小の値のindexを取得する
    tuner = ""
    diff = 1000
    for key in Regular_Tuner.keys():
        for v in values:
            if(abs(Regular_Tuner[key] - v) < abs(diff) ):
                diff = Regular_Tuner[key] - v
                tuner = key
    return tuner

def Display_Window(x,y):
    peaks = Detect_Peak(x,y)
    # print(peaks)
    tuner = GetNearestValue(x[peaks])
    # print(str(tuner)
    if tuner:
        label.config(
            text = str(tuner),
            font = ("MSゴシック","20","bold")
        )
        label.update()

root.mainloop()
```
`def  GetNearestValue(values)`は`Regular_Tuner`に格納された`Value`の値に最も近い`Key`を抽出する関数です。
実際に起動してみた結果が以下のようになります。1弦の開放弦を鳴らしてみた結果です。
##### 音程表示
![音階のみ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334934/c6587a25-d6ea-11ae-dbde-80051dc1ecac.png)
（味気ない…。）
　ひとまず、チューニングが正しい状態で正しい音程を表示することができました。

# 3.チューニングするべき音程からどれくらい離れているかを表示する
　ようやく最終工程にたどり着くことができました。先ほどのコードにちょこっと変更を加えることでどのくらい周波数がずれているのかを表示することができるようになります。コード自体は最後に載せますので結果だけ。
##### 最終結果（上から1弦の開放弦～6弦の開放弦です。）
![結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334934/052c6b85-439f-460b-8867-b2484b9288f3.png)

　ギターのチューニングはスマホアプリで行ったので恐らくチューニングは合っている上での検証結果です（本末転倒）。5弦と6弦のときだけ上手くいかない結果となりましたが、おそらくパラメータチューニングやアルゴリズムの改善、マイクの変更などをすればもう少し結果が変わるのかなぁと思います。

# コード全体
```python:getSound.py
import pyaudio # 音声を録音するためのライブラリ   
import numpy as np
import matplotlib.pyplot as plt # 描画用のライブラリ
import scipy
import tkinter as tk

SAMP_RATE = 44100 #サンプリングレート
FORMAT = pyaudio.paInt16
CHUNK = 1024
CHANNELS = 1

Regular_Tuner = {
    "E" : 82,
    "A" : 110,
    "D" : 147,
    "G" : 196,
    "B" : 247,
    "E" : 330
}

root = tk.Tk()
root.geometry('600x400')
root.title('サンプル画面')

label = tk.Label(
    root,
    width=20,
    height=1
)
label.pack()

def Record_Audio():
    audio = pyaudio.PyAudio()
    record_data = audio.open(
        format = FORMAT,
        channels = CHANNELS,
        rate = SAMP_RATE,
        input = True,
        frames_per_buffer = CHUNK
    )

    return record_data,audio

def Record_Stop(record_data,audio):
    record_data.stop_stream()
    record_data.close()
    audio.terminate()

def Detect_Peak(x,y):
    # 録ったデータのピークを全て調べる
    peaks,index =  scipy.signal.find_peaks(y,prominence=0.01,height=(0,440))
    list_peaks = []
    # 基音（440Hz未満のピーク値だけを抽出する）
    for p in peaks:
        if(x[p] > 440):
            break
        list_peaks.append(p)
    return list_peaks


def GetNearestValue(values):
    # 差が最小の値のindexを取得する
    tuner = ""
    diff = 1000
    for key in Regular_Tuner.keys():
        for v in values:
            if(abs(Regular_Tuner[key] - v) < abs(diff) ):
                diff = Regular_Tuner[key] - v
                tuner = key
    return tuner,int(diff)

def Display_Window(x,y):
    peaks = Detect_Peak(x,y)
    # print(peaks)
    (tuner,diff) = GetNearestValue(x[peaks])
    # print(str(tuner) + " : " + str(diff))
    if tuner:
        label.config(
            text = str(tuner) + ":" + str(diff),
            font = ("MSゴシック","20","bold")
        )
        label.update()

def Display_Data(x,y):
    peaks = Detect_Peak(x,y)
    plt.plot(x,y)
    plt.scatter(x[peaks],y[peaks],color = 'red')
    plt.xlabel("frequency [Hz]")
    plt.ylabel("amplitude spectrum[V]")
    plt.xlim(1,1024)
    plt.grid()
    plt.draw()
    plt.pause(0.001)
    plt.cla()

def Fourier_Transform(record_data):
    data = record_data.read(1024)
    audio_data = np.frombuffer(data,dtype='int16')
    # 取得したデータをフーリエ変換をする(複素配列)
    F = np.fft.fft(audio_data)
    # 振幅を求める
    F = F / (SAMP_RATE / 2)
    window = scipy.signal.hann(SAMP_RATE)
    F = F * (SAMP_RATE / np.sum(window))
    amp = np.abs(F)
    # 周波数を求める
    freq = np.fft.fftfreq(1024,d=(1/SAMP_RATE))
    # パワースペクトルを求める
    amp = pow(amp,2)
    # Display_Data(freq[:1024//2],amp[:1024//2])
    Display_Window(freq[:1024//2],amp[:1024//2])
if __name__ == "__main__":

    (record_data,audio) = Record_Audio()

    while True:
        try:
            Fourier_Transform(record_data)
        except KeyboardInterrupt:
            break
    
    Record_Stop(record_data,audio)

root.mainloop()
```
# まとめ
　思い付きからギターのチューナーを作成してみました。パラメータチューニングなどをサボった結果精度が低いものが出来ましたが、信号処理の復習をしながら作成することができました。畳み込みなども用いればまた違ったアルゴリズムで実現できると思いますが、今回はあきらめました。精度を上げるのは気が向いたらやろうと思います。
　もし間違い等ありましたら、コメント等で教えていただけると幸いです。