ArduinoでFFTを使う

マイクから拾った音の音階を調べるプログラムのテストです。Arduinoだけを使ってFFTする方法を調べてみたのですが、情報が古く、日本語では欲しい情報になかなかたどり着けなかったので上手く行ったやり方を紹介します。

FFTとは

一定期間の音信号を解析して、どの周波数成分がどの程度含まれているか求める手法です。信号の数を2のべき乗にすることによって高速に計算することが出来ます。

あんま上手く説明できません。とりあえず、鳴っている音の周波数が何なのかわかる手法です。

使うもの

  • ESP8266(手持ちのArduino nanoでもやってみましたが、メモリが足りずスケッチが書き込めませんでした。他のArduinoは要検証です。)。以下の2個セットのやつが値段的にも最安でWiFiも使えてメモリも多いのでよく買ってます。
  • マイクモジュール

やり方

ここのやり方に沿って進めていくだけですが、かいつまんで解説していきます。
https://www.norwegiancreations.com/2017/08/what-is-fft-and-how-can-you-implement-it-on-an-arduino/

ライブラリをインストール

Arduino IDEを開き、スケッチ→ライブラリをインクルード→ライブラリを管理…の順にクリックします。

スケッチ→ライブラリをインクルード→ライブラリを管理… の順にクリック

検索バーに「fft」を入力し、「arduinoFFT」をインストールします。

arduinoFFTをインストール

これだけで完了です。

プログラム書き込み

下にサンプルプログラムを置いておきます。上記サイトに掲載されているプログラムほぼそのままです。一部ESP8266向けに変更しています。オリジナルよりもサンプル数とサンプリング周期を上げています。

FFTの特徴として、サンプリング周波数の半分の周波数までしか解析することが出来ません。サンプリング周波数が8000Hzなら、4000Hz以上の音は検出できません。

また、サンプル数を多くすると、結果の分解能が上がる代わりに測定にかかる時間が増えます。サンプル数は2のn乗にしなければならないと決まっていますので、欲しい分解能に応じて決める必要があります。

分解能はサンプリング周波数[Hz]÷サンプル数になります。つまり下のサンプルプログラムでは8000÷256=32[Hz]刻みの結果しか得られないことになります。判別する周波数の精度を上げたい場合にはサンプル数(=サンプリング時間)をあげる必要があります。

下記のプログラムでは最も成分の多い周波数(ピーク周波数)を出力するようにしています。

#include "arduinoFFT.h"
 
#define SAMPLES 256             //Must be a power of 2
#define SAMPLING_FREQUENCY 8000 //Hz, must be less than 10000 due to ADC
 
arduinoFFT FFT = arduinoFFT();
 
unsigned int sampling_period_us;
unsigned long microseconds;
 
double vReal[SAMPLES];
double vImag[SAMPLES];
 
void setup() {
    Serial.begin(115200);
 
    sampling_period_us = round(1000000*(1.0/SAMPLING_FREQUENCY));
}
 
void loop() {
   
    /*SAMPLING*/
    for(int i=0; i<SAMPLES; i++)
    {
        microseconds = micros();    //Overflows after around 70 minutes!
     
        vReal[i] = analogRead(A0);  //ESP8266の場合は「A0」。普通のArduinoは「0」。
        vImag[i] = 0;
     
        while(micros() < (microseconds + sampling_period_us)){
        }
    }
 
    /*FFT*/
    FFT.Windowing(vReal, SAMPLES, FFT_WIN_TYP_HAMMING, FFT_FORWARD);
    FFT.Compute(vReal, vImag, SAMPLES, FFT_FORWARD);
    FFT.ComplexToMagnitude(vReal, vImag, SAMPLES);
    double peak = FFT.MajorPeak(vReal, SAMPLES, SAMPLING_FREQUENCY);
 
    /*PRINT RESULTS*/
    Serial.println(peak);     //Print out what frequency is the most dominant.
 
    for(int i=2; i<(SAMPLES/2); i++)
    {
        /*View all these three lines in serial terminal to see which frequencies has which amplitudes*/
         
        //Serial.print((i * 1.0 * SAMPLING_FREQUENCY) / SAMPLES, 1);
        //Serial.print(" ");
        //Serial.println(vReal[i], 1);    //View only this line in serial plotter to visualize the bins
    }
 
    delay(1);  //Repeat the process every second OR:
    //while(1);       //ESPだと無限ループがエラーになるので注意
    
}

Arduinoとマイクを接続

わざわざ書くまでもありませんが、Arduinoとマイクを接続します。3.3VとGNDとA0をマイクモジュールにつなぐだけです。

マイクモジュールを使えば簡単!

テスト

プログラムを書き込んだらシリアルプロッタを開いて口笛を吹いてみます。

口笛が下手

口笛の音階があっているかはさておき、音の高さを判別することができました!

追記:電源を安定化すれば精度が上がる

上記の方法で問題なくFFTはできるのですが、たまに誤差が多い結果が出ることもありました。安いマイクなのでしょうがないのかな?と思っていましたが、ふと思いついて電源を見直したところ精度がめっちゃ向上したので報告します。

