【JUCEチュートリアル】Create a string model with delay lines、弦の物理モデリングの実装

パンダさん

ウェーブガイドシンセという方式で弦の振動をモデリングしていくみたいですね。

コパンダ

ウェーブガイドシンセ、仕組み的な部分が全然わからないね~

JUCEチュートリアル、「Create a string model with delay lines」の一番の要所である、弦の物理モデリングの実装を進めています。内容がいきなり高度になりすぎて、かなり模索状態で進めています。今までの実装と違うところは、「事前知識が全くない」という点です(`・ω・´)

物理モデリングの実装の方法、「デジタルウェーブガイドシンセ」は、一番最初の記事で紹介したように、英語に論文があり、自由に閲覧できるのですが、物理などの専門的知識も必要で、私自身、物理とか、全然精通していなく、プログラムを照合しながら、論文を見つつ、雰囲気をつかみながら読み進めるという手法でチュートリアルの実装を追ってみました。

今回は、とにかくWaveguideStringクラスを実装して、音を鳴らしてみたいと思います。

今回の記事は、「Create a string model with delay lines」チュートリアルを進めてきた、前回までの実装の続きとなります。

1.物理モデリングについて調査

2.デモアプリ実装のための準備

3.ディレイライン、リングバッファの実装

JUCE公式チュートリアルページはこちらになります。

目次

こんな人の役に立つかも

・JUCE「Create a string model with delay lines」チュートリアルを進めている人

・Projucerテンプレートで「Create a string model with delay lines」チュートリアルの実装を行なっていきたい人

・JUCEでwaveguide synthの実装をしたい人

こんな音が出力されます

実装完了すると、次のような音になりました。AudioEngineクラスでのディストーション、リバーブ、ディレイなどのエフェクトはオフにしています。

※若干のアタックノイズがあるので、小さめの音量から再生することをお勧めします。

パンダさん

弦をはじいたときの音のような感じですね。

音を鳴らすまでの実装

前回までのProjucerテンプレートにチュートリアルプログラムの移植を行ってきたプログラムに、WaveguideStringクラスを追加します。

WaveguideStringクラスの追加

ダウンロードできる「DSPDelayLineTutorial_01.h」ファイルから、WaveguideStringクラスを抜き出してきて、PluginProcessor.hに貼り付けました。このファイルの段階では、いくつかのプログラムがまだ未実装の段階です。

template <typename Type>
class WaveguideString
{
public:
    //==============================================================================
    WaveguideString()
    {
        setTriggerPosition (Type (0.2));
        setPickupPosition (Type (0.8));
        setDecayTime (Type (0.5));
    }

    //==============================================================================
    void prepare (const juce::dsp::ProcessSpec& spec)
    {
        sampleRateHz = (Type) spec.sampleRate;
        tempBlock = juce::dsp::AudioBlock<float> (heapBlock, spec.numChannels, spec.maximumBlockSize);
        filter.prepare (spec);
        updateParameters();
    }

    //==============================================================================
    void reset() noexcept
    {
        forwardDelayLine .clear();
        backwardDelayLine.clear();
    }

    //==============================================================================
    void setFrequency (Type newValueHz)
    {
        freqHz = newValueHz;
        updateParameters();
    }

    //==============================================================================
    void setPickupPosition (Type newValue)
    {
        jassert (newValue >= Type (0) && newValue <= Type (1));
        pickupPos = newValue;
        updateParameters();
    }

    //==============================================================================
    void setTriggerPosition (Type newValue)
    {
        jassert (newValue >= Type (0) && newValue <= Type (1));
        triggerPos = newValue;
        updateParameters();
    }

    //==============================================================================
    void setDecayTime (Type newValue) noexcept
    {
        jassert (newValue >= Type (0) && newValue <= Type (1));
        decayTime = newValue;
        updateParameters();
    }

    //==============================================================================
    void trigger (Type velocity) noexcept
    {
        jassert (velocity >= Type (0) && velocity <= Type (1));
        ignoreUnused (velocity);
    }

    //==============================================================================
    template <typename ProcessContext>
    void process (const ProcessContext& context) noexcept
    {
        auto&& outBlock = context.getOutputBlock();
        auto numSamples = outBlock.getNumSamples();
        auto* dst = tempBlock.getChannelPointer (0);
        ignoreUnused (outBlock, numSamples, dst);
    }

private:
    //==============================================================================
    DelayLine<Type> forwardDelayLine;
    DelayLine<Type> backwardDelayLine;
    juce::dsp::IIR::Filter<Type> filter;

    juce::HeapBlock<char> heapBlock;
    juce::dsp::AudioBlock<float> tempBlock;

    size_t forwardPickupIndex  { 0 };
    size_t backwardPickupIndex { 0 };
    size_t forwardTriggerIndex { 0 };
    Type decayCoef;

    Type sampleRateHz { Type (1e3) };
    Type freqHz       { Type (1) };
    Type pickupPos    { Type (0) };
    Type triggerPos   { Type (0) };
    Type decayTime    { Type (0) };

    //==============================================================================
    size_t getDelayLineLength() const noexcept
    {
        return forwardDelayLine.size();
    }

    //==============================================================================
    Type processSample() noexcept
    {
        return Type (0);
    }

    //==============================================================================
    void updateParameters()
    {
        reset();
    }
};

現段階では、幾つかの関数は空白となっています。

updateParameters関数

updateParameters関数に以下のプログラムを追記しました。updateParameters関数は、クラスの一番下のprivateなメンバ関数として存在しています。

※jmapはjuce::jmapとしています。チュートリアルとは少し違います。

void updateParameters()
{
    //==ここから追記しました。===
    auto length = (size_t) juce::roundToInt (sampleRateHz / freqHz);
    forwardDelayLine .resize (length);
    backwardDelayLine.resize (length);
 
    forwardPickupIndex  = (size_t) juce::roundToInt (juce::jmap (pickupPos, Type (0), Type (length / 2 - 1)));
    backwardPickupIndex = length - 1 - forwardPickupIndex;
 
    forwardTriggerIndex = (size_t) juce::roundToInt (juce::jmap (triggerPos, Type (0), Type (length / 2 - 1)));
 
    filter.coefficients = juce::dsp::IIR::Coefficients<Type>::makeFirstOrderLowPass (sampleRateHz, 4 * freqHz);
 
    decayCoef = juce::jmap (decayTime, std::pow (Type (0.999), Type (length)), std::pow (Type (0.99999), Type (length)));
    //==ここまでを追記します。 

    reset();
}

updateParameters関数では、「length」、「forwardPickupIndex」、「backwardPickupIndex」、「fowarfTriggerIndex」、「fillter.coefficients」、「decayCoef」といったウェーブガイドシンセで利用するパラメータを更新します。

trigger関数

次に、trigger関数です。MIDIノートオン時に実行される関数です。ignoreUnused関数をコメントアウトして、for分のブロックのプログラムを追記しました。

    void trigger (Type velocity) noexcept
    {
        jassert (velocity >= Type (0) && velocity <= Type (1));
        //ignoreUnused (velocity);
        
        //以下のプログラムを追記します。
        for (size_t i = 0; i <= forwardTriggerIndex; ++i)
        {
            auto value = juce::jmap (Type (i), Type (0), Type (forwardTriggerIndex), Type (0), velocity / 2);
            forwardDelayLine .set (i, value);
            backwardDelayLine.set (getDelayLineLength() - 1 - i, value);
        }
 
        for (size_t i = forwardTriggerIndex; i < getDelayLineLength(); ++i)
        {
            auto value = juce::jmap (Type (i), Type (forwardTriggerIndex), Type (getDelayLineLength() - 1), velocity / 2, Type (0));
            forwardDelayLine .set (i, value);
            backwardDelayLine.set (getDelayLineLength() - 1 - i, value);
        }
    }

trigger関数は、MIDIノートオンとなったタイミングで毎回呼び出される関数で、for文で2つのディレイラインのバッファを埋めるような働きをします。

音声処理に関する関数

processSample関数です。updateParameters関数の上にあるprivateなメンバ関数です。

    Type processSample() noexcept
    {
        //return Type (0);
        //上の一行をコメントアウトして、以下のプログラムを追記しました。
        auto forwardOut  = forwardDelayLine .back();
        auto backwardOut = backwardDelayLine.back();
 
        forwardDelayLine .push (-backwardOut);
        backwardDelayLine.push (-decayCoef * filter.processSample (forwardOut));
 
        return forwardDelayLine.get (forwardPickupIndex) + backwardDelayLine.get (backwardPickupIndex);
    }

process関数です。publicなメンバ関数で一番下にある関数です。ignoreUnusedをコメントアウトして、forのブロックを追加しました。process関数から、↑で実装したprocessSample関数を呼び出しています。

    template <typename ProcessContext>
    void process (const ProcessContext& context) noexcept
    {
        auto&& outBlock = context.getOutputBlock();
        auto numSamples = outBlock.getNumSamples();
        auto* dst = tempBlock.getChannelPointer (0);
        //ignoreUnused (outBlock, numSamples, dst);
        //次のプログラムに変更します。
        for (size_t i = 0; i < numSamples; ++i)
            dst[i] = processSample();
 
        for (size_t ch = 1; ch < tempBlock.getNumChannels(); ++ch)
            juce::FloatVectorOperations::copy (tempBlock.getChannelPointer (ch),
                                               tempBlock.getChannelPointer (0),
                                               (int) numSamples);
 
        outBlock.copyFrom (context.getInputBlock()).add (tempBlock.getSubBlock (0, outBlock.getNumSamples()));
    }

Voiceクラス

Voiceクラスは一音に対する発音処理を行うクラスでした。現状、オシレータ→ゲイン(音量)という処理をおこなっていますので、オシレータの発音から、WaveguideStringクラスによる発音に変更します。

class Voice : public juce::MPESynthesiserVoice
{
//...略...    
    enum
    {
        oscIndex,
        stringIndex,//追加しました。
        masterGainIndex
    };

    //2番目にWaveguideStringクラスを入れました。
    juce::dsp::ProcessorChain<CustomOscillator<float>, WaveguideString<float>, juce::dsp::Gain<float>> processorChain;
};

WaveguideStringクラスのオブジェクトを追加しています。オシレータである、oscIndex(とCustomOscillator)は削除してもいいですが、チュートリアルでも、オシレータクラスのオブジェクトは残したまま、利用しないような形に変更しています。

オシレータの発音をWaveguideStringの発音に変更したいので、noteStarted関数の以下の部分を変更します。

    void noteStarted() override
    {
        auto velocity = getCurrentlyPlayingNote().noteOnVelocity.asUnsignedFloat();
        auto freqHz = (float)getCurrentlyPlayingNote().getFrequencyInHertz();

        processorChain.get<oscIndex>().setFrequency(freqHz, true);

        //processorChain.get<oscIndex>().setLevel(velocity);
        //上の1行をコメントアウトして、以下のプログラムに変更しました。
        auto& stringModel = processorChain.get<stringIndex>();
        stringModel.setFrequency (freqHz);
        stringModel.trigger (velocity);
    }

AudioEngineクラス

AudioEngineクラスで、ディレイエフェクトなどの効果がかかるので、エフェクトをオフにしました。

class AudioEngine : public juce::MPESynthesiser
{
//...略...
    void renderNextSubBlock(juce::AudioBuffer<float>& outputAudio, int startSample, int numSamples) override
    {
        MPESynthesiser::renderNextSubBlock(outputAudio, startSample, numSamples);

        auto block = juce::dsp::AudioBlock<float>(outputAudio).getSubBlock((size_t)startSample, (size_t)numSamples);
        auto context = juce::dsp::ProcessContextReplacing<float>(block);
        //ここをコメントアウトしました。
        //fxChain.process(context);
    }
};

純粋に、ウェーブガイドシンセでの弦のモデリングの音がどのようなものかを確認したかったので、すべてのエフェクトをオフにしましたが、理想的な弦の音、というものは、若干つまらなく感じますね^^;

ディレイなどエフェクトをいれて遊ぶと、より幻想的な雰囲気の音色が作成できて面白いなと感じました。

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