MidiBufferオブジェクトをどうやって使うか、勉強になります。
MIDIメッセージのスケジューリングができる点が便利だね
JUCEのチュートリアル、前回の続きです。今回の内容は、主にMidiBufferクラスに関する実装がメインになります。このクラスが使えるようになると、開発の幅が広がりそうです。
MIDIの「Create MIDI data」チュートリアルを進めています。このチュートリアルでは、ZIPでダウンロードできる「MidiMessageTutorial_02.h」の内容に相当します。また、チュートリアル内の項目の「The MidiBuffer class」の内容となります。
こんな人の役に立つかも
・JUCEプログラミングを勉強している人
・JUCEの「Create MIDI data」チュートリアルを進めている人
・JUCEでMIDIを扱いたい人
実装の続き
setNoteNumber関数の変更
以下の2点を変更・追加しました。
void MainComponent::setNoteNumber(int noteNumber)
{
auto message = juce::MidiMessage::noteOn(midiChannel, noteNumber, (juce::uint8) 100);
message.setTimeStamp(juce::Time::getMillisecondCounterHiRes() * 0.001 - startTime);
//[1]変更しました。
//addMessageToList (message);
addMessageToBuffer(message);
//[2]ノートオフメッセージを追加します。
auto messageOff = juce::MidiMessage::noteOff(message.getChannel(), message.getNoteNumber());
messageOff.setTimeStamp(message.getTimeStamp() + 0.1);
addMessageToBuffer(messageOff);
}
今までは、[1]の「addMessageToList」で直接MIDIメッセージをGUI右のテキストエディタに表示していました。(※アプリでは、メッセージ表示を発音の代わりとしています。)ここで直接addMessagetoListをするだけだと、この関数が実行されたタイミングでテキスト表示されてしまうので、ノートオンのすぐ後にノートオフが発生します。
これを、後ほど実装するaddMessageToBufferというMidiBufferオブジェクトにMidiメッセージを登録する処理に置き換えました。MidiBufferクラスで、タイムスタンプに応じたMidiメッセージの発生ができるようにすることで、Midiメッセージの時間的な管理ができるようになります。
[2]では、ノートオンメッセージと同様に、ノートオフメッセージを登録します。
setNoteNumber関数を実行することで、「ノートオン→決められた時間後にノートオフ」という一連の流れのMidiメッセージをMidiBufferオブジェクトに登録することができるようになりました。
addMessageToBuffer
cppファイルに、先ほども出てきた、addMessageToBuffer関数を追加します。
//追加しました。
void MainComponent::addMessageToBuffer(const juce::MidiMessage& message)
{
auto timestamp = message.getTimeStamp();
auto sampleNumber = (int)(timestamp * sampleRate);
midiBuffer.addEvent(message, sampleNumber);
}
この関数は、MidiBufferクラスのオブジェクトである「midiBuffer」にMIDIメッセージを登録します。
MidiBufferクラスの関数である「addEvent」を利用してMIDIメッセージを登録するのですが、タイムスタンプの与える単位が、サンプル数なので、サンプルレートを掛け合わせてタイムスタンプの時間をサンプル数単位に変換しています。
timerCallback関数
最後に、TimerCallback関数でMidiBufferから実際にMIDIメッセージを発生させます。
void MainComponent::timerCallback()
{
//[1]現在の経過時間を取得します。
auto currentTime = juce::Time::getMillisecondCounterHiRes() * 0.001 - startTime;
auto currentSampleNumber = (int)(currentTime * sampleRate);
//[2]midiBufferからタイムスタンプ
for (const auto metadata : midiBuffer)
{
// [2-1]
if (metadata.samplePosition > currentSampleNumber)
break;
// [2-2]
auto message = metadata.getMessage();
message.setTimeStamp(metadata.samplePosition / sampleRate);
addMessageToList(message);
}
//[3]
midiBuffer.clear(previousSampleNumber, currentSampleNumber - previousSampleNumber);
previousSampleNumber = currentSampleNumber;
}
[1]の処理では、現在時間を取得しています。「getMillisecondCounterHiRes」関数は、システム起動時からの時間をカウントしていますので、ここからstartTime変数(コンストラクタでアプリ起動時の時間を格納しています。)を引くことで、この処理が実行された時間を取得します。0.001をかけることで秒を単位に変更しています。そして、サンプルレートを掛け合わせて、サンプル数単位へと変換を行います。最終的に、アプリ起動時から、現在の時間がサンプル数として取得できます。
[2]では、C++の範囲forループという方法でループさせています。
このようにしてMidiBufferの中のMIDIメッセージを取り出すことができます。[2-1]では、取り出したMIDIメッセージのタイムスタンプ(サンプル数単位)を取得します。ここで、タイムスタンプが「currentTime」より大きければ、将来のMIDIメッセージとなり、今は何もしません。そのため、breakで処理を終了させます。
先ほどのタイムスタンプが、現在時刻または、それより小さければ、発生させるべきMIDIメッセージとして、[2-2]の処理へ進みます。ここでは、メッセージを取得して、そのタイムスタンプ情報を、サンプル数単位から元の時間単位に戻します。これは、最終的に、MIDIメッセージの発生の代わりになるaddMessageToList関数を変更することなく、メッセージを与えるためです。
[3]では、最後にタイムスタンプが現在をすぎた(処理済みのメッセージ)をMidiBufferからクリアします。
スタートのタイムスタンプと、サンプル数の範囲を指定して、その範囲のMIDIメッセージがmidiBufferからクリアされます。前回の時間から現在の時間までをクリアすれば良いので、「previousSampleNumber」変数が開始サンプルナンバーとなり、「currentSampleNumber – previousSampleNumber」が、クリアするサンプルの区間になります。
そして、currentSampleNumberをpreviousSampleNumberとすることで、次回実行の際の消去のスタートポイントとします。
MIDIメッセージは、MidiBufferクラスを使うと、簡単に時間的な処理もできるようになるんですね。
あとは、アプリ毎に、どこでMIDIを処理するか、考えればいいんだね。タイマーをThreadクラスで使ったり、音声処理内で処理したり、いくつか方法もありそうだね。