【JUCEプラグイン開発】ディレイエフェクトの処理見直し、パラメータの反映をタイマーで行う

パンダさん

音声処理に気を付けたいな、と思いました。

コパンダ

processBlockからタイマー機能に一部を移そう~

※本記事の内容は、環境によって動作していない点が発覚しました、macでビルドした際の不具合について、こちらの記事に備忘録しました。

前回のディレイエフェクトの処理では、processBlock関数の中でパラメータを更新していました。いろいろと考えていたところ、チュートリアルで何度か、音声処理スレッド内で余分な処理を行わないほうが良い、みたいなことを見てきた気がしますので、タイマー処理で行ってみることにしました。juce::Timerで呼び出されるtimerCallback関数は、メインメッセージスレッドで処理されるとのことで、音声処理に影響を与えない形で処理を行うことができるようです。メッセージスレッドは、他に、GUIや、その他データ処理などを行うようなスレッドのようです。

ということで、タイマークラスを利用した構造に変更していきました。

前回の記事はこちらです。

目次

こんな人の役に立つかも

・JUCEを勉強している人

・JUCEでVSTプラグインを作成したい人

・JUCEでディレイを実装したい人

実装

まずは、AundioProcessorクラスにjuce::Timerクラスを継承させます。PluginProcessor.hファイルの〇〇AudioProcessor(〇〇にはプロジェクト名が入ります)に次のように変更しました。

//[1]タイマーを継承しました。
class TheDelayAudioProcessor  : public juce::AudioProcessor, private juce::Timer
{
public:
    //[2]タイマーコールバック関数を追加しました。
    void timerCallback() override
    {
        playHead = this->getPlayHead();
        playHead->getCurrentPosition(currentPositionInfo);

        current_bpm = currentPositionInfo.bpm;

        if (previous_bpm != current_bpm) {
            float val2 = (float)60.0 / current_bpm;

            if (val2 >= 2.0f) {
                val2 = 1.99f;
            }

            float val3 = val2;

            processorChain.get<DelayIndex>().setDelayTime(0, val2);
            processorChain.get<DelayIndex>().setDelayTime(1, val3);

            previous_bpm = current_bpm;
        }
        //DBG("TimerBPM:" << current_bpm);
    }

タイマーは、[1]のように、juce::Timerを継承して利用します。[2]で、publicなメンバにtimerCallback関数をoverrideして使います。

timerCallback内の処理は、前回processBlockで行っていた、DAWからのBPM取得と、BPMが変化しているかどうかのif文での判定処理を行います。タイマーでのメッセージスレッドの処理でDAWのBPMの変化を常に監視するようにしました。

コンストラクタには、タイマーを開始させるプログラムが必要になります。

startTimerHz関数で1秒間に30回実行するようにしました。

TheDelayAudioProcessor::TheDelayAudioProcessor()
//...略...
{
//...略...

    //タイマーコールバックを1秒に30回処理するようにしてスタートします。
    startTimerHz(30);

}

最後に、processBlock関数で条件判定や、BPMを取得してきた部分をコメントアウトしました。

void TheDelayAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
//...略...
    //[1]以下の処理をコメントアウトしました。
    /*playHead = this->getPlayHead();
    playHead->getCurrentPosition(currentPositionInfo);

    current_bpm = currentPositionInfo.bpm;

    if (previous_bpm != current_bpm) {
        float val2 = (float)60.0 / current_bpm;

        if (val2 >= 2.0f) {
            val2 = 1.99f;
        }

        float val3 = val2;

        //DBG("test:" << val2);

        processorChain.get<DelayIndex>().setDelayTime(0, val2);
        processorChain.get<DelayIndex>().setDelayTime(1, val3);

        previous_bpm = current_bpm;
    }*/
//...略...

ちょっとした考察

ptocecssBlock関数は、音声処理ブロック毎に必ず実行させなければいけない、音声の入出力と密接に関係している処理なので、サンプリングレート48000Hzで音声バッファサイズが512サンプル数の場合、約10msecの処理時間となります。(512/48000、1秒間に48000サンプルを512サンプル毎に処理を行う)サンプルバッファが小さくなると、この処理時間がどんどん小さくなってきて、処理しきれなくなると、出力音にノイズが出たりDAWの再生が停止したりします。

JUCEの他のチュートリアルでも見た覚えがありますが、if文は結構オーバーヘッドが大きいみたいで、余り利用しないほうが良いと判断して、今回のように、メッセージスレッドで処理するタイマーを利用しました。

パラメータの監視、反映で音声処理を止めてしまっていては、プラグインとしても元も子もないと思いますので、このようなプログラム構造を取りたいと思います。以前勉強したフーリエ変換、スペクトルアナライザの表示のチュートリアル(公式の順序的には後のチュートリアル^^;)でも、音声バッファをメッセージスレッドに取得してきて、表示関連の計算などを行っていました。

音声処理に如何に負荷を取り除いていくかという点は重要なポイントだと感じています。

また、メッセージスレッドという言葉をJUCE開発で初めて聞いたのですが、チュートリアルをやってきた中での文脈的な意味合いで、

音声処理スレッド:時間を必ず守らないといけない処理、どんなに短い遅れも許されない。

メッセージスレッド:時間が厳密に決まっていない処理、処理が多少遅れても問題がない処理

ととらえるようにしています。

音声処理スレッドに処理の遅れが発生すると、出力音声となる致命的な問題が発生しますが、メッセージスレッドについては、それほど問題とならないような処理が多いです。例えば、今回のディレイタイムのパラメータ更新は、DAWのテンポ変更のタイミングに対して遅れたとしても、人間の反応が追い付かないくらい短い時間なので、聴感上問題がないと思います。ただ、毎回再生する毎に機械レベルでの多少のパラメータ反映タイミングの違いが出てくるという点はあります。これは、同じ曲を何回再生しても毎回若干に違うタイミングになる可能性が残されている、ということです。ただ、若干に違うという時間も、人間にしたら、同じタイミングなので、あえて気にする場合としては、2回音声ファイルとしてバウンスして逆位相で当てたときに完全に消えないときがある可能性がある、みたいな雰囲気です。

よかったらシェアしてね!
目次
閉じる