【JUCEプラグイン開発】ディレイエフェクトの効果をDAWのテンポと同期する

パンダさん

やっぱ、ディレイといえば、BPMに同期したいですね~

コパンダ

前にDAWのテンポ取得していたような

前回、ディレイのパラメータを可変にしました。DAWでよく見るディレイは、BPMとリンクする機能があります。やはり、ディレイは、ディレイタイムのパラメータをDAW側のBPMに同期する機能は必須なきがします。そして、以前にDAWのテンポを取得するような調査を行っていました。DAWのBPMを取得する方法は、以前にこちらの記事で調べましたので、ご参照ください。

ということで、役者がそろっていましたので、早速実装をしてみました。

今回の記事はこちらの記事の続きとなります。

シンプルなディレイエフェクトの実装

パラメータを可変にする

目次

こんな人の役に立つかも

・JUCEを勉強している人

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

・JUCEのディレイでBPMと同期する実装をしたい人

実装の方向性と制限事項

まず、DAWのBPMを取得して、次の式で拍を秒数に変換します。

60 ÷ BPM = 1拍の時間

今回は、シンプルに1拍毎にディレイをするようにしてみました。BPM120のときは、1拍0.5秒、BPM60のときは1拍1秒、BPM30のときは、1拍2秒となります。

今回の実装では、最大のディレイタイムを固定値としているので、setMaxDelayTimeに設定した時間を越えるようなディレイを作ることができません。前回までは、2.0秒を設定していますので、この時間を越えるBPM30のときに、メモリバッファの例外が発生します。そのため、最大のディレイタイムを越えた場合は、値を1.99fなど、バッファを越えない時間に強制的に変更することで、対応しています。

コパンダ

得意のとりあえずのパワープレイです

実装

まずは、DAWのBPMを取得するために、PluginProcessor.hに以下のメンバを準備しました。

PluginProcessor.h

class TheDelayAudioProcessor  : public juce::AudioProcessor
{
//...略...

private:
    //...略...

    //privateなメンバの準備です。
    juce::AudioPlayHead* playHead;
    juce::AudioPlayHead::CurrentPositionInfo currentPositionInfo;
    double current_bpm = 0.0;
    double previous_bpm = 0.0;

    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TheDelayAudioProcessor)
};

current_bpmと、previous_bpmを準備したのは、音声処理で、bpmの変化がない場合、処理を無視できるようにしたかったからです。

PluginProcessor.cpp

続いて、PluginProcessor.cppです。

prepareToPlay関数

void TheDelayAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
    processorChain.prepare({ sampleRate, (juce::uint32) samplesPerBlock, 2 });

    //[1]DAWのBPMを取得します。
    playHead = this->getPlayHead();
    playHead->getCurrentPosition(currentPositionInfo);

    //[2]BPMを変数に保持します。
    current_bpm = currentPositionInfo.bpm;
    previous_bpm = current_bpm;

    //[3]val2とval3を取得した値に変更しました。
    float val2 = (float)60.0 / current_bpm;

    //[3-1]MaxDelayTimeとして設定した値を越えないようにします。
    if (val2 >= 2.0f) {
        val2 = 1.99f;
    }
    //[3-2]左右チャンネルで同じ時間としました。
    float val3 = val2;

    float val4 = *wetLevel;
    float val5 = *feedback;

    processorChain.get<DelayIndex>().setDelayTime(0, val2);
    processorChain.get<DelayIndex>().setDelayTime(1, val3);
    processorChain.get<DelayIndex>().setWetLevel(val4);
    processorChain.get<DelayIndex>().setFeedback(val5);
}

prepareは音声処理の開始時などに実行されますので、ここでもディレイパラメータの設定をしておく必要があります。

ディレイの時間を[1]でBPMから取得して、[2]で時間に変更します。[3]では、以前val2とval3はスライダーの値を取得していたものを、計算で算出したディレイタイムとしました。[3-1]では、先ほどのsetMaxDelayTimeで設定した最大ディレイタイムを上回らないようにしています。[3-2]は、左右チャンネルが同じディレイタイムとなるように値をコピーしました。

processBlock関数

最後に音声処理です。

void TheDelayAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
//...略...

    //[1]DAWのBPMを取得します。
    playHead = this->getPlayHead();
    playHead->getCurrentPosition(currentPositionInfo);

    current_bpm = currentPositionInfo.bpm;

    //[2]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;
    }

    float val4 = *wetLevel;
    float val5 = *feedback;
    processorChain.get<DelayIndex>().setWetLevel(val4);
    processorChain.get<DelayIndex>().setFeedback(val5);
 
    auto block = juce::dsp::AudioBlock<float>(buffer).getSubBlock((size_t)0, (size_t)buffer.getNumSamples());
    auto context = juce::dsp::ProcessContextReplacing<float>(block);
    processorChain.process(context);
}

[1]で、DAWからBPMを取得します。そして、current_bpmに現在のBPMを入れます。

[2]では、previous_bpmと比較して、current_bpmに変化があった場合、val2の値を計算、setDelayTimeでパラメータを変更するようにしました。val2とval3は、前回までスライダーからの値を入力していたものをそのまま流用しています。

課題と考察

BPMを取得する処理をprocessBlock内でやってしまいましたが、本当はTimerを使って一定時間毎に確認するような処理に変更したほうがいいような気がしてきました。フーリエ変換なども、一度音声バッファを音声処理プロセスからコピーして別スレッドで処理するような仕組みにしていましたので、この点は、プログラムの構造を見直した方がいいと思いました。が、現状普通に動作しているので、やる気が起きません 笑

とはいえ、今後、いろいろと機能を追加していく際に、上記の点が問題となってくる可能性もありますので、ここは、タイマーを使ったバージョンがしっかりと動作するかも検証しておきたいなと感じた次第です。

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