Handling Midi Eventsのチュートリアルの続きです。「Posting messages to list」から進めていきます。
チュートリアルページはこちらになります。
こんな人の役に立つかも
・JUCEプログラミングを勉強している人
・JUCEの「Handling Midi Events」チュートリアルを進めている人
・JUCEでMIDIを扱いたい人
cppファイル実装の続き
postMessageToList関数
今回のデモアプリでは、MIDIメッセージは最終的に、発音ではなく、テキスト表示する、という出力になります。以前のチュートリアルでも同様にテキスト出力していましたが、今回は、次のようにサブクラスを利用して出力する方法を利用しなければいけません。
void MainComponent::postMessageToList(const juce::MidiMessage& message, const juce::String& source)
{
(new IncomingMessageCallback(this, message, source))->post();
}
この関数では、最初にヘッダに実装した「CallbackMessage」クラスのサブクラス「IncomingMessageCallback」クラスをnewしてpostして利用します。
※以前ヘッダに定義したサブクラスの引用です。↓
class IncomingMessageCallback : public juce::CallbackMessage
{
public:
IncomingMessageCallback (MainContentComponent* o, const juce::MidiMessage& m, const juce::String& s)
: owner (o), message (m), source (s)
{}
void messageCallback() override
{
if (owner != nullptr)
owner->addMessageToList (message, source);
}
Component::SafePointer<MainContentComponent> owner;
juce::MidiMessage message;
juce::String source;
};
MessageCallbackのドキュメントに、は古くなったので、MessageManager::callAsync()を推奨、と書いてあるのが若干気になります。
サブクラス内のaddMessageToListから、以前のテキスト表示の処理と同じ流れになります。このようにサブクラスを挟む理由として、postMessageToList関数がどのスレッドで呼び出されるかわからないからです。
今回のアプリでは次の2つのタイミングで呼び出されます。
①外部MIDIデバイスのMIDI入力を処理するシステムスレッド、handleIncomingMidiMessage関数から。
②KeyboardStateクラスのリスナー、handleNoteOnから呼び出される場合はメッセージスレッドから呼び出されます。
本来、システムスレッド(OS側の処理をしている)には、MIDIのGUI表示を実行するなどの処理は入れるべきではないです、もっと優先度の高いOS関連の処理を行わなければいけません。そのため、このようにMessageCallbackのサブクラスを利用して、メッセージスレッドでMIDIメッセージの出力(今回はテキスト表示)をするようにしています。
ドキュメントを見ると、使いかたが書いてあるので、これ以上中身を追うというよりは、こういう使い方なんだと納得するのが良さそうです。
デストラクタ
アプリ終了時に、しっかり、MidiKeyboardStateクラスのオブジェクトkeyboardStateからリスナーを削除、また、外部MIDIデバイスからのコールバックも削除しておきます。
MainComponent::~MainComponent()
{
//keyboardStateのリスナーを削除します。
keyboardState.removeListener(this);
//外部MIDIデバイスからのコールバックを削除します。
deviceManager.removeMidiInputDeviceCallback(juce::MidiInput::getAvailableDevices()[midiInputList.getSelectedItemIndex()].identifier, this);
}
resized関数
今回のアプリのGUI配置に関する部分です。次のプログラムをresizedにコピーしておきます。
void MainComponent::resized()
{
auto area = getLocalBounds();
midiInputList.setBounds(area.removeFromTop(36).removeFromRight(getWidth() - 150).reduced(8));
keyboardComponent.setBounds(area.removeFromTop(80).reduced(8));
midiMessagesBox.setBounds(area.reduced(8));
}
その他の関数
次の3つの関数は、「create MIDI data」チュートリアルのプログラムと同様になります。cppファイルに追加しておきます。
以下の記事でも、チュートリアルをやりましたので、ご参照ください。
getMidiMessageDescription
この関数は、引数に受け取るMidiMessageが、どんなMIDIメッセージなのかを判定してその内容を文字列に変換する関数です。
MidiMessageクラスには、「getDescription」という同様の機能をもつ関数があります。チュートリアルでは、理解を深めるために、自分で実装をしているとのことでした。
juce::String MainComponent::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());
}
logMessage
引数として受け取った文字列をテキストエディタクラスのオブジェクト「midiMessageBox」にテキストを表示するための関数です。
void MainComponent::logMessage(const juce::String& m)
{
midiMessagesBox.moveCaretToEnd();
midiMessagesBox.insertTextAtCaret(m + juce::newLine);
}
addMessageToList
Midiメッセージの表示文字列を作成する関数です。タイムスタンプなどを付加して文字列にします。最終的に先ほど定義したlogMessage関数で出力します。
void MainComponent::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);
}