【JUCEプラグイン開発】シンプルなディレイエフェクトの実装

パンダさん

すごくシンプルにディレイを実装してみました。

コパンダ

色々と応用が利きそうで楽しみだね〜

JUCEチュートリアルの「Create a string model with delay lines」で、リングバッファのディレイラインと、ディレイエフェクトの実装例を学びました。今回は、このディレイの部分を抜き出して、個別のエフェクトとして実装してみたいと思い、Projucerのプラグインテンプレートから、個別に実装してみました。もっともシンプルな実装を実現してみたいと思います。

目次

こんな人の役に立つかも

・JUCEを勉強している人

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

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

こんな音が出ます

Logicのエレピの音源に今回のディレイエフェクトをかけた様子です。適当に弾いています。左チャンネルと、右チャンネルで違うディレイタイムを設定しています。今回、パラメータはプログラム内固定で実装しています。

プロジェクトの準備

Projucerの「basic」テンプレートから作成しました。プロジェクト名は、「TheDelay」としました。以前作成した、シンプルなハイパスフィルターと同様のプロジェクトの準備作業となります。

もちろん、プロジェクトには、DSPモジュールのインポートを忘れずに行っておきました。

実装

必要なクラスの導入

ディレイエフェクトの効果を得るためのDelayLineとDelayというクラスを2つ、ヘッダーに実装します。

DelayLineクラス

JUCEチュートリアル、「Create a string model with delay lines」で実装したDelayLineクラスです。ディレイ、とついていても、イメージ的には、循環して永遠に続くバッファ、のようなイメージです。信号の遅延のために保管するようなバッファなので、ディレイラインと呼んでいるようです。ディレイラインの仕組みについての考察等は、こちらの記事で行いましたので、ご参照ください。

今回は、このDelayLineクラスを利用するので、PluginProcessor.hにコピペしました。(位置はインクルードのすぐ下にコピペしました。)

#pragma once

#include <JuceHeader.h>
//以下のDelayLineクラスをコピペしました。
template <typename Type>
class DelayLine
{
public:
    void clear() noexcept
    {
        std::fill(rawData.begin(), rawData.end(), Type(0));
    }

    size_t size() const noexcept
    {
        return rawData.size();
    }

    void resize(size_t newValue)
    {
        rawData.resize(newValue);
        leastRecentIndex = 0;
    }

    Type back() const noexcept
    {
        return rawData[leastRecentIndex];
    }

    Type get(size_t delayInSamples) const noexcept
    {
        jassert(delayInSamples >= 0 && delayInSamples < size());
        return rawData[(leastRecentIndex + 1 + delayInSamples) % size()];
    }

    /** Set the specified sample in the delay line */
    void set(size_t delayInSamples, Type newValue) noexcept
    {
        jassert(delayInSamples >= 0 && delayInSamples < size());
        rawData[(leastRecentIndex + 1 + delayInSamples) % size()] = newValue;
    }

    /** Adds a new value to the delay line, overwriting the least recently added sample */
    void push(Type valueToAdd) noexcept
    {
        rawData[leastRecentIndex] = valueToAdd;
        leastRecentIndex = leastRecentIndex == 0 ? size() - 1 : leastRecentIndex - 1;
    }

private:
    std::vector<Type> rawData;
    size_t leastRecentIndex = 0;
};

Delayクラス

Delayクラスは、DelayLineクラスの循環バッファを利用して、ディレイエフェクトの効果を付加するようなクラスです。以前の記事もご参照ください。

ディレイエフェクトの実装①、パラメータ編

ディレイエフェクトの実装②、音声処理編

今回は、先ほどのDelayLineクラスの下にコピペしました。

template <typename Type, size_t maxNumChannels = 2>
class Delay
{
public:
    //==============================================================================
    Delay()
    {
        setMaxDelayTime(2.0f);
        setDelayTime(0, 0.7f);
        setDelayTime(1, 0.5f);
        setWetLevel(0.8f);
        setFeedback(0.5f);
    }

    //==============================================================================
    void prepare(const juce::dsp::ProcessSpec& spec)
    {
        jassert(spec.numChannels <= maxNumChannels);
        sampleRate = (Type)spec.sampleRate;
        updateDelayLineSize();
        updateDelayTime();

        filterCoefs = juce::dsp::IIR::Coefficients<Type>::makeFirstOrderHighPass(sampleRate, Type(1e3));

        for (auto& f : filters)
        {
            f.prepare(spec);
            f.coefficients = filterCoefs;
        }
    }

    //==============================================================================
    void reset() noexcept
    {
        for (auto& f : filters)
            f.reset();

        for (auto& dline : delayLines)
            dline.clear();
    }

    //==============================================================================
    size_t getNumChannels() const noexcept
    {
        return delayLines.size();
    }

    //==============================================================================
    void setMaxDelayTime(Type newValue)
    {
        jassert(newValue > Type(0));
        maxDelayTime = newValue;
        updateDelayLineSize();
    }

    //==============================================================================
    void setFeedback(Type newValue) noexcept
    {
        jassert(newValue >= Type(0) && newValue <= Type(1));
        feedback = newValue;
    }

    //==============================================================================
    void setWetLevel(Type newValue) noexcept
    {
        jassert(newValue >= Type(0) && newValue <= Type(1));
        wetLevel = newValue;
    }

    //==============================================================================
    void setDelayTime(size_t channel, Type newValue)
    {
        if (channel >= getNumChannels())
        {
            jassertfalse;
            return;
        }

        jassert(newValue >= Type(0));
        delayTimes[channel] = newValue;

        updateDelayTime();
    }

    //==============================================================================
    template <typename ProcessContext>
    void process(const ProcessContext& context) noexcept
    {
        auto& inputBlock = context.getInputBlock();
        auto& outputBlock = context.getOutputBlock();
        auto numSamples = outputBlock.getNumSamples();
        auto numChannels = outputBlock.getNumChannels();

        jassert(inputBlock.getNumSamples() == numSamples);
        jassert(inputBlock.getNumChannels() == numChannels);

        for (size_t ch = 0; ch < numChannels; ++ch)
        {
            auto* input = inputBlock.getChannelPointer(ch);
            auto* output = outputBlock.getChannelPointer(ch);
            auto& dline = delayLines[ch];
            auto delayTime = delayTimesSample[ch];
            auto& filter = filters[ch];

            for (size_t i = 0; i < numSamples; ++i)
            {
                auto delayedSample = filter.processSample(dline.get(delayTime));
                auto inputSample = input[i];
                auto dlineInputSample = std::tanh(inputSample + feedback * delayedSample);
                dline.push(dlineInputSample);
                auto outputSample = inputSample + wetLevel * delayedSample;
                output[i] = outputSample;
            }
        }
    }

private:
    //==============================================================================
    std::array<DelayLine<Type>, maxNumChannels> delayLines;
    std::array<size_t, maxNumChannels> delayTimesSample;
    std::array<Type, maxNumChannels> delayTimes;
    Type feedback{ Type(0) };
    Type wetLevel{ Type(0) };

    std::array<juce::dsp::IIR::Filter<Type>, maxNumChannels> filters;
    typename juce::dsp::IIR::Coefficients<Type>::Ptr filterCoefs;

    Type sampleRate{ Type(44.1e3) };
    Type maxDelayTime{ Type(2) };

    //==============================================================================
    void updateDelayLineSize()
    {
        auto delayLineSizeSamples = (size_t)std::ceil(maxDelayTime * sampleRate);

        for (auto& dline : delayLines)
            dline.resize(delayLineSizeSamples);
    }

    //==============================================================================
    void updateDelayTime() noexcept
    {
        for (size_t ch = 0; ch < maxNumChannels; ++ch)
            delayTimesSample[ch] = (size_t)juce::roundToInt(delayTimes[ch] * sampleRate);
    }
};

PluginProcessor.h

プラグインのAudioProcessorクラスに、先ほど作成したDelayクラスのインスタンスを定義します。この時、ProcessorChainクラスに含めてDelayクラスのインスタンスを作成することで、他のエフェクト(DSPモジュール)を追加するときに、容易になるようにしました。

class TheDelayAudioProcessor  : public juce::AudioProcessor
{
//...略...
private:
    //以下のenumと、processorChainを追加しました。
    enum
    {
        DelayIndex
    };

    juce::dsp::ProcessorChain<Delay<float>> processorChain;

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

ProcessorChainクラスは、各種DSPを数珠繋ぎにできる便利なクラスです。利用したい、すべてのDSPモジュールに対してprepareしたり、processしたりを個別モジュール毎に記載しなくてよくなります。例えば、この後に音量調整をしたいときは、juce::dsp::Gainモジュールを簡単に追加したりできますので、今回もこのprocessorChainを利用します。

音声処理

PluginProcessorに実装する、音声処理に関する実装です。

prepareToPlay関数

void TheDelayAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
    //以下の内容を追加しました。
    processorChain.prepare({ sampleRate, (juce::uint32) samplesPerBlock, 2 });
}

ProcessorChainクラスのprepareは、「dsp::ProcessSpec」構造体を受けますので、構造体の形にのっとって値を渡します。構造体のメンバは、「double sampleRate」「uint32 maximumBlockSize」「uint32 numChannels」で構成されています。sampleRateは、prepareToPlayの引数をそのまま渡します。maximumBlockSizeは、prepareToPlayの第二引数を渡します。そして、今回はステレオ対応ということで、チャンネル数2を渡します。これらのメンバは、「{}」でくくり、構造体として渡しました。

processBlock関数

音声処理のprocessBlock関数です。[1]以下のプログラムを追加しています。今回は、Delayクラスのprocess内で、チャンネル毎の処理ループを回していますので、もともとあったチャンネル毎のfor文は消しました。

void TheDelayAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
    juce::ScopedNoDenormals noDenormals;
    auto totalNumInputChannels  = getTotalNumInputChannels();
    auto totalNumOutputChannels = getTotalNumOutputChannels();

    for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
        buffer.clear (i, 0, buffer.getNumSamples());

    //[1]以下の内容を追加(デフォルトのチャンネル毎のfor文から変更)しました。
    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);
}

ProcessorChainクラスのprocess関数は、引数に「ProcessContext」の参照を取ります。渡すためのcontextをprocessBlockのAudioBufferから作成します。まずは、blockというAudioBlockクラスのインスタンスを作成します。引数のAudioBufferクラスの参照、bufferに入ってきたデータをAudioBlockに一度格納するというイメージです。getSubBlock関数で、AudioBufferの0番目からサンプル数の最後の数までを取得して、AudioBlockクラスのblockを作成しました。次に、blockをProcessContextReplacing構造体にします。contextという名称のインスタンスに入れて、これをprocess関数への引数とすることで、processorChainに音声バッファを渡すことができました。ここの部分は、チュートリアル 「Create a string model with delay lines」 のAudioEngineクラスの「renderNextSubBlock」関数の渡し方を参考にしました。

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