JUCEプラグイン開発、MIDIのアルペイジエイタープラグインを作成する

ArpMasterと名付けたアルペジオエイターを開発しました。

シンセ○○に名前が近いのは気のせいかな・・・

JUCEプラグインで、MIDIに関するプラグインについて勉強するために、JUCEチュートリアルのアルペジエイターを実装してみました。

https://docs.juce.com/master/tutorial_plugin_examples.html#tutorial_plugin_examples_arpeggiator

本記事では、Projucerのプラグインテンプレートから作成する点と、プラグインのパラメータ管理クラス、AudioProcessorValueTreeState(APVTS)を利用する点を追加して実装を行います。

APVTSでのパラメータ追加については、こちらの記事もご参照ください。

目次

こんな人の役にたつかも

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

・JUCEでアルペイジオエイターを作成したい人

・JUCEプラグインのMIDIの入力バッファについて勉強したい人

ArpMasterの機能

入力されたMIDIノートをアルペジオします。アルペイジオする音符の長さをスピードパラメータでコントロールします。

和音がこのようなアルペジオになります。(テンポシンクなどの機能はありません^^;)

0.0~1.0のスピードパラメータがあります。

プラグインの実装

プロジェクトの作成

Projucerのプラグインテンプレートからプロジェクトを作成しました。プロジェクト名は、今回はArpeggiator01、プラグイン名はArpMasterとしました。(あのシンセで有名なプラグインは意識していませんよ–;) 

今回は、MIDIプラグインということで、プロジェクトの設定項目を追加しています。

スクリーンショットのMIDI関連の項目にチェックを入れることで、MIDIプラグインとして、音声入出力設定が自動的に反映されます。また、この設定を行うことで、Logic pro XでもMIDIFXとして認識されるようでした。

プログラムの実装

プロセッサ側

音声処理を行うプロセッサ側のプログラムです。PluginProcessor.hとPluginProcessor.cppにプログラムを追加します。

PluginProcessor.h

次のprivateなメンバを追加しました。

class Arpeggiator01AudioProcessor  : public juce::AudioProcessor
{
public:
//...略...
private:
    //privateなメンバの追加です。
    //[1]APVTSのパラメータ関連です。
    juce::AudioProcessorValueTreeState parameters;
    std::atomic<float>* speed = nullptr;
    //juce::AudioParameterFloat* speed;
    //[2]MIDIの処理に利用する変数関係です。
    int currentNote, lastNoteValue;
    int time;
    float rate;
    juce::SortedSet<int> notes;
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Arpeggiator01AudioProcessor)
};

[1]は、APVTSクラスのパラメータとして「speed」というパラメータを作成したいので、チュートリアルで利用しているAudioParameterFloat*のspeedの代わりとして追加しました。

[2]は、MIDIの処理をするための各種変数です。SortedSetクラスのオブジェクトであるnotesは、「特定の並べ替えルールに従って一意の int 変数のセットを保持するSortedSetオブジェクト」とチュートリアルで説明されています。

・同じ値を2個入れることはできない。

・入れた値は自動的に並び替えされる

ということらしいです。並び替えは値が小さい順番になるのか、その辺りが調べきれていません^^;今回のアルペジエイターには都合の良い数値管理のクラスという感じでしょうか。

PluginProcessor.cpp

[1]で、APVTSにパラメータspeedを追加して初期化します。

Arpeggiator01AudioProcessor::Arpeggiator01AudioProcessor()
//...略...
    //[1]APVTSへのパラメータの追加です。
    ,parameters(*this, nullptr, juce::Identifier("APVTSTutorial"),
        {
            std::make_unique<juce::AudioParameterFloat>("speed",  // ID
                                                        "Speed",  // name
                                                         0.0f,     // min
                                                         1.0f, // max
                                                         0.8f)// default
        })
{
    //addParameter (speed = new juce::AudioParameterFloat ("speed", "Arpeggiator Speed", 0.0, 1.0, 0.8));
    //[2]パラメータの紐付けです。
    speed = parameters.getRawParameterValue("speed");
}

[2]では、ヘッダで宣言した「speed」へ、APVTSのID名「speed」パラメータを紐付けています。また、今回は、APVTSでパラメータを管理するので、チュートリアルで記載されている行をコメントアウトしています。

次に、prepareToPlayで、各種変数を初期化しました。StoreSetのオブジェクトは、clear関数で中身を空にできます。

void Arpeggiator01AudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
    //追加しました。
    notes.clear();
    currentNote = 0;
    lastNoteValue = -1;
    time = 0;
    rate = static_cast<float> (sampleRate);
}

processBlockは、前半と後半に分割できます。

前半では、MIDIバッファからMIDIイベントを取得します。

後半では、MIDIの発音を制御しています。

まずは、前半です。

[1]では、MIDIエフェクトであるので、音声バッファのチャンネル数が0であることを確認して、そうでない場合アサートを出すようにしています。

[2]では、音声バッファのサンプル数で、このprocessBlockが処理する時間(サンプル数)が何個か取得しています。MIDIプラグインですが、処理する1ブロック分の長さは音声バッファから情報を取得するようです。このサンプル数は、DAWの設定によって可変ですので、音声バッファ1ブロック内のサンプル数を取得するという点は重要です。

void Arpeggiator01AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
    //処理内容を次のように変更しました。
    //[1]MIDIエフェクトなので、音声バッファチャンネルがないことを確認します。
    jassert (buffer.getNumChannels() == 0);                                                        

    //[2]音声バッファのサンプル数からタイミング情報を取得します。
    auto numSamples = buffer.getNumSamples();                                                      

    //[3]ノートのデュレーションをspeedとサンプルレートから計算します。
    auto noteDuration = static_cast<int> (std::ceil (rate * 0.25f * (0.1f + (1.0f - (*speed)))));  

    //[4]MIDIバッファのすべてのイベントに対して処理を行います。
    for (const auto metadata : midiMessages)                                                                
    {
        const auto msg = metadata.getMessage();

        if      (msg.isNoteOn())  notes.add (msg.getNoteNumber());
        else if (msg.isNoteOff()) notes.removeValue (msg.getNoteNumber());
    }

    midiMessages.clear(); 

[3]のノートデュレーションの計算の、std::ceilという関数で少数を整数に切り上げています。speedが1に近いほど短いノートのデュレーションになります。一つの音を鳴らし続ける時間になります。

[4]では、midiBufferとして受け取ったMIDIデータのイベントを一つづつ確認して、noteOnメッセージの時は、notesオブジェクトに「ノートナンバーの値」を追加、noteOffの時は、notesからノートナンバーの値を削除する処理をします。

notesには、ノートオンになっている最中のノートナンバーが蓄積されていきます。

※図では、バッファサイズが512ということにしています。

次に、後半部分のMIDI出力を作成する部分です。

MIDI出力は、今のバッファで発音中のノートのデュレーションが終了して次のノートに遷移するかどうかを判定してMIDI出力を行います。

[1]の条件は、ノートのデュレーションがこのバッファ処理の間に完了するかどうかを判定します。time変数は、前回のバッファブロックの処理時までに経過したnoteDurationの経過サンプル数が格納されています。そのため、条件として、ブロック全体のサンプル数の時間を足して、その長さがnoteDurationより長いか、短いかで今回のバッファブロックの間にノートの発音が終了するかどうかがわかります。

    if ((time + numSamples) >= noteDuration)//[1]ノートの長さがこのバッファブロックで終了するかを判定します。
    {
        //[2]このブロック内の何サンプル目でnoteOffかを算出します。
        auto offset = juce::jmax (0, juce::jmin ((int) (noteDuration - time), numSamples - 1));

        if (lastNoteValue > 0)//[3]鳴らしているノートナンバーがある場合NoteOffします。
        {
            midiMessages.addEvent (juce::MidiMessage::noteOff (1, lastNoteValue), offset);
            lastNoteValue = -1;
        }

        if (notes.size() > 0)//[4]和音の場合、次に鳴らすノートを指定します。
        {
            currentNote = (currentNote + 1) % notes.size();
            lastNoteValue = notes[currentNote];
            midiMessages.addEvent (juce::MidiMessage::noteOn  (1, lastNoteValue, (juce::uint8) 127), offset);
        }

    }

    time = (time + numSamples) % noteDuration;//[1]の条件判定に利用する変数timeを計算します。
}

[1]の条件は、具体的な数値で考えるとわかりやすいです。

例)バッファブロックが256として設定して、noteDurationが500のノートの発音を行う場合

1回目のprocessBlock:

time + numSamples = 0 + 256サンプル = 256サンプル なので、noteDurationの500には到達しません。

剰余演算で、(time + numsamples)%500 = 256なので、timeに256が格納され、timeは256という、経過サンプル数が格納されます。

2回目のprocessBlock:

time + numSamples = 256 + 256サンプル = 512サンプルなので、noteDurationの500を、今回のバッファブロック処理で越えてきます。

剰余演算で、(time + numsamples)%500 = 268サンプルが、次のnoteDurationの経過時間としてtimeに格納されます。

→time変数は、バッファブロックで経過したノートの長さをサンプル数で表現しているものととらえます。

[2]では、ノートオフ信号を発生させるサンプル数をoffsetという変数に求めています。「noteDuration-time」の値は、今回のバッファブロックのノートが終了するサンプル数となります。

[3]の条件は、lastNoteValueが0より多きい場合の処理です。lastNoteValueには、前バッファブロックで鳴らしていたノートナンバーが保持されていますので、今回のバッファブロック処理でこのノートをオフさせます。この条件で、noteOffメッセージをoffsetサンプル数目に送出するMIDIイベントをMIDIバッファに追加しています。ここで、lastNoteValueを-1に設定して初期化しておきます。

[4]では、和音の場合、notesに複数のノートナンバーが格納されていますので、これを条件として、次のノートを鳴らす処理を行います。

currentNote変数は、notesに格納されている数値へのインデックスの役割を担い、1を足して、「%notes.size()」とnotesオブジェクトに格納されている数値で剰余演算を取ることで、「0からサイズ数」までの数値を繰り返すようになります。そして、lastNoteValue変数にnotes[currentNote]を格納することで、次に発音するノートナンバーを得ることができます。このノートナンバーをoffsetのサンプル数からnoteOnにします。

juce::AudioProcessorEditor* Arpeggiator01AudioProcessor::createEditor()
{
    //Editorの引数にparametersを追加しました。
    return new Arpeggiator01AudioProcessorEditor (*this, parameters);
}

createEditor関数でのエディタのオブジェクトの生成に、引数を追加しました。

エディタ側

APVTSのパラメータをGUIのスライダーに紐づけます。

PluginEditor.h

[1]~[4]のプログラムで、APVTSのパラメータの設定を行います。[3]はチュートリアルでのパラメータの追加方法なので、コメントアウトしています。

class Arpeggiator01AudioProcessorEditor  : public juce::AudioProcessorEditor
{
public:
    Arpeggiator01AudioProcessorEditor (Arpeggiator01AudioProcessor&, juce::AudioProcessorValueTreeState& vts);//[1]コンストラクタ引数にvstを追加します。

//...略...
    //[2]アタッチメントオブジェクトをメンバに追加します。
    typedef juce::AudioProcessorValueTreeState::SliderAttachment SliderAttachment;

private:
    //[3]次のコメントアウトは、チュートリアルでのパラメータの追加なので削除します。
    //Arpeggiator01AudioProcessor& audioProcessor;
    
    //[4]privateなメンバを追加しました。
    juce::AudioProcessorValueTreeState& valueTreeState;
    juce::Slider speedSlider;
    std::unique_ptr<SliderAttachment> speedAttachment;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Arpeggiator01AudioProcessorEditor)
};
PluginEditor.cpp

コンストラクタの引数にvtsの初期化を追加、プロセッサ側のオブジェクトpの初期化は削除しました。

[2]はスライダーの初期化です。

Arpeggiator01AudioProcessorEditor::Arpeggiator01AudioProcessorEditor (Arpeggiator01AudioProcessor& p , juce::AudioProcessorValueTreeState& vts)//[1]引数にvstを追加します。
    : AudioProcessorEditor (&p), valueTreeState(vts)//[2]メンバイニシャライザ、vstの初期化と、pの削除です。, audioProcessor (p)
{

    //[2]スライダーを可視化してパラメータに紐付けます。
    addAndMakeVisible(speedSlider);
    speedAttachment.reset(new SliderAttachment(valueTreeState, "speed", speedSlider));
    
    setSize (400, 300);
}

GUIの背景色の塗りつぶしをpaint関数で行います。

void Arpeggiator01AudioProcessorEditor::paint (juce::Graphics& g)
{
    g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
}

resized関数にGUIへのスライダーの配置プログラムを追加します。

void Arpeggiator01AudioProcessorEditor::resized()
{
    speedSlider.setBounds(10, 10, 200, 30);
}

音声処理の部分は意外と複雑でした。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次