どうやらArduinoの3.3Vピンから電源を取るとノイズが多いようで、マイクの出力がノイズを含んでいました。

マイクの電源を別に用意したところ、ノイズがなくなりFFTの出力もキレイになりました。

マイク用電源を別に追加

電池を別に用意するのが面倒な場合は、電源安定回路やレギュレータを使うのもありです。esp8266のVinピンから取っても大丈夫でした。(VinはUSBの5Vと繋がっている)

今回は以上です。

「ArduinoでFFTを使う」への13件のフィードバック

  1. 素晴らしいですね!
    シリアルプロッタというのを使うとそのままグラフに表されるんですか?
    だとしたら本当にスゴイです!

    1. ありがとうございます
      もちろんシリアルプロッタだけでグラフを表示できます!
      まあシリアルプロッタも万能ではなくて、軸ラベルの表示や表示範囲の変更が出来なかったりします。結果を簡単に確認したい時によく使います。

  2. パパドラゴンさん
    Twitterで返事できてなくてすみません。スマホ換えた時に移すの失敗してしまってもう触れないんです…
    がメールでDMは見れています。どうぞお気になさらず!私はここのページの回路を自分でもやってみようとしているところです。また余裕ある時こういうページ書いてください。私には意義あるページです。
    ではまた!

    1. ありがとうございます!
      そう言っていただけると大変励みになります
      なにかご質問等ありましたら気軽にご連絡ください

  3. 初めまして.
    このシリアルプロッタに表示される「縦軸」と「横軸」をご教授お願いします.

    1. ご質問ありがとうございます
      縦軸は、ピーク周波数(最も大きい成分を持つ周波数)で単位はHzです。
      横軸はデータインデックスで、出力するたびにカウントアップする値です。上のプログラムでは約0.032秒毎にFFTを計算するので、時刻tと考えてもらっても構いません。

  4. FFT解析すごいですね!参考になります。
    実はAM/FM兼用(AMスーパー)Radioを作成しようと考えていまして、アンテナとバリコンの共振周波数をどうにかして7セグにだせないものかと考えておりました。アンテナとバリコンの共振回路からの信号をFFT解析して最大周波数のみを出力すればよさそうですね!
    長文失礼しました!

  5. 初めまして。
    私も同じようなものを製作しようと思っており参考にさせていただいているのですが、こちらのサイトとは違う型番のマイクセンサを用いた場合にスケッチのどの部分を変更すればいいのかについて苦戦しております。使っている基盤はArduino UNOです。

    1. すぐお返事できなくてすみません。
      基本的にはどのようなマイクを使ってもスケッチを変更する必要はありません。
      マイクの出力がArduinoのanalogRead関数で読めさえすればOKです。

      Arduino UNOの場合は、
      vReal[i] = analogRead(A0);
      の部分を、
      vReal[i] = analogRead(0);
      のように変える必要があります。(マイクの出力をA0ピンにつないだ場合)
      まずはanalogRead関数だけでマイクの出力がシリアルプロッタに表示できるか確認するのが良いと思います。

  6. サイトを拝見させていただきました。
    自分も加速度センサーから得られた値でノイズを除去したいがために、arduinoでFFT解析の参考となるサイトを探していたため助かりました。もし、よろしければこちらのグラフはFFTした後だと思いますが、する前のグラフ見せてもらうことできますか?違いが見たいです

    1. コメントありがとうございます。こちらがFFTに掛ける前のマイクからの入力データです。
      横軸が時間、縦軸が電圧[V]です。
      sensor-raw

      ちなみに、記事内のグラフは1回のFFT結果(横軸が周波数[Hz]、縦軸がパワー)ではなく、短時間の入力をFFTにかけ、そのピーク周波数を連続でプロットしています。1回のFFT結果はvRealを見てください。

  7. 初めまして.
    arduinoでFFTを使おうとしているためにこのサイトを拝見しております.
    その中で質問なのですが,FFTされたセンサ値のグラフと,FFTされる前の値のグラフを同時に流すことは可能でしょうか?
    お手数ですが、ご返事いただけると幸いです.

    1. コメントありがとうございます。
      センサ値とFFT結果を同じグラフに表示することは不可能です。なぜならセンサ値とFFT結果は単位や性質が全く異なるからです。
      センサ値は「横軸が時間、縦軸が電圧」の時系列データであるのに対し、FFTをかけると「横軸が周波数、縦軸がパワー(その周波数成分が一定時間あたりにどのくらい含まれるか)」になります。
      前者の波形はシリアルプロッタで表示できますが、後者はそもそも時系列データではないので同時に表示することは不可能です。

      ちなみに本記事最後の口笛の音階のグラフは、毎時刻ごとに過去0.032秒間のセンサ値をFFTにかけ、ピーク値(最もパワーが大きい周波数)を表示しています
      これならば横軸は時間になり、センサ値と同時に表示することは一応可能ですが、センサ値(電圧)とピーク値(周波数)を比較するメリットは無いかと思います。

匿名 へ返信する コメントをキャンセル

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA