チュートリアルはスタンドアロンのアプリばかりなんですよね
プラグインの枠組みに機能実装するために少し慣れる必要があるね
JUCEプログラミング、スペクトログラムを表示するチュートリアルのプログラムをVSTプラグインの形に移植しました。今後、作成していきたいプログラムはプラグイン形式としていきたいので、チュートリアルの内容をプラグインの構造に落とし込んでいくことが重要だと思い、やってみました。JUCEというよりは、C++的なプログラムの構造のほうが重要なので、C++についてもっと知りたくなってきました。
移植前のスペクトログラムを表示するプログラムの公式チュートリアルページはこちらです。
https://docs.juce.com/master/tutorial_simple_fft.html
本ブログでも、↑のチュートリアルを記事としていますので、ご参考ください。
The fast Fourier transform1(フーリエ変換について調査)
The fast Fourier transform2(デモアプリの実装)
The fast Fourier transform3(スペクトログラムの描画処理詳細)
出来上がるプラグイン
スクリーンショットのように、Juce Plug-In HostアプリでVSTプラグインとして立ち上げ、ステレオのマイク入力のスペクトログラムを表示しました。
※Juce Plug-In Hostアプリは、DAWなどがなくてもVSTプラグインを試すことができるJUCEに付属してくるプログラムです。アプリとして利用するために、自分プロジェクトをビルドする必要があります。こちらの記事で準備方法などを記載しておりますので、ご参照ください。
出来上がるプラグインの機能的な考察
プラグインの枠組みには、音声処理スレッドを担当するPluginProcessorと、GUIなどのメッセージスレッドを担当するPluginEditorという二つのプログラム単位に分かれています。機能分割をしつつ、実装していくことでプラグインとして移植することができます。
機能的な切り分けを考えました。
PluginProcessorの音声処理スレッドは、常に止めることができませんので、FFTの音声バッファデータを準備するところまでを担当します。
FFTでの周波数領域への変換処理から、スペクトログラムの描画処理はPluginEditorに実装します。
この2つの処理がお互いに非同期的に動作することで、音声処理スレッドはひたすら音声データを準備し続け、描画処理は描画のタイミングでデータを拾って処理(毎秒60回)するということができます。スタンドアロンのときとスレッド的な動作は同じなのですが、プラグインとすることで、クラス別にプログラムを実装することになるので、EditorクラスからProcessorクラスのFFTデータへのアクセスなど、アクセサを実装したりしてクラス間のデータ参照の機能などを追加する必要がありそうです。
プロジェクトの作成
新規プロジェクトとしてProjucerで「Plugin」のテンプレートから作成し、moduleにDSPモジュールを追加します。DSPモジュールは、次のようにプロジェクトから追加できます。
PluginProcessor
PluginProcessorの機能を実装していきます。クラスとしては、「AudioProcessor」クラスを継承した「〇〇AudioProcessor」クラス(〇〇にはプロジェクト名が入ります。)が初期状態で準備されています。このクラスは、音声バッファデータを取得する「processBlock」関数があります。processBlock関数で音声バッファを取得して、FFTの入力となる「fftData」配列へ音声データを格納します。
PluginProcessor.h
次のようにクラスを定義に追記をしました。
class Test2AudioProcessor : public juce::AudioProcessor
{
public:
//...略...
//[1]次の関数を追加しました。
bool getFFTReady();
void setFFTReady(bool b);
float* getFFTDataPtr();
float getFFTDataSample(int sample);
void pushNextSampleIntoFifo(float sample) noexcept;
//[2]フーリエ変換のための各種定数をメンバとして追加します。
static constexpr auto fftOrder = 10;
static constexpr auto fftSize = 1 << fftOrder;
private:
//[3]privateなメンバも追加します。
std::array<float, fftSize> fifo;
std::array<float, fftSize * 2> fftData;
int fifoIndex = 0;
bool nextFFTBlockReady = false;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Test2AudioProcessor)
};
[1]では、ProcessorEditorからも参照したデータのアクセサを新たに定義しました。pushNextSampleFifoはスタンドアロンのときと同様、そのまま使います。
[2]ここのフーリエ変換のための設定定数もチュートリアルのときと同様です。fftOrderは10なので2^10=1024となります。「constexpr」なので、コンパイル時に定数になります。fftSizeはfftOrder定数の1ビット左シフトなので、2048という定数となります。この2つの定数はpublicなので、ProcessorEditorからも見ることができます。
[3]のprivateなメンバも、チュートリアルと同じように準備しました。「nextFFTBlockReady」変数は、FFTが完了したかどうかのフラグとなるので、ProcessorEditor側から使いたいです。また、fftDataはFFTをするためのデータなので、これもデータの中身をProcessorEditorで使いたいです。そのため、[1]のいくつかのアクセサを定義しました。
※実際、nextFFTBlockReadyフラグは読み取り、書き込みをする関数としてしまったので、privateである必要がなくなっている気がします。この点は、C++などのプログラムの構成などをもう少し勉強して改善していけたらいいなと考えています。
PluginProcessor.cpp
pushNextSampleIntoFifoの追加
pushNextSampleInfoFifo関数は、チュートリアルのときと変化していません。cppファイルに追加します。
void Test2AudioProcessor::pushNextSampleIntoFifo(float sample) noexcept
{
if (fifoIndex == fftSize)
{
if (!nextFFTBlockReady)
{
std::fill(fftData.begin(), fftData.end(), 0.0f);
std::copy(fifo.begin(), fifo.end(), fftData.begin());
nextFFTBlockReady = true;
}
fifoIndex = 0;
}
fifo[(size_t)fifoIndex++] = sample;
}
この関数は、呼び出すと音声バッファデータを順番にfifoに格納していきます。fftSize個に達したときに、nextFFTBlockReadyフラグがfalseなら、fftData配列へとfifoをコピーします。fftで利用するデータ、fftData配列にデータを埋める関数となっています。
processBlock関数
processBlock関数も、[1]の部分へと処理内容を変更します。
void Test2AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
juce::ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();
//[1]以下の処理がデフォルトから変更した点です。
if (buffer.getNumChannels() > 0)
{
auto* channelData = buffer.getReadPointer(0);
for (auto i = 0; i < buffer.getNumSamples(); ++i)
pushNextSampleIntoFifo(channelData[i]);
}
}
[1]の処理で少しプラグイン用に変更しています。スタンドアロンのアプリのとき、getNextAudioBlockがオーディオスレッドとして動作する関数となっていました。引数として、AudioSourceChannelInfoクラスのオブジェクトが渡されていましたが、プラグインでは、AudioBufferクラスのオブジェクトが渡されます。そのため、bufferから直接getNumChannels関数を呼び出すことができます。また、getReadPointer関数や、getnumSamples関数も同様です。
処理内容としては、0チャンネルの音声サンプルをpushNextSampleInfoFifo関数へと渡しています。
追加定義のアクセサ
bool Test2AudioProcessor::getFFTReady(){
return nextFFTBlockReady;
}
void Test2AudioProcessor::setFFTReady(bool b) {
nextFFTBlockReady = b;
}
float* Test2AudioProcessor::getFFTDataPtr() {
return fftData.data();
}
float Test2AudioProcessor::getFFTDataSample(int sample) {
return fftData[sample];
}
nextFFTBlockReadyの値を「getFFTReady」で取得でき、「setFFTReady」で変更できるようにしました。
また、fftDataの配列のポインタを「getFFTDataPtr」で取得でき、fftDataの値を「getFFTDataSample」で取得できるようにしました。
PluginEditor
次に、FFTの計算や、その値を利用してスペクトログラムの描画などを行い、GUIに反映したりするメッセージスレッド(っていっていいのか、以前チュートリアルではこういっていたような)クラスを実装していきます。デフォルトで準備される「Test2AudioProcessorEditor」クラスをカスタマイズしていきます。
PluginEditor.h
class Test2AudioProcessorEditor : public juce::AudioProcessorEditor
, private juce::Timer//[1]Timerを継承します。
{
public:
//...略...
//[2]追加したメンバ関数です。
void timerCallback() override;
void drawNextLineOfSpectrogram();
private:
Test2AudioProcessor& audioProcessor;
//[3]次のprivateなメンバを追加します。
juce::dsp::FFT forwardFFT;
juce::Image spectrogramImage;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Test2AudioProcessorEditor)
};
[1]のように、Timerクラスを継承して、[2]でタイマーのコールバック関数を定義します。drawNextLineOfSpectrogram関数はこちらのクラスに追加します。[3]では、FFTモジュールのオブジェクトと画像のオブジェクトを追加しておきます。
PluginEditor.cpp
コンストラクタ
Test2AudioProcessorEditor::Test2AudioProcessorEditor (Test2AudioProcessor& p)
: AudioProcessorEditor (&p), audioProcessor (p)
//[1]次の2行のメンバイニシャライザを追加しました。
, spectrogramImage(juce::Image::RGB, 512, 512, true)
, forwardFFT(p.fftOrder)
{
//[2]Timerを60Hzでスタートさせます。
startTimerHz(60);
setSize (400, 300);
}
FFTの初期化、画像の初期化、タイマーのスタートなどを行います。
paint関数
void Test2AudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll(juce::Colours::black);
g.setOpacity(1.0f);
g.drawImage(spectrogramImage, getLocalBounds().toFloat());
}
spectrogramImageを描画するように処理します。ここもスタンドアロンのときと同じ内容になります。
timerCallback関数
PluginEditor.cppの一番最後に追加しました。Processorで準備したFFT用の音声バッファを毎秒60回処理します。
void Test2AudioProcessorEditor::timerCallback()
{
auto flg = audioProcessor.getFFTReady();//[1]アクセサで取得しに行きます。
if (flg)
{
drawNextLineOfSpectrogram();
audioProcessor.setFFTReady(false);//[2]アクセサでフラグを設定します。
repaint();
}
}
[1]で、Processor側のnextFFTBlockReadyの状態をみて処理を行うので、アクセサ「getFFTReady」で取得するようにしています。[2]では、FFTのデータを使い終わったことをフラグを変更して通知する必要があるので、「setFFTReady」関数でnextFFTBlockReadyの状態をfalseに戻します。
drawNextLineOfSpectrogram関数
TimerCallbackに続けて、PluginEditor.cppに追記しました。
void Test2AudioProcessorEditor::drawNextLineOfSpectrogram()
{
auto rightHandEdge = spectrogramImage.getWidth() - 1;
auto imageHeight = spectrogramImage.getHeight();
spectrogramImage.moveImageSection(0, 0, 1, 0, rightHandEdge, imageHeight);
//[1]FFTへ渡すfftDataへのアクセス方法を変更しました。
forwardFFT.performFrequencyOnlyForwardTransform(audioProcessor.getFFTDataPtr()/*fftData.data()*/);
auto maxLevel = juce::FloatVectorOperations::findMinAndMax(audioProcessor.getFFTDataPtr()/*fftData.data()*/, audioProcessor.fftSize / 2); //[2]fftDataへのアクセス方法を変更しました。
for (auto y = 1; y < imageHeight; ++y)
{
auto skewedProportionY = 1.0f - std::exp(std::log((float)y / (float)imageHeight) * 0.2f);
auto fftDataIndex = (size_t)juce::jlimit(0, audioProcessor.fftSize / 2, (int)(skewedProportionY * audioProcessor.fftSize / 2));
auto level = juce::jmap(audioProcessor.getFFTDataSample(fftDataIndex)/*fftData[fftDataIndex]*/, 0.0f, juce::jmax(maxLevel.getEnd(), 1e-5f), 0.0f, 1.0f);//[3]fftDataのデータ取得の方法を変更しました。
spectrogramImage.setPixelAt(rightHandEdge, y, juce::Colour::fromHSV(level, 1.0f, level, 1.0f));
}
}
この関数では、スペクトログラムの画像を作成しますが、fftData配列はProcessorのAudioProcessorクラスに定義したメンバなので、アクセサを利用した方法でデータを取得するように変更しています。
変更点としては、[1]のFFTの引数へ与える配列へのポインタを「getFFTDataPtr」、[2]のfindMinAndMaxに与える引数の配列へのポインタも「getFFTDataPtr」、[3]のfftDataの配列内データの取得に「getFFTDataSample」のアクセサを利用するように変更しました。
動作検証の注意
VisualStudioなどのデバッグモードでプログラムを実行すると、初期状態で入力がミュート状態となりました。
オーディオのセッティングで「Mute audio input」のチェックをはずすと「Input」に指定した音声が入力されます。PCデフォルトのマイクだと、ハウリングを発生しやすいので、注意が必要です。