MIDIのノートオフや、MidiBufferについて学びます。
ノートオンだけだとあまり良くないよね〜
JUCEのチュートリアル、MIDIの「Create MIDI data」チュートリアルを進めています。このチュートリアルでは、ZIPでダウンロードできる「MidiMessageTutorial_02.h」の内容に相当します。また、チュートリアル内の項目の「The MidiBuffer class」の内容となります。
また、この記事は、以前のチュートリアルで作成したアプリを元にしています。
こんな人の役に立つかも
・JUCEプログラミングを勉強している人
・JUCEの「Create MIDI data」チュートリアルを進めている人
・JUCEでMIDIを扱いたい人
アプリの改良の概要
前回のアプリでは、MIDIノートオンメッセージを発生させていましたが、ノートオフメッセージを投げていないので、よくありません。ということで、MIDIノートオフを一定の時間後に発生させる、という仕組みを作成していきます。
次の図のように、ノートオンを発生させるボタンを押すと、0.1秒後にノートオフメッセージが発生します。
MidiBufferクラス
MidiBufferクラスを利用して、簡単なMIDIのスケジューリングを行うことができるようです。
タイムスタンプをもつMidiMessageオブジェクトをMidiBufferクラスのオブジェクトに登録して、Timerで一定期間毎に発生させるMIDIメッセージを確認することで実現できます。(今回はタイマーでやっていますが、getNextAudioBlockなど、一定間隔で繰り返すような処理に組み込んだりもできるようです。一定間隔で確認、という点がポイントみたいです。)
プログラムの実装
MainComponent.h
まずはMainComponent.hファイルで、以下の点を変更しました。
//[1]Timerクラスを継承し、Componentクラスの継承に変更しておきます。
class MainComponent : public juce::Component,//: public juce::AudioAppComponent
private juce::Timer
{
public:
MainComponent();
~MainComponent() override;
//[2]今回は、音声処理を消しておきます。
//void prepareToPlay(int samplesPerBlockExpected, double sampleRate) override;
//void getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill) override;
//void releaseResources() override;
void paint(juce::Graphics& g) override;
void resized() override;
private:
static juce::String getMidiMessageDescription(const juce::MidiMessage& m);
void setNoteNumber(int noteNumber);
void logMessage(const juce::String& m);
void addMessageToList(const juce::MidiMessage& message);
//[3]新たに次の2つのメンバ関数を追加しました。
void timerCallback() override;
void addMessageToBuffer(const juce::MidiMessage& message);
juce::TextButton bassDrumButton;
juce::TextButton snareDrumButton;
juce::TextButton closedHiHatButton;
juce::TextButton openHiHatButton;
juce::Label volumeLabel;
juce::Slider volumeSlider;
juce::TextEditor midiMessagesBox;
int midiChannel = 10;
double startTime;
//[4]新たに次の3つのメンバ変数を追加しました。
juce::MidiBuffer midiBuffer; // [4-1]
double sampleRate = 44100.0; // [4-2]
int previousSampleNumber = 0; // [4-3]
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainComponent)
};
[1]では、音声処理を停止するため、ProjucerのGUIテンプレートと同様になるようにComponentクラスの継承に変更しています。Audioのテンプレートから音声処理を削除する点につきましては、次の記事でも記載していますので、ご参照ください。
また、今回は、タイマーを利用するので、Timerクラスの継承も行っておきます。
複数継承すると、カンマを忘れがちなんですよね^^;
[2]では、音声処理を停止するので、音声処理に関する関数をコメントアウトしました。
[3]で、後ほど実装していく今回のメンバ関数を追加しています。
[4]では、今回利用するメンバ変数を定義します。サンプルレート値について、今回は音声処理がないので、44100と決め打ちして利用していくようです。
MainComponent.cpp
cppファイルに加える変更について見ていきます。
音声処理の削除部分
以前のチュートリアルで作成したアプリは、Projucerの「Audio」テンプレートで作成したので、今回は、以下の音声処理の部分をコメントアウトしました。
MainComponent::~MainComponent()
{
//shutdownAudio();
}
/*
void MainComponent::prepareToPlay (int samplesPerBlockExpected, double sampleRate){}
void MainComponent::getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill)
{
bufferToFill.clearActiveBufferRegion();
}
void MainComponent::releaseResources() {}
*/
コンストラクタ
[1]のように、スライダーのコールバック関数の関数をMidiBufferへ値を追加する処理に変更しました。addMessageToBufferは後ほど実装する関数になります。また、[2]で、今回は、タイマーが出てきますので、タイマーを開始させます。
MainComponent::MainComponent()
: startTime(juce::Time::getMillisecondCounterHiRes() * 0.001)
{
//...略...
volumeSlider.onValueChange = [this]
{
auto message = juce::MidiMessage::controllerEvent (10, 7, (int) volumeSlider.getValue());
message.setTimeStamp (juce::Time::getMillisecondCounterHiRes() * 0.001 - startTime);
addMessageToBuffer (message);//[1]変更しました。
};
//...略...
setSize(800, 300);
startTimer(1);//[2]タイマーの初期化を追加しました。
}
タイマーは、1msecで動作させています。また、これはメッセージスレッドで動作させていますので、実際には、あまり精度がよくはないということです。
今回は、チュートリアルをシンプルにするために、このような形でタイマーを使ってMidiBufferオブジェクトを動作させるようです。ほとんどの場合、MIDIを使うと音声も使うことになるので、音声処理スレッド(getNextAudioBlock関数内)で使うとより良い結果になりそうです。
アプリによって、適した方法で一定間隔にMidiBufferの処理を実行する方法を考える必要はありそうですね。
今回はここまでとして、次回も実装の続きを行っていきます。