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

# 動機
　ギターのチューナーが壊れてしまい、スマホアプリを使ってみたもののあまりの広告の多さにｲﾗｲﾗｲﾗｲﾗ…。ギター初心者にとってチューナが壊れるのは痛手なので、大学時代に信号処理系の研究を行っていたこともあり、チューナーを作成してみることにしました。手あたり次第で作ってるので優しい目で見ていただけるとありがたいです。
[GitHub](https://github.com/NicoTheDog/GuitarTuner)にも登録しているので参考にしてみてください
[後編はこちらから](https://qiita.com/NicoTheDog/items/fef6c828b1e1904b90c3)
### 追記
  どうやらピッチ推定なるものがあるらしいです。車輪の再開発をしてしまっているみたいなので、詳しく知りたい方はこちらのURL群などを参照してください。
[音の三大要素について　第五回](https://note.com/empath_note/n/n014691cbba0b)
[基本周波数をlibrosaで簡単に測定する【Python】](https://self-development.info/%E5%9F%BA%E6%9C%AC%E5%91%A8%E6%B3%A2%E6%95%B0%E3%82%92librosa%E3%81%A7%E7%B0%A1%E5%8D%98%E3%81%AB%E6%B8%AC%E5%AE%9A%E3%81%99%E3%82%8B%E3%80%90python%E3%80%91/)
  また、今回のやり方だと完璧な精度を求めることはできなさそうなので、アルゴリズムを変えないといけなさそうですね。
[FFTのピークでは基本周波数を測れないわけ](https://tadaoyamaoka.hatenablog.com/entry/2016/08/27/123525)

# 実行環境
- Windows10
- python 3.10.11
# 要件定義
　ギターのチューナー作成に当たり、超絶ざっくり要件定義を行なってみました。
ユースケースとしては以下の２点があると考えました。
1. 音をリアルタイムで録り、録った音の音程を表示し、現在の音程がどれだけ正しいかを確認すること
1. チューニングするべき音程からどれくらい離れているかを表示することで、チューニングを行うこと

# 1.音をリアルタイムで録る
　チューナーは当たり前ですが、リアルタイムで音を録りその音の高さを表示してくれます。リアルタイムで音を録るにあたり[こちらの記事](https://niyanmemo.com/335/)を参考にさせていただきました。
下準備として、`pyaudio`,`numpy`,`matplot`をインストールします。
##### pyaudio
```
$ python3 -m pip install pyaudio
```
##### numpy
```
$ python3 -m pip install pyaudio
```
##### matplot
```
$ python3 -m pip install pyaudio
```
##### サンプルコード
```python:getSound.py
import pyaudio # 音声を録音するためのライブラリ   
import numpy as np
import matplotlib.pyplot as plt # 描画用のライブラリ

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

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()

if __name__ == "__main__":

    (record_data,audio) = Record_Audio()

    while True:
        try:
        except KeyboardInterrupt:
            break
    
    Record_Stop(record_data,audio)
```
　これで音をリアルタイムに録ってくれる機能を作成することができました。（素晴らしいコードをありがとうございます…）

# 2.録った音の音程を表示する
## 2-1　フーリエ変換
 さて、上記のコードだけでは音を録れているのか分からない上に、音程を表示するためには録った音の周波数を調べる必要があります。そこで活躍するのが`高速フーリエ変換
(FFT)`です。
まず簡単に、フーリエ変換について説明します。
音声データは以下のように時間領域として保存されているため、横軸は時間、縦軸は変位として現わされています。先ほどのコードに`matplot`を用いて表示しています。
##### 適当な音声データ
![音声データサンプル画像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334934/c1fbe226-a077-3e83-ac25-ba839b745f62.png)
そして、この波は様々な音（波）の組み合わせにより作られています。例えば雑音や人の話し声などなどです。このままでは複雑で抽出したい音（今回でいうとギターの音色）をあれこれできません。そこで活躍するのがフーリエ変換です。フーリエ変換をすることで波を分解することができます。つまり、時間領域のデータを横軸が周波数（Hz）縦軸を振幅の周波数領域に変換することができます。
##### フーリエ変換後
![フーリエ変換後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334934/e5f6501b-0a3c-e019-2f79-3c2ee4edcfde.png)
　これだけの変換ではまだ複雑で見にくいのでさらに変換をかけます。というのも、変換をかけたデータは複素数が含まれているためです。（詳しくは参考書や別サイトを参照してください。）

##### さらに見やすく変換（見やすいように一部を抽出しています）
![絶対値画像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334934/18768fa5-9fda-834c-fa7c-22ebfae2aa0d.png)
　先ほど抽出したデータの絶対値をとることで見やすいデータにすることができました。これにより、ようやくギターの音程をしらべるための下準備をすることができました。

## 2-2 ギターの音を録ってみる
　さて、下準備をしたところでギターの音をとってみよう思います。
ギターの音をとるためには、ピークを知る必要があるため、`scipy`を用いてピークを検出してみます。
まずはインストールから
```
$ python3 -m pip install scipy 
```

つづいて、以下のコードをソースコードに追加します。
```python
import scipy
def Detect_Peak(y):
    return scipy.signal.find_peaks(y,prominence=0.01,height=(0,440))

def Display_Data(x,y):
    (peaks,index) = Detect_Peak(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])

```
 さっそく起動して確認してみます。１弦の開放弦を鳴らしてみた結果です。１弦の開放弦の周波数は329.63Hzみたいなので、おそらく取れているのではないかと思います。
![peak.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334934/3dc721b5-5571-8730-5502-cf0aefdb4a2b.png)
　これから、この値などを用いて表示やチューニングが正しくできているかを調べていきます。

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

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

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(y):
    return scipy.signal.find_peaks(y,prominence=0.01,height=(0,440))

def Display_Data(x,y):
    (peaks,index) = Detect_Peak(y)
    print(index)
    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])

if __name__ == "__main__":

    (record_data,audio) = Record_Audio()

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


# まとめ
前編はこれにて終了にしたいと思います。（開発途中のため）
間違っている説明などありましたら、適宜コメント等で指摘していただけるとありがたいです。
後編はいつ書くかは未定ですが、なるべく早めに上げようと思います。
参考
[NumPyを使った高速フーリエ変換による周波数解析](https://helve-blog.com/posts/python/numpy-fast-fourier-transform/)
[PythonでFFTとそのピーク検出をする方法](https://qiita.com/kuwamaso/items/6a4c8310c0222a72afe0)
[Python マイク入力音声をリアルタイムで波形(グラフ)表示する方法[Windows Pyaudioとmatplotlib]](https://niyanmemo.com/335/)