チュートリアル: MIDIデータの作成
このチュートリアルでは、MIDIデータを表現するために使用されるMidiMessageクラスを紹介します。タイムスタンプ付きのMIDIメッセージのバッファを処理するためのMidiBufferクラスも紹介します。
レベル: 初級
プラットフォーム: Windows, macOS, Linux, iOS, Android
クラス: MidiMessage, MidiBuffer, Time, Timer
はじめに
このチュートリアルでは、MIDI全般に精通していることを前提としています。また、JUCEのボタンとスライダーの使用にも精通している必要があります(Tutorial: The Slider classとTutorial: Listeners and Broadcastersを参照してください)。
このチュートリアルのデモプロジェクトをダウンロードしてください: PIP | ZIP。プロジェクトを解凍し、Projucerで最初のヘッダーファイルを開いてください。
この手順でヘルプが必要な場合は、Tutorial: Projucer Part 1: Getting started with the Projucerを参照してください。
デモプロジェクト
このデモプロジェクトは、MIDIチャンネル10でMIDIメッセージを作成するための4つのボタンを表示します。これらのボタンは、標準の(General MIDI)ドラムサウンドの4つのノートオンメッセージを作成します: バスドラム、スネアドラム、クローズドハイハット、オープンハイハット。また、ボリュームコントローラーメッセージ(連続コントローラー7)を作成するスライダーもあります。インターフェイスは次のスクリーンショットに示されています。

