LoginSignup
58
37

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

Last updated at Posted at 2023-05-13

動機

 ギターのチューナーが壊れてしまい、スマホアプリを使ってみたもののあまりの広告の多さにイライライライラ…。ギター初心者にとってチューナが壊れるのは痛手なので、大学時代に信号処理系の研究を行っていたこともあり、チューナーを作成してみることにしました。手あたり次第で作ってるので優しい目で見ていただけるとありがたいです。
GitHubにも登録しているので参考にしてみてください
後編はこちらから

追記

どうやらピッチ推定なるものがあるらしいです。車輪の再開発をしてしまっているみたいなので、詳しく知りたい方はこちらのURL群などを参照してください。
音の三大要素について 第五回
基本周波数をlibrosaで簡単に測定する【Python】
また、今回のやり方だと完璧な精度を求めることはできなさそうなので、アルゴリズムを変えないといけなさそうですね。
FFTのピークでは基本周波数を測れないわけ

実行環境

  • Windows10
  • python 3.10.11

要件定義

 ギターのチューナー作成に当たり、超絶ざっくり要件定義を行なってみました。
ユースケースとしては以下の3点があると考えました。

  1. 音をリアルタイムで録る
  2. 録った音の音程を表示する
  3. チューニングするべき音程からどれくらい離れているかを表示する

1.音をリアルタイムで録る

 チューナーは当たり前ですが、リアルタイムで音を録りその音の高さを表示してくれます。リアルタイムで音を録るにあたりこちらの記事を参考にさせていただきました。
下準備として、pyaudio,numpy,matplotをインストールします。

pyaudio
$ python3 -m pip install pyaudio
numpy
$ python3 -m pip install pyaudio
matplot
$ python3 -m pip install pyaudio
サンプルコード
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
そして、この波は様々な音(波)の組み合わせにより作られています。例えば雑音や人の話し声などなどです。このままでは複雑で抽出したい音(今回でいうとギターの音色)をあれこれできません。そこで活躍するのがフーリエ変換です。フーリエ変換をすることで波を分解することができます。つまり、時間領域のデータを横軸が周波数(Hz)縦軸を振幅の周波数領域に変換することができます。

フーリエ変換後

フーリエ変換後.png
 これだけの変換ではまだ複雑で見にくいのでさらに変換をかけます。というのも、変換をかけたデータは複素数が含まれているためです。(詳しくは参考書や別サイトを参照してください。)

さらに見やすく変換(見やすいように一部を抽出しています)

絶対値画像.png
 先ほど抽出したデータの絶対値をとることで見やすいデータにすることができました。これにより、ようやくギターの音程をしらべるための下準備をすることができました。

2-2 ギターの音を録ってみる

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

$ python3 -m pip install scipy 

つづいて、以下のコードをソースコードに追加します。

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

さっそく起動して確認してみます。1弦の開放弦を鳴らしてみた結果です。1弦の開放弦の周波数は329.63Hzみたいなので、おそらく取れているのではないかと思います。
peak.png
 これから、この値などを用いて表示やチューニングが正しくできているかを調べていきます。

コード全体

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を使った高速フーリエ変換による周波数解析
PythonでFFTとそのピーク検出をする方法
Python マイク入力音声をリアルタイムで波形(グラフ)表示する方法[Windows Pyaudioとmatplotlib]

58
37
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
58
37