Help us understand the problem. What is going on with this article?

Siv3Dで録音関係を扱う場合の注意点

More than 5 years have passed since last update.

バッファが空になってFFTに失敗するタイミングが存在する

Siv3Dで録音した結果にFFTをかけてリアルタイムで表示したい,と言う時,どういうわけか定期的にスペクトルが記録されないタイミングがあります.

原因の一つとして,バッファ内に音声データが足りないせいでFFTが失敗するということが挙げられます.
これはFFTの仕組み上,どうしても仕方がないことなので諦めるしかないのですが,これの頻度を少しでも減らすということはできます.

その方法が,録音時間を長く取ることです.

デフォルトだと録音時間は10秒で,10秒毎にFFTが失敗するわけですが,これを例えば10分とか取るようにすれば,FFTの失敗する頻度も減るわけです.

# include <Siv3D.hpp>

void Main()
{
    const Font font(30);

    Window::Resize(1024, 320);

    // デフォルトだと10秒間録音している
    //Recorder::SetBufferLengthBySec(s3d::RecorerSamplingRate::S44100, 10);
    Recorder::SetBufferLengthBySec(s3d::RecorerSamplingRate::S44100, 60 * 5);

    if (!Recorder::Start())
    {
        return;
    }

    // この子で何秒間録音してるか調べます
    TimerSec timerSec;
    timerSec.start();

    while (System::Update())
    {
        if (Recorder::IsEnd())
        {
            // バッファが一杯になったのでバッファの最初に戻る
            Recorder::Restart();
            timerSec.restart();
        }

        Waving::FFT(Recorder::GetWave(), Recorder::GetPos());

        const float* p = Waving::FFTBuffer();

        for (int i = 0; i < 1024; ++i)
        {
            RectF(i, Window::Height(), 1, -Pow(p[i], 0.5f) * 500).draw(HSV(i));
        }

        const int mouseX = Mouse::Pos().x;

        Line({ mouseX + 1, 0 }, { mouseX + 1, Window::Height() }).draw();

        const String freq = Format(DecimalPlace(1), Waving::FFTResolution(11025)* mouseX, L"Hz");

        font(freq).draw(Mouse::Pos() - Point{ 0, 50 });

        font(timerSec.elapsed()).draw(0, 0);
    }
}

ただし,これの最大の欠点が,メモリを多く取ってしまうことになります.
実行中のプログラムのメモリ使用量は,10秒間だと20MBほどですが,5分になると70MBぐらいまで増えます.
最近の環境だと気にならないほどなのですが,1分10MBぐらい食ってしまうことは覚えておいたほうがいいかもしれません.

FFTのSampleLengthに注意する

Waving::FFTBuffer()では,const float*という形で,FFTの結果を受け取ることができます.

一応,関数を触った時のアノテーションでは,const float*の長さは,sampleLength/2ということになっています.
ここでのsampleLengthとは,サンプリング周波数のことではありません.
FFTのサンプル数のことを表しています.
なので,うっかりサンプリング周波数/2のつもりでFFTを使ってしまうと,添字が範囲外に出てしまいます.

結論の方を先に書いてしまうと,

// SampleLength::Defaultのサンプル数は8192です.
Waving::FFT(Waving::GetWave(), Waving::GetPos(), Waving::SampleLength::Default);
const float* p = Waving::FFTBuffer();
for (int i = 0; i < 8192 / 2; ++i) font(p[i]).draw();  // ここは適当

と書かないと,落ちることがあるので注意しましょう.

FFTの周波数分解能と時間分解能

サンプリング定理によると,サンプリング周波数はスペクトル周波数の2倍ないと,元の信号を復元できないという特性があります.
前の章ではうっかりFFTのサンプル数とサンプリング定理の話を混同してしまって,そこがハマりどころだったのですが,今度は周波数分解能と時間分解能の話です.

FFTの結果はサンプル数/2という話でしたが,実は周波数分解能とは,サンプリング周波数/(サンプル数/2)の関係にあります.
式を簡略化すると,2サンプリング周波数/サンプル数の関係です.
ある周波数スペクトルが表している,周波数スペクトルの範囲が,周波数分解能ということになります.

この周波数分解能を上げたければ,単純にサンプル数を増やすだけでいいです.
サンプル数を増やすと周波数分解能が上がる=周波数スペクトルを細かく取れるようになります.

一方で,サンプル数を増やすことによる弊害ももちろんあります.
それが時間分解能が下がってしまう,という問題です.

時間分解能はとりあえず,下の画像を見るとわかりやすいです.

サンプリング周波数は44.1kHzです.
上の画像のサンプル数は8kです.下の画像は2kになっています.

無題.jpg

無題.jpg

上の方はのぺーってしてますが,下の方はかなり細かいです.
サンプル数を少なくしたほうが,かなり具体的に取れていることがわかります.
もちろん,サンプル数をさらに少なくすれば,時間分解能は上がりますが,今度は周波数分解能が下がってしまいます.

つまるところ,周波数分解能と時間分解能はトレードオフということですね.
正確な時間変化を取るのであれば,サンプル数を少なくし,詳細なスペクトル周波数を調べたいのであれば,サンプル数を多くすることです.

ちなみに,サンプリング周波数を小さくすると取れる周波数スペクトルの最大値も変わってきます.
周波数スペクトルが5000Hz程度であれば,サンプリング周波数は11025Hzぐらいで十分です.
一方で,高周波も調べたいとなると,サンプリング周波数は44100Hzぐらい必要になります.

結論としては,FFTを使う場合,目的に応じて,サンプリング周波数,時間分解能,周波数分解能のトレードオフが大事ということです.

GRGSIBERIA
なんでもやる人.元未踏クリエータ.三次元幾何学と音響工学を少々.
http://www.grgsiberia.net/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away