C++
audio
MIDI
VST
JUCE
JUCEDay 9

[JUCE] ポップアップメニューから MIDI CC Learn してみる

この記事はJUCE Advent Calender 9日目の記事です。

導入

この記事ではMIDIコントローラーと同期させてスライダーとかを動かすMIDI CC(control change) Learnの実装を紹介します。実際にスライダーを右クリックするとポップアップメニューが表示されて、そこからMIDI CC LearnするVSTを作ります。

ここではGUIコンポーネントの説明とかをすっ飛ばすので少し関連の公式チュートリアルを見た後のほうがわかりやすいかもしれません。

JUCEにおけるポップアップメニュー

JUCEにはPopupMenuというクラスがメニューの表示などを行います。このクラス自体はコンポーネントとは別なようで、AudioProcessorEditorクラスに渡したりせず直接表示関数を呼びます。なのでどこからでも呼び出せそうです。

ポップアップメニュー付きスライダーを作る

ここではポップアップメニューを表示させるためにSliderクラスを継承して新しいコンポーネントを作り、メニュー表示機能付きSliderを作成します。ポップアップメニューの結果を通知するためにListenerクラスも新しく作ります。

popup_slider.h
#pragma once

#include <vector>

#include "../JuceLibraryCode/JuceHeader.h"

namespace SynthComponent {
/*==============================================================================
PopupSlider class
- this class is extended slider with popup menu.
==============================================================================*/
class PopupSlider : public juce::Slider {

 public:
  // Midi learn、clearをリスナーに通知するための関数を追加
  class Listener : public juce::Slider::Listener {
   public:
    virtual ~Listener(){};
    virtual void requestMidiLearn(PopupSlider* slider) = 0;
    virtual void requestClearLearn(PopupSlider* slider) = 0;
  };

  enum MenuIds {
    // アイテムの番号
    CANCEL,
    MIDI_LEARN,
    CLEAR_MIDI_LEARN,
  };

  PopupSlider();
  ~PopupSlider();

  // PopupMenuクラス用のリスナーの登録
  void addListener(PopupSlider::Listener* listener);

  // マウスイベントの処理
  void mouseDown(const MouseEvent& e) override;

  // メニューの結果を処理するための関数
  void handlePopupResult(int result);

 private:
  std::vector<PopupSlider::Listener*> listeners_;
  juce::PopupMenu* menu_;  // メニューを追加
};
}

このようにSliderにポップアップメニューを追加するような形で宣言します。右クリックでポップアップメニューを表示したいのでmouseDownをオーバーライドします。

次にcppファイルに具体的な定義を記述していきます。まずはコンストラクタとデストラクタです。

popup_slider.cpp
PopupSlider::PopupSlider() {
  menu_ = new juce::PopupMenu ();
  menu_->clear();
  menu_->addItem(MIDI_LEARN, "Learn MIDI CC", true);
  menu_->addItem(CLEAR_MIDI_LEARN, "Clear MIDI CC Learn", true);
}

PopupSlider::~PopupSlider() {
  delete menu_;
  menu_ = nullptr; 
}

コンストラクタでポップアップメニューを動的に作成します。addItemはメニューの要素を追加するための関数で、引数はこんな感じ。

addItem (int itemResultID,        // アイテムのID
         const String &itemText,  // 表示するテキスト
         bool isEnabled=true,     // 表示するかどうか
         bool isTicked=false)     // trueだと項目の左にチェックマーク

次にaddListenerを定義します。PopupSliderの持つlisteners_にリスナーを追加した後、Sliderに対してもaddListenerを呼びます。値が変更された時(sliderValueChanged)にはSliderクラスのリスナーが使用されます。

popup_slider.cpp
void PopupSlider::addListener(PopupSlider::Listener* listener) {
  listeners_.push_back(listener);
  juce::Slider::addListener(listener);
}

mouseDownでは右クリックに対してポップアップメニューを表示させるよう記述し、それ以外の場合はSliderクラスとして動作させるといいでしょう。

popup_slider.cpp
void PopupSlider::mouseDown(const MouseEvent& e) {
  if (e.mods.isPopupMenu()) {
    // ここで非同期的にメニューを表示
    menu_->showMenuAsync(PopupMenu::Options(),
      ModalCallbackFunction::forComponent(sliderPopupCallback, this));
  } else {
    Slider::mouseDown(e);
  }
}

メニューを表示させる関数にはいくつかありますが、ここではshowMenuAsyncという関数を使います。この関数は非同期的にポップアップメニューを表示させ、その結果を引数としてコールバック関数を呼び出します。コールバック関数はModalCallbackFunction::forComponentを使って、ModalComponentManager::Callbackクラスでラッピングします(ここでは詳細は省く)。コールバック関数はcppファイルに、クラスの外部スコープでstaticをつけて記述します。この関数では結果の例外処理と結果を処理する関数との橋渡し的な役割をします。

popup_slider.cpp
static void sliderPopupCallback(int result, PopupSlider* slider) {
  if (slider != nullptr && result != PopupSlider::CANCEL)
    slider->handlePopupResult(result);
}

最後にsliderPopupCallback()から参照されているhandlePopupResult()を定義します。この関数でやっと結果の分岐を行います。

popup_slider.cpp
void PopupSlider::handlePopupResult(int result) {
  switch (result) {
    case MIDI_LEARN:
      for (auto listener : listeners_) {
        listener->requestMidiLearn(this);
      }
      break;
    case CLEAR_MIDI_LEARN:
      for (auto listener : listeners_) {
        listener->requestClearLearn(this);
      }
      break;
  }
}

これでPopupSliderクラスは完成です。

とりあえず表示してみる

そろそろ飽きてきてとにかく表示してみたいのでPluginEditorをいじっていきます。Projucerで適当にプラグインプロジェクトを作って始めましょう。(ここではMidiLearnTestという名前のプロジェクトで始めています。)

PluginEditor.h
class MidiLearnTestAudioProcessorEditor  : public AudioProcessorEditor
{
// 省略 //
    private:
        // This reference is provided as a quick way for your editor to
        // access the processor object that created it.
        MidiLearnTestAudioProcessor& processor;

        SynthComponent::PopupSlider slider_;  // ←ここ!

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiLearnTestAudioProcessorEditor)
};

自動生成されている部分は適宜省略しています。Editorクラスに先ほど作成したPopupSliderクラスを追加します。

PluginEditor.cpp
MidiLearnTestAudioProcessorEditor::MidiLearnTestAudioProcessorEditor (MidiLearnTestAudioProcessor& p)
    : AudioProcessorEditor (&p), processor (p)
{
    // Make sure that before the constructor has finished, you've set the
    // editor's size to whatever you need it to be.

    slider_.setRange(0.0, 1.0);   // 追加
    addAndMakeVisible(&slider_);  // 追加

    setSize (400, 300);
}

サイズやらは適当に。

PluginEditor.cpp
void MidiLearnTestAudioProcessorEditor::resized()
{
    // This is generally where you'll want to lay out the positions of any
    // subcomponents in your editor..
    slider_.setBounds(100, 125, 200, 50);  // 追加
}

これでポップアップメニューを表示することができました。ここまででビルドしてみるとポップアップメニューが表示できます。まだパラメーターもリスナーもを追加していないのでGUIだけの状態です。次はパラメーターを追加してMIDI CC Learnを実装しましょう。
2017-12-09 (2).png

MIDI CC Learnを実装する

MIDI CC Learnを実装する前に、まずはパラメーターを追加しましょう。パラメーターはAudioParameterFloatクラスを使い、その管理にjuce::Stringをキーとするstd::mapを使います。こうすることでパラメーターの名前で管理することができます。しかしデメリットもあり、enumと配列による管理に比べて、エディタの入力補完が使えない、名前が違っていた場合は実行時エラーになるという問題があります。

PluginProcessor.h
class MidiLearnTestAudioProcessor  : public AudioProcessor
{
public:
// 省略 //
private:
    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiLearnTestAudioProcessor)

    AudioParameterFloat* slider1_;

    std::map<juce::String, AudioParameterFloat*> params_map_;  // パラメーターの保持 
    std::map<int, AudioParameterFloat*> control_map_;  // CCとパラメーターの対応関係を保持

    AudioParameterFloat* learn_request_;  // リクエスト中のパラメーターを保持
};

忘れずにProcesserの方にパラメーターを通知し、マップにも追加しておきます。

PluginProcessor.cpp
MidiLearnTestAudioProcessor::MidiLearnTestAudioProcessor()
// 省略 //
{
    slider1_ = new AudioParameterFloat("slider1", "slider1", 0.0f, 1.0f, 0.0f);
    addParameter(slider1_);
    params_map_.insert(std::make_pair(slider1_->name, slider1_));
}

次にProcessorクラスにリスナーを継承させ、関数を追加していきます。

PluginProcessor.h
class MidiLearnTestAudioProcessor  : public AudioProcessor,
    public SynthComponent::PopupSlider::Listener  // リスナークラスを追加
{
public:
    // 省略 //

    // パラメーターの受け渡し
    AudioParameterFloat* getParameter(juce::String name);

    // スライダーからの値変更の通知
    void sliderValueChanged(Slider* slider) override;

    // さっきの
    void requestMidiLearn(SynthComponent::PopupSlider* slider) override;
    void requestClearLearn(SynthComponent::PopupSlider* slider) override;

private:
  // 省略 // 
};

Editor側からパラメーターが欲しければAudioProcessor::getParameter(int index)を使いたいところですが、なぜか非推奨になっていて、公式のクラスドキュメントに以下の文が書かれています。

NOTE! This method will eventually be deprecated! It's recommended that you use the AudioProcessorParameter class instead to manage your parameters.

なのでgetParamterを自分で用意しています。せっかくmapで持っているので名前で検索をかけられるようにします。

PluginProcessor.cpp
AudioParameterFloat* MidiLearnTestAudioProcessor::getParameter(juce::String name) {
  return params_map_[name.toStdString()];
}

リスナーからオーバーライドした関数も定義していきます。

PluginProcessor.cpp
void MidiLearnTestAudioProcessor::sliderValueChanged(Slider* slider) {
    *slider1_ = slider->getValue();
}

void MidiLearnTestAudioProcessor::requestMidiLearn(SynthComponent::PopupSlider* slider) {
    learn_request_ = params_map_[slider->getName().toStdString()];
}

void MidiLearnTestAudioProcessor::requestClearLearn(SynthComponent::PopupSlider* slider) {
  auto itr = control_map_.begin();
  while (itr != control_map_.end()) {
    auto pair = *itr;
    if (pair.second->name == slider->getName()) {
      itr = control_map_.erase(itr);
    }
    else {
      itr++;
    }
  }
}

requesutMidiLearnではlearn_request_の上書き、requestClearLearnではCCの番号とパラメーターを関係づけているマップからパラメーターを削除しています。超個人的にはこのwhileを使ったイテレーター回しがかっこいいと思います(小並感)。

最後に実際にMIDIを受け付ける部分を書きます。やーっとprocessBlockに手を付けます。ここもイテレーターの回し方が変なので注意しましょう。このコードの大枠は公式のチュートリアルから取ってきていますのでわからなかったらそちらもご覧ください。

PluginProcessor.cpp
void MidiLearnTestAudioProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages)
{
    // ここでMidi Bufferを読む
    MidiMessage midi_message;
    int sample_ofset;

    for (MidiBuffer::Iterator i(midiMessages); i.getNextEvent(midi_message, sample_ofset);) {
        if (midi_message.isController()) {
            int cc_num = midi_message.getControllerNumber();
            if (control_map_.count(cc_num)) {
                int value = midi_message.getControllerValue();
                if (value < 128)
                    *control_map_[cc_num] = value / 127.0f;
            } else if (learn_request_) {
                control_map_[cc_num] = learn_request_;
                learn_request_ = nullptr;
            }
        }
    }
}

もっといい方法がありそうですが、こんな感じで。大体はエラー処理なので気にしなくていいです。やっていることはマップからCCの番号に対応しているパラメーターを取り出し更新し、learn_request_があれば(nullptrでなければ)マップに登録しています。

これでやっと完成です!!

実際に動かしてみるとMIDIコントローラーに合わせてスライダーが動きます(始めてやると割と感動します)。

↓ビルドする際はgithubから全ソースコード取得して実行してください。
https://github.com/Astellon/MidiLearnTest

終わりに

ここまでで、「ポップアップメニューから MIDI CC Learn」は終了です。長かったでしょうか(多分短い)。私も疲れました…(いやもっと早く書き始めるべきだった)。

それでは良きVSTライフを~♪♪

関連するページ