右側のパネルには、生成されたMIDIメッセージのリストがタイムスタンプ(アプリケーションが起動されたときからの相対時間)とともに表示されます。
アプリケーションはMIDIデータを送信したり、音を出したりしません。MIDIデータを表示するだけです。
MidiMessageクラス
このチュートリアルでは、いくつかのMIDIメッセージタイプを作成するために必要なコードを示します。また、ほとんどのMIDIメッセージタイプを解析するためのコードも含まれています。一般的に、MidiMessageクラスには、MidiMessageオブジェクトを作成するためのstaticメンバー関数の範囲が含まれています(たとえば、ノートオンメッセージを作成するためのMidiMessage::noteOn()関数)。また、MidiMessageオブジェクトをクエリおよびアクセスするためのメンバー関数の範囲もあります(たとえば、MidiMessage::isNoteOn()およびMidiMessage::getNoteNumber()関数)。
MidiMessageオブジェクトの作成
MidiMessageクラスの公開静的メンバー関数を見てください。これは、さまざまなタイプのMIDIメッセージを作成するためのすべての関数をリストしています。個々のバイトまたは生データからMidiMessageオブジェクトを作成することもできますが、これらはMIDI仕様に従って有効なMIDIメッセージでなければなりません。(デバッグビルドで無効なMidiMessageオブジェクトを作成すると、アサーションが生成されます。)
MidiMessageオブジェクトは、通常、ローカル変数またはメンバー変数として格納し、値で渡す必要があります。
ノートオンメッセージを作成するには、MidiMessage::noteOn()関数を使用します。これには、MIDIチャンネル(1 .. 16で番号付け)、ノート番号(0 .. 127)、およびベロシティ(uint8値0 .. 127)が必要です。または、ベロシティをfloat値として表現することもでき、内部的に0 .. 127に変換されます(最も近い整数に丸められます)。
ゼロベロシティのノートオンは実際にはノートオフメッセージであるため、ノートオンベロシティは1 .. 127の範囲です(これにより、ノートオンの最小浮動小数点ベロシティは約0.004fになります)。また、ノートオフベロシティを指定できるMidiMessage::noteOff()関数もあります(一部のシンセサイザーで認識されます)。
デモプロジェクトでは、ベロシティ100のノートオンメッセージを作成し、クリックされたボタンに応じて異なるノート番号を設定します:
void setNoteNumber (int noteNumber)
{
auto message = juce::MidiMessage::noteOn (midiChannel, noteNumber, (juce::uint8) 100);
message.setTimeStamp (juce::Time::getMillisecondCounterHiRes() * 0.001 - startTime);
addMessageToList (message);
}
noteNumber値はボタンの1つによって設定され、setNoteNumber()関数に渡されることに注意してください。また、値100をuint8型にキャストする必要があります。これを行わないと、MidiMessage::noteOn()関数のどのバージョンを呼び出すべきかについて、コンパイラの曖昧さが生じます。
MidiMessageのタイムスタンプを設定することはオプションですが、イベントが生成または受信された時刻を追跡するのに非常に便利です。デフォルトのタイムスタンプはゼロであり、タイムスタンプの時間単位は定義されていません。一般的に、使用する時間単位を決定するのはアプリケーション次第です。このシンプルなケースでは、Time::getMillisecondCounterHiRes()関数を使用して現在の時刻を取得し、0.001を掛けることで、単位として秒を使用しています(アプリケーションが開始された時刻を引いて、その時点からの相対時間にしています)。
ボリュームスライダーは、連続コントローラー(CC)メッセージを作成するために使用されます。CC7は、ボリュームコントロール変更メッセージです:
volumeSlider.onValueChange = [this] {
auto message = juce::MidiMessage::controllerEvent (midiChannel, 7, (int) volumeSlider.getValue());
message.setTimeStamp (juce::Time::getMillisecondCounterHiRes() * 0.001 - startTime);
addMessageToList (message);
};
MidiMessageオブジェクトの解析
addMessageToList()関数は、タイムスタンプとMIDIメッセージを解析して、インターフェイスのメッセージのリストに表示できるようにします:
void addMessageToList (const juce::MidiMessage& message)
{
auto time = message.getTimeStamp();
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);
logMessage (timecode + " - " + getMidiMessageDescription (message));
}
getMidiMessageDescription()関数は、実際にMIDIデータを解析して、メッセージの人間が読める説明を取得します。
static juce::String getMidiMessageDescription (const juce::MidiMessage& m)
{
if (m.isNoteOn())
return "Note on " + juce::MidiMessage::getMidiNoteName (m.getNoteNumber(), true, true, 3);
if (m.isNoteOff())
return "Note off " + juce::MidiMessage::getMidiNoteName (m.getNoteNumber(), true, true, 3);
if (m.isProgramChange())
return "Program change " + juce::String (m.getProgramChangeNumber());
if (m.isPitchWheel())
return "Pitch wheel " + juce::String (m.getPitchWheelValue());
if (m.isAftertouch())
return "After touch " + juce::MidiMessage::getMidiNoteName (m.getNoteNumber(), true, true, 3) + ": " + juce::String (m.getAfterTouchValue());
if (m.isChannelPressure())
return "Channel pressure " + juce::String (m.getChannelPressureValue());
if (m.isAllNotesOff())
return "All notes off";
if (m.isAllSoundOff())
return "All sound off";
if (m.isMetaEvent())
return "Meta event";
if (m.isController())
{
juce::String name (juce::MidiMessage::getControllerName (m.getControllerNumber()));
if (name.isEmpty())
name = "[" + juce::String (m.getControllerNumber()) + "]";
return "Controller " + name + ": " + juce::String (m.getControllerValue());
}
return juce::String::toHexString (m.getRawData(), m.getRawDataSize());
}
同じ機能は、メンバー関数[MidiMessage::getDescription()](https://docs.juce.com/master/classMidiMessage.html#a868d95a096fad999de5ba11f9a2f6340 "Returns a human-readable description of the midi message as a string, for example "Note On C#3 Veloci...")を通じてすでに利用可能です。ここでは既製の実装を使用せず、さまざまなタイプのMIDIメッセージを操作する方法を説明するために自分で実装します。
この関数は、すべてのタイプのMIDIメッセージを解析しようとします(これまでにノートオンとコントローラーメッセージの作成しか見ていませんが)。ここで、MidiMessageオブジェクトのデータにアクセスする推奨方法を確認できます:
- MIDIメッセージのタイプを判断します(「is」で始まる関数の1つを使用)。次に
- そのタイプのMIDIメッセージにアクセスするための適切な関数を使用します。
メッセージがシステムメッセージ(たとえば、システムエクスクルーシブ)である場合にのみ、この関数の最後の行に到達します。MidiMessage::getRawData()を使用して任意のメッセージの生データにアクセスできますが、通常、ほとんどの目的では、組み込み関数の範囲を使用する方が簡単(そしてより読みやすい)です。
間違ったタイプのメッセージのMidiMessageのデータにアクセスするために関数を使用すると、エラーが発生します。たとえば、MidiMessage::getNoteNumber()関数は、任意のMidiMessageオブジェクトから値を返しますが、これはメッセージがノートオンまたはノートオフメッセージのいずれかであることを確認するものではありません。関数MidiMessage::isNoteOn()、MidiMessage::isNoteOff()、またはMidiMessage::isNoteOnOrOff()のいずれかで最初にチェックする必要があります。
演習: getMidiMessageDescription()関数を変更して、ノートオンメッセージのベロシティをリストします。使用する関数を見つけるには、APIリファレンスを確認してください。
MidiBufferクラス
デモアプリケーションの1つの問題は、ノートオフメッセージを作成しないことです。パーカッション音用のMIDIメッセージを作成しているだけなので、これは大きな問題のようには見えません。しかし、対応するノートオンメッセージのノートオフメッセージを作成しないのは悪い習慣です(持続音では、_スタック_したノートにつながります)。
setNoteNumber()関数でノートオンの直後にノートオフを追加することができます:
auto message = juce::MidiMessage::noteOn (1, noteNumber, (uint8) 100);
message.setTimeStamp (juce::Time::getMillisecondCounterHiRes() * 0.001 - startTime);
addMessageToList (message);
auto messageOff = juce::MidiMessage::noteOff (message.getChannel(), message.getNoteNumber());
messageOff.setTimeStamp (juce::Time::getMillisecondCounterHiRes() * 0.001 - startTime);
addMessageToList (messageOff);
ノートオフメッセージのタイムスタンプを変更することもできます(たとえば、ノートオンメッセージの0.1秒後)が、これはメッセージがリストに投稿されるタイミングを変更しません:
auto message = juce::MidiMessage::noteOn (1, noteNumber, (uint8) 100);
message.setTimeStamp (juce::Time::getMillisecondCounterHiRes() * 0.001 - startTime);
addMessageToList (message);
auto messageOff = juce::MidiMessage::noteOff (message.getChannel(), message.getNoteNumber());
messageOff.setTimeStamp (message.getTimeStamp() + 0.1);
addMessageToList (messageOff);
MidiBufferクラスは、タイムスタンプに基づいてMIDIメッセージのバッファを反復処理するための関数を提供します。これを説明するために、特定のタイムスタンプを持つMidiMessageオブジェクトをMidiBufferオブジェクトに追加するシンプルなスケジューリングシステムをセットアップします。次に、Timerオブジェクトを使用して、MIDIメッセージが配信される予定かどうかを定期的にチェックします。
Timerクラスは、高精度のタイミングには適していません。これは、すべての関数呼び出しを_メッセージスレッド_に保つことで例をシンプルに保つために使用されます。より堅牢なタイミングには、別のスレッドを使用する必要があります(ほとんどの場合、音声スレッドがMidiBufferオブジェクトを音声にレンダリングするのに適しています)。
MainContentComponentクラスにいくつかのメンバーを追加します:
juce::MidiBuffer midiBuffer; // [1]
double sampleRate = 44100.0; // [2]
int previousSampleNumber = 0; // [3]
- [1]: MidiBufferオブジェクト自体。
- [2]: MidiBufferクラスは、MIDIメッセージのタイムスタンプの単位として_サンプル_を使用します。音声を生成していませんが、_サンプルレート_として使用するものを選択する必要があります。このメンバーを使用してサンプルレートを保存します。(一般的な値であるため、44,100を使用します。)
- [3]: MidiBuffer内でどのタイムスタンプにすでに到達したかを追跡する必要があります。このメンバーを使用して、このタイムスタンプをサンプル単位で保存します。
MidiBufferオブジェクトへのMIDIメッセージの追加
MIDIメッセージをメッセージのリストに直接追加する代わりに、MidiBufferオブジェクトに追加します。MidiBuffer::addEvent()関数を呼び出すこの関数を追加します:
void addMessageToBuffer (const juce::MidiMessage& message)
{
auto timestamp = message.getTimeStamp();
auto sampleNumber = (int) (timestamp * sampleRate);
midiBuffer.addEvent (message, sampleNumber);
}
次に、setNoteNumber()関数とSlider::onValueChangeヘルパーオブジェクトを変更して、この関数を使用します。これにより、MIDIメッセージイベントを将来にスケジュールできます:
void setNoteNumber (int noteNumber)
{
auto message = juce::MidiMessage::noteOn (1, noteNumber, (juce::uint8) 100);
message.setTimeStamp (juce::Time::getMillisecondCounterHiRes() * 0.001 - startTime);
addMessageToBuffer (message);
auto messageOff = juce::MidiMessage::noteOff (message.getChannel(), message.getNoteNumber());
messageOff.setTimeStamp (message.getTimeStamp() + 0.1);
addMessageToBuffer (messageOff);
}
volumeSlider.onValueChange = [this] {
auto message = juce::MidiMessage::controllerEvent (10, 7, (int) volumeSlider.getValue());
message.setTimeStamp (juce::Time::getMillisecondCounterHiRes() * 0.001 - startTime);
addMessageToBuffer (message);
};
MidiBufferオブジェクトの反復処理
バッファからメッセージを読み取るには、タイマーを実装する必要があります。Timerクラスを基本クラスとして追加します:
class MainContentComponent : public juce::Component,
private juce::Timer
{
そして、Timer::timerCallback()関数を実装します:
void timerCallback() override
{
auto currentTime = juce::Time::getMillisecondCounterHiRes() * 0.001 - startTime;
auto currentSampleNumber = (int) (currentTime * sampleRate); // [4]
for (const auto metadata : midiBuffer) // [5]
{
if (metadata.samplePosition > currentSampleNumber) // [6]
break;
auto message = metadata.getMessage();
message.setTimeStamp (metadata.samplePosition / sampleRate); // [7]
addMessageToList (message);
}
midiBuffer.clear (previousSampleNumber, currentSampleNumber - previousSampleNumber); // [8]
previousSampleNumber = currentSampleNumber; // [9]
}
- [4]: 現在の時刻をサンプル単位で計算します。
- [5]: バッファ内のメッセージを反復処理します。
- [6]: MidiBufferオブジェクトから最近取得したMIDIメッセージのタイムスタンプが将来の場合、処理を終了し、
while()ループを終了します。 - [7]: 取得したMidiMessageオブジェクトのタイムスタンプは、サンプル番号に基づいたタイムスタンプになります。これを秒ベースのタイムスタンプシステムにリセットして、
addMessageToList()関数を変更せずに動作するようにしましょう。 - [8]: MidiBuffer::clear()関数は、特定の範囲内のタイムスタンプを持つMIDIメッセージをバッファから削除します。これを使用して、処理したばかりのメッセージを削除します。
- [9]: 次回
timerCallback()関数が呼び出されたときに使用するために、この関数が実行された時刻を追跡します。
最後に、MainContentComponentコンストラクタでタイマーを開始する必要があります:
setSize (800, 300);
startTimer (1);
}
これらの変更のコードは、デモプロジェクトのMidiMessageTutorial_02.hファイルにあります。
演習: クラッシュシンバル(ノート番号49)とライドシンバル(ノート番号51)のボタンを追加します。パンニングコントロール(CC10)のスライダーを追加します。resized()関数には、これら3つのコンポーネントを追加するためのスペースが残されています。
まとめ
このチュートリアルでは、MidiMessageクラスとMidiBufferクラスを紹介しました。このチュートリアルを読んだ後、次のことができるようになります:
- 特定のタイプのMidiMessageオブジェクトを作成する --- ノートオン、ノートオフ、連続コントローラー(コントロールチェンジ)など。
- MidiMessageオブジェクトを解析して、そのタイプを発見し、有用なデータを取得する。
- MIDIメッセージをMidiBufferオブジェクトに保存する。
- タイムスタンプに基づいてMidiBufferオブジェクト内のMIDIメッセージを反復処理する。