親子IoTワークショップ

マイコンとクラウドを使ってIoTとプログラミングを学ぼう

[ トップ | 開催予定・概要 | 2024-12開催 | お知らせ | Facebook ]

FFTを使って音の波形から周波数を求める

ここでは、純音ではない音波からその周波数を求める作業をします。前段階として、「録音した音をグラフにして「見える化」する」を済ませておいてください。

以下、純音ではない音波の例です。これは私が自分のウクレレでC4を弾いた時の音波です。ぐちゃぐちゃした波なので、これは純音ではありません。

図中の赤く示した部分が「波ひとつ分」で、この波が何度も繰り返されています。波ひとつ分の長さ(時間間隔)は5ミリ秒弱なので、ここではざっくり4ミリ秒ということにしましょう。波ひとつで4ミリ秒なので、1秒の時間間隔を取ったらその中に波は250個(1,000/4)収まります。ということは、この音波の周波数は250となります(周波数の定義は、1秒間の間に波が何回震えるか、つまり1秒間に波が何回繰り返されるかです)。「波ひとつで4ミリ秒」というのが「だいたいそのくらいの数」だったので、「周波数が250」というのも「だいたいそのくらいの数」です(C4の正式な周波数は261です)。

フーリエ変換、FFTとは

「だいたいの数」ではなく、正確な数として周波数を求めるにはフーリエ変換(Fourier transform)という数学的手法(mathematical method)を使います。これを使うと、どんな形のぐちゃぐちゃな波でも、その周波数を求めることができます。

ここでは、フーリエ変換の中身は説明できません。これは、高校で習う数学(三角関数、波の重ね合わせ、複素数など)を組み合わせ、そこに大学の内容を付け足してようやく理解できるものなので。そこで、ここではフーリエ変換が「何をしてくれるのか」だけ理解してください。

まずは、以下の図の赤い波を見てください(この図はWikipediaからコピーしました)。この赤い波の周波数を求めることを考えます。フーリエ変換がやるのは、この赤い波を、純音の音波の組み合わせとして表現することです。

どんな波も、単純な(純音の)波の組み合わせ(重ね合わせ)として表現できます。逆にいうと、単純な波を組み合わせれば、どんな複雑な(ぐちゃぐちゃな)波でも作り出すことができます。フーリエ変換は、与えられた波を単純な複数の波に分解してくれます。上の図で、赤い波は青い6つの波に分解されています(逆にいうと、その青い波を重ね合わせると赤い波になります)。以下の図は、この6つの青い波を見やすく(バラバラに)示したものです。

この図では、縦方向の振れ幅(振幅)の順に青い波を並べています。一番手前の青い波の振幅が最も大きく、一番奥の青い波の振幅が最も小さくなっています。そして、個々の波の振幅を棒グラフで表しています。フーリエ変換の結果得られるのは、この青い波の棒グラフです。より正確に言えば、分解された純音の波(青い波)の周波数と振幅が得られます。

分解された波(青い波)の振幅は、元々の波(赤い波)を合成する際の「重要度」(あるいは「貢献度」)だと考えてください。上の図では、青い波を6つ重ね合わせると赤い波を合成できますが、その合成の際に最も重要な役割を果たす(貢献をする)のが、一番手前の青い波です。奥の方へ行くほど重要度(貢献度)は低くなります。つまり青い波の棒グラフは、重要度(貢献度)の度合いを表しています。

この棒グラフのことを「周波数スペクトラム(frequency spectrum)」といいます。横軸が周波数、縦軸が重要度(貢献度)です。ここでは「spectrum」を「スペクトラム」と英語風に読む(カタカナ化する)ことにします。フーリエ変換の生みの親ジョセフ・フーリエはフランス人なので、日本語の数学や物理の教科書では「spectrum」をフランス語風に「スペクトル」とすることがほとんどです。でも、「スペクトル」と言ってもフランス語会話でしか通じないので、ここでは「スペクトラム」ということにします。

まとめると、フーリエ変換がやるのは、

ことです。

フーリエ変換をコンピュータで実行するためのアルゴリズム(algorithm)がFast Fourier Transform(FFT)です。「アリゴリズム」と言う言葉が分かりにくければ、フーリエ変換をコンピュータで実行するための「やり方」(作業手順、仕掛け、メカニズム)だと思ってください。

楽器の音をFFTする

それでは、実際にFFTをやってみましょう。サンプルプログラムはsound-fft.pyです。このプログラムは、WAV形式で音がファイルに録音されていて、そのファイル名がtest.wavであることを想定しています。「録音した音をグラフにして「見える化」する」で使ったrecord-sound-samples.pyを動かして、事前に音をtest.wavに録音しておいてください。

sound-fft.pyは、以下のようにWAVファイルから音のサンプルを読み、そのサンプルを関数sanalysis.fft()に渡してFFTを実行しています。

soundSamples = sanalysis.wavToSamples(fileName)
freqSpectrum = sanalysis.fft(soundSamples)

関数sanalysis.fft()はFFTを行い、その結果を周波数スペクトラムとして返します。ここでは、その結果は変数freqSpectrumに格納されています。そして、以下のように関数sanalysis.drawFreqSpectrum()を使って、得られた周波数スペクトラムをグラフ表示します。

sanalysis.drawFreqSpectrum(freqSpectrum=freqSpectrum,
                           samplingRate=44100)

sanalysis.drawFreqSpectrum()を使うときには、周波数スペクトラムと、元々の録音データのサンプリングレートを渡してください。ここでは、record-sound-samples.pyを使って(CD品質で)録音したとの想定で、サンプリングレートに44,100を指定しています。私のウクレレのC4の音は、以下のような周波スペクトラムになりました。

この図の横軸は周波数、縦軸は重要度(貢献度)です。横軸の周波数の数値は0から始まって、右へ行くほど大きくなっていきます。そして、それぞれの周波数の値に対応する重要度(貢献度)が縦軸の値です。上のグラフでは、0に近い場所の周波数で最大の重要度(貢献度)を記録しています(重要度は0.02を少し超えたあたり)。この周波数をもった純音が、元々のウクレレの音を合成する際に最も重要な役割を果たす(貢献をする)波です。ざっくり言えば、この周波数が、元々のウクレレの音の周波数に近いということです。

そこで、この周波数をグラフの中で眺めるのではなく、数字として得ることにします。このために、関数sanalysis.getHighestPeakFrequency()を使います。この関数を使うときにも、周波数スペクトラムと、元々の録音データのサンプリングレートを渡してください。

highestPeakFreq = sanalysis.getHighestPeakFrequency(
                                    freqSpectrum=freqSpectrum,
                                    samplingRate=44100)
print("Highest peak frequency (Hz): " + str(highestPeakFreq))

この関数は、与えられた周波数スペクトラムの中で最大の重要度(貢献度)をもった周波数を返します。以下のように、ThonnyのShellにその周波数が表示されるはずです。

Highest peak frequency (Hz): 263.00596385405566

先に、「0に近い場所の周波数で最大の重要度(貢献度)を記録」と書きましたが、その周波数が具体的には263であることが分かりました。元々のウクレレの音はC4の音で、C4の周波数は261.626なので、FFTが非常に近い答えを出したことが分かります(1%以下の誤差)。誤差を手で計算するのではなく自動的に(プログラムで)計算するには、以下のようなプログラムを最後に追加してください。

toneFrequency = 261.626
error = abs(toneFrequency - highestPeakFreq)
print("Error (%): " + str(error/toneFrequency * 100))

FFTの結果から「最大の重要度(貢献度)をもった周波数」を求め、それを元々の音の周波数と比べると、必ず誤差(error)があります。この誤差はゼロにはなりません。なぜなら、最大ではないが重要度(貢献度)をある程度もった周波数(重要度が2番目以降の周波数)を無視しているからです。

FFTの結果から楽器音の音名を割り出す

sound-fft-notename.py

スペクトラム・アナライザーやチューナーを使ってみる

周波数スペクトラムから複数のピークを取り出す

ビープ音(純音)をFFTする

自習プロジェクトの目次に戻る