MIDIイベントの処理
このチュートリアルでは、MIDI入力イベントの扱い方を説明します。外部ソースからMIDIデータを渡すことに加えて、オンスクリーンキーボードコンポーネントを紹介します。
レベル:中級
プラットフォーム:Windows, macOS, Linux
クラス: AudioDeviceManager,MidiMessage,MidiInputCallback,ComboBox,MidiKeyboardComponent,MidiKeyboardState,CallbackMessage,ScopedValueSetter
スタート
このチュートリアルのデモ・プロジェクトのダウンロードはこちらから:PIP|ZIP.プロジェクトを解凍し、最初のヘッダーファイルをProjucerで開く。
このステップにヘルプが必要な場合は、以下を参照してください。Tutorial: Projucer Part 1: Getting started with the Projucer.
コンピュータに外部MIDIソースを接続するのが理想的です。そうでない場合は、ある種のバーチャルMIDI音源(コンピューター上にバーチャルMIDIポートを作るもの)があると便利です。
デモ・プロジェクト
このデモ・プロジェクトでは、画面上にMIDIキーボードが表示され、コンボボックスを使ってハードウェア・デバイスのMIDI入力を1つ選択することができます。これらのソースから受信した MIDI イベントはウィンドウの下部に表示されます。これは次のスクリーンショットに示されています:

MIDI入力
このチュートリアルでは、基本的なアプリケーションで MIDI 入力を扱う方法を示します。JUCEは、接続されているハードウェアMIDIインタフェースのリストを簡単に見つけることができます。またMidiKeyboardComponentクラスのメンバ変数を見てみよう。まず、このクラスのメンバ変数を見てみよう。MainContentComponent
クラスである:
juce::AudioDeviceManager deviceManager; // [1]
juce::ComboBox midiInputList; // [2]
juce::Label midiInputListLabel;
int lastInputIndex = 0; // [3]
bool isAddingFromMidiInput = false; // [4]
juce::MidiKeyboardState keyboardState; // [5]
juce::MidiKeyboardComponent keyboardComponent; // [6]
juce::TextEditor midiMessagesBox;
double startTime;
- [1]を使用します。AudioDeviceManagerクラスを使用して、どのMIDI入力デバイスが有効になっているかを調べます。
- [2]MIDI入力デバイスの名前をこのコンボボックスに表示し、ユーザーが選択できるようにします。
- [3]別の入力を選択した際に、以前に選択したMIDI入力の登録を解除するために使用します。
- [4]このフラグは、オンスクリーンキーボードのマウスクリックではなく、外部ソースからMIDIデータが届いていることを示します。
- [5](その)MidiKeyboardStateクラスは、現在どのMIDI鍵盤が押されているかを記録します。
- [6]これはオンスクリーンキーボードコンポーネントです。
の中でMainContentComponent
コンストラクタで初期化します。[3],[4]そして[6].また、MIDIデータのタイムスタンプを相対的に表示できるように、アプリケーションの開始時間も記録します。
MainContentComponent()
: keyboardComponent (keyboardState, juce::MidiKeyboardComponent::horizontalKeyboard),
startTime (juce::Time::getMillisecondCounterHiRes() * 0.001)
{
を通過させなければならない。MidiKeyboardStateオブジェクトを初期化する。MidiKeyboardComponentオブジェクトである。そして、これらは静的に割り当てられたオブジェクトなのでMidiKeyboardStateは、メンバー変数の最初にリストされなければならない。
MIDI入力リスト
MIDI入力のリストを含むコンボボックスは、コンピュータに接続されているMIDI入力のリストをMidiInputクラスでは、MidiInput::getDevices() 関数を使います:
addAndMakeVisible (midiInputList);
midiInputList.setTextWhenNoChoicesAvailable ("No MIDI Inputs Enabled");
auto midiInputs = juce::MidiInput::getAvailableDevices();
juce::StringArray midiInputNames;
for (auto input : midiInputs)
midiInputNames.add (input.name);
midiInputList.addItemList (midiInputNames, 1);
midiInputList.onChange = [this] { setMidiInput (midiInputList.getSelectedItemIndex()); };
// find the first enabled device and use that by default
for (auto input : midiInputs)
{
if (deviceManager.isMidiInputDeviceEnabled (input.identifier))
{
setMidiInput (midiInputs.indexOf (input));
break;
}
}
// if no enabled devices were found just use the first one in the list
if (midiInputList.getSelectedId() == 0)
setMidiInput (0);
ユーザーが選択されたMIDI入力を変更した場合、そのMIDI入力に割り当てられたラムダ関数は、そのMIDI入力に割り当てられたラムダ関数に変更されます。ComboBox::onChangeヘルパー・オブジェクトが呼び出される:
midiInputList.onChange = [this] { setMidiInput (midiInputList.getSelectedItemIndex()); };
についてsetMidiInput()
関数は、アプリケーションが選択されたデバイスのリッスンを開始するようにします。また、そのデバイスが現在無効になっている場合は有効にします:
void setMidiInput (int index)
{
auto list = juce::MidiInput::getAvailableDevices();
deviceManager.removeMidiInputDeviceCallback(list[lastInputIndex].identifier, this);
auto newInput = list[index];
if (! deviceManager.isMidiInputDeviceEnabled (newInput.identifier))
deviceManager.setMidiInputDeviceEnabled (newInput.identifier, true);
deviceManager.addMidiInputDeviceCallback (newInput.identifier, this);
midiInputList.setSelectedId (index + 1, juce::dontSendNotification);
lastInputIndex = index;
}
外部MIDI入力を扱う
を実施する。MidiInputCallback::handleIncomingMidiMessage() pure virtual関数を使う。これによりキーボードの状態が更新され(これによりMidiKeyboardComponentオブジェクト):
void handleIncomingMidiMessage (juce::MidiInput* source, const juce::MidiMessage& message) override
{
const juce::ScopedValueSetter scopedInputFlag (isAddingFromMidiInput, true);
keyboardState.processNextMidiEvent (message);
postMessageToList (message, source->getName());
}
に注目してほしい。scopedInputFlag
変数はScopedValueSetterクラスを作成します。これは次のようなものである:
- の現在の状態を保存する。
isAddingFromMidiInput
メンバーだ。 - を設定する。
isAddingFromMidiInput
メンバーをtrueに設定する。 - 関数が終了すると
isAddingFromMidiInput
メンバーを関数開始時の状態に戻す。
MIDIキーボードの状態とコンポーネント
の中でMainContentComponent
コンストラクタはMidiKeyboardComponentオブジェクトがMainContentComponent
親コンポーネントを可視化する。またMidiKeyboardStateオブジェクト(ないコンポーネント):
addAndMakeVisible (keyboardComponent);
keyboardState.addListener (this);
MidiKeyboardStateListener クラスには、次の 2 つがあります。pure virtual関数を実装しなければならない。これらはMidiKeyboardStateListener::handleNoteOn()そしてMidiKeyboardStateListener::handleNoteOff()の機能がある。
void handleNoteOn (juce::MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override
{
if (! isAddingFromMidiInput)
{
auto m = juce::MidiMessage::noteOn (midiChannel, midiNoteNumber, velocity);
m.setTimeStamp (juce::Time::getMillisecondCounterHiRes() * 0.001);
postMessageToList (m, "On-Screen Keyboard");
}
}
void handleNoteOff (juce::MidiKeyboardState*, int midiChannel, int midiNoteNumber, float /*velocity*/) override
{
if (! isAddingFromMidiInput)
{
auto m = juce::MidiMessage::noteOff (midiChannel, midiNoteNumber);
m.setTimeStamp (juce::Time::getMillisecondCounterHiRes() * 0.001);
postMessageToList (m, "On-Screen Keyboard");
}
}
ここではisAddingFromMidiInput
メンバが使用される。これにより、ハードウェア入力から到着したイベントが複数回リストに投稿されるのを防ぐことができる。
メーリングリストへのメッセージ投稿
についてpostMessageToList()
関数は、最初は少し変わって見えるかもしれない:
void postMessageToList (const juce::MidiMessage& message, const juce::String& source)
{
(new IncomingMessageCallback (this, message, source))->post();
}
についてIncomingMessageCallback
クラスはCallbackMessageクラスを使用する必要がある。どのスレッドからpostMessageToList()
関数が呼び出されます。この関数は、ユーザーがMidiKeyboardComponentオブジェクトから呼び出されます。しかし、データが外部MIDIソースから到着した場合は、バックグラウンドのMIDIスレッド(おそらくオペレーティング・システムのスレッド)から呼び出されます。
についてCallbackMessageクラスは、メッセージ・スレッド上で関数を呼び出す手段を提供します。このCallbackMessageクラスはReferenceCountedObjectクラスに保存されます。そのため、(どうやら)このクラスにはIncomingMessageCallback
オブジェクトはどこにでもある。実際IncomingMessageCallback::post()
関数(これはMessageManager::MessageBase::post()関数)が処理するキューにオブジェクトを追加する。MessageManagerクラスである。そのMessageManagerクラスは最終的にこのオブジェクトをキューで見つけIncomingMessageCallback::messageCallback()
関数を呼び出す。この関数が呼び出されるとIncomingMessageCallback
オブジェクトは削除される。したがって、このオブジェクトの寿命は(ほぼ)自動的に処理される。
これは、データをメッセージスレッドに送る必要があるので、本当に必要なだけです。MIDIアプリケーションでは何らかのスレッド間通信が必要だと思われるが、正確な実装は状況による。
メッセージの表示
についてaddMessageToList()
そしてgetMidiMessageDescription()
の関数と非常によく似ている。Tutorial: Create MIDI data.主な違いは、出典を記録することである。[7]MIDIメッセージ(どのハードウェア入力、またはオンスクリーンキーボード)の
void addMessageToList (const juce::MidiMessage& message, const juce::String& source)
{
auto time = message.getTimeStamp() - startTime;
auto hours = ((int) (time / 3600.0)) % 24;
auto minutes = ((int) (time / 60.0)) % 60;
auto seconds = ((int) time) % 60;
auto millis = ((int) (time * 1000.0)) % 1000;
auto timecode = juce::String::formatted ("%02d:%02d:%02d.%03d",
hours,
minutes,
seconds,
millis);
auto description = getMidiMessageDescription (message);
juce::String midiMessageString (timecode + " - " + description + " (" + source + ")"); // [7]
logMessage (midiMessageString);
}
ユーザー・インターフェースに、モジュレーション・ホイール(CC1)やピッチ・ホイールなどのメッセージを送信し、それに反応するスライダーをいくつか追加する。
概要
このチュートリアルでは、MIDI入力イベントを処理したり表示したりするためのいくつかのクラスを紹介しました。特に以下のことができるようになるはずです:
- 使用可能な MIDI 入力デバイスをリストアップします。
- MIDI入力デバイスのメニューを作成する。
- ハードウェア入力に届くMIDIを聞く。
- を使用してMIDIノートデータを表示します。MidiKeyboardComponentクラスである。
- を使って、他のスレッドからのメッセージをメッセージスレッドに投稿する。CallbackMessageクラスである。