ArpMasterと名付けたアルペジオエイターを開発しました。
シンセ○○に名前が近いのは気のせいかな・・・
JUCEプラグインで、MIDIに関するプラグインについて勉強するために、JUCEチュートリアルのアルペジエイターを実装してみました。
https://docs.juce.com/master/tutorial_plugin_examples.html#tutorial_plugin_examples_arpeggiator
本記事では、Projucerのプラグインテンプレートから作成する点と、プラグインのパラメータ管理クラス、AudioProcessorValueTreeState(APVTS)を利用する点を追加して実装を行います。
APVTSでのパラメータ追加については、こちらの記事もご参照ください。
こんな人の役にたつかも
・JUCEでMIDIプラグインを作成したい人
・JUCEでアルペイジオエイターを作成したい人
・JUCEプラグインのMIDIの入力バッファについて勉強したい人
ArpMasterの機能
入力されたMIDIノートをアルペジオします。アルペイジオする音符の長さをスピードパラメータでコントロールします。
和音がこのようなアルペジオになります。(テンポシンクなどの機能はありません^^;)
0.0~1.0のスピードパラメータがあります。
プラグインの実装
プロジェクトの作成
Projucerのプラグインテンプレートからプロジェクトを作成しました。プロジェクト名は、今回はArpeggiator01、プラグイン名はArpMasterとしました。(あのシンセで有名なプラグインは意識していませんよ–;)
今回は、MIDIプラグインということで、プロジェクトの設定項目を追加しています。
スクリーンショットのMIDI関連の項目にチェックを入れることで、MIDIプラグインとして、音声入出力設定が自動的に反映されます。また、この設定を行うことで、Logic pro XでもMIDIFXとして認識されるようでした。
プログラムの実装
プロセッサ側
音声処理を行うプロセッサ側のプログラムです。PluginProcessor.hとPluginProcessor.cppにプログラムを追加します。
PluginProcessor.h
次のprivateなメンバを追加しました。
class Arpeggiator01AudioProcessor : public juce::AudioProcessor
{
public:
//...略...
private:
//privateなメンバの追加です。
//[1]APVTSのパラメータ関連です。
juce::AudioProcessorValueTreeState parameters;
std::atomic<float>* speed = nullptr;
//juce::AudioParameterFloat* speed;
//[2]MIDIの処理に利用する変数関係です。
int currentNote, lastNoteValue;
int time;
float rate;
juce::SortedSet<int> notes;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Arpeggiator01AudioProcessor)
};
[1]は、APVTSクラスのパラメータとして「speed」というパラメータを作成したいので、チュートリアルで利用しているAudioParameterFloat*のspeedの代わりとして追加しました。
[2]は、MIDIの処理をするための各種変数です。SortedSetクラスのオブジェクトであるnotesは、「特定の並べ替えルールに従って一意の int 変数のセットを保持するSortedSetオブジェクト」とチュートリアルで説明されています。
・同じ値を2個入れることはできない。
・入れた値は自動的に並び替えされる
ということらしいです。並び替えは値が小さい順番になるのか、その辺りが調べきれていません^^;今回のアルペジエイターには都合の良い数値管理のクラスという感じでしょうか。
PluginProcessor.cpp
[1]で、APVTSにパラメータspeedを追加して初期化します。
Arpeggiator01AudioProcessor::Arpeggiator01AudioProcessor()
//...略...
//[1]APVTSへのパラメータの追加です。
,parameters(*this, nullptr, juce::Identifier("APVTSTutorial"),
{
std::make_unique<juce::AudioParameterFloat>("speed", // ID
"Speed", // name
0.0f, // min
1.0f, // max
0.8f)// default
})
{
//addParameter (speed = new juce::AudioParameterFloat ("speed", "Arpeggiator Speed", 0.0, 1.0, 0.8));
//[2]パラメータの紐付けです。
speed = parameters.getRawParameterValue("speed");
}
[2]では、ヘッダで宣言した「speed」へ、APVTSのID名「speed」パラメータを紐付けています。また、今回は、APVTSでパラメータを管理するので、チュートリアルで記載されている行をコメントアウトしています。
次に、prepareToPlayで、各種変数を初期化しました。StoreSetのオブジェクトは、clear関数で中身を空にできます。
void Arpeggiator01AudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
//追加しました。
notes.clear();
currentNote = 0;
lastNoteValue = -1;
time = 0;
rate = static_cast<float> (sampleRate);
}
processBlockは、前半と後半に分割できます。
前半では、MIDIバッファからMIDIイベントを取得します。
後半では、MIDIの発音を制御しています。
まずは、前半です。
[1]では、MIDIエフェクトであるので、音声バッファのチャンネル数が0であることを確認して、そうでない場合アサートを出すようにしています。
[2]では、音声バッファのサンプル数で、このprocessBlockが処理する時間(サンプル数)が何個か取得しています。MIDIプラグインですが、処理する1ブロック分の長さは音声バッファから情報を取得するようです。このサンプル数は、DAWの設定によって可変ですので、音声バッファ1ブロック内のサンプル数を取得するという点は重要です。
void Arpeggiator01AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
//処理内容を次のように変更しました。
//[1]MIDIエフェクトなので、音声バッファチャンネルがないことを確認します。
jassert (buffer.getNumChannels() == 0);
//[2]音声バッファのサンプル数からタイミング情報を取得します。
auto numSamples = buffer.getNumSamples();
//[3]ノートのデュレーションをspeedとサンプルレートから計算します。
auto noteDuration = static_cast<int> (std::ceil (rate * 0.25f * (0.1f + (1.0f - (*speed)))));
//[4]MIDIバッファのすべてのイベントに対して処理を行います。
for (const auto metadata : midiMessages)
{
const auto msg = metadata.getMessage();
if (msg.isNoteOn()) notes.add (msg.getNoteNumber());
else if (msg.isNoteOff()) notes.removeValue (msg.getNoteNumber());
}
midiMessages.clear();
[3]のノートデュレーションの計算の、std::ceilという関数で少数を整数に切り上げています。speedが1に近いほど短いノートのデュレーションになります。一つの音を鳴らし続ける時間になります。
[4]では、midiBufferとして受け取ったMIDIデータのイベントを一つづつ確認して、noteOnメッセージの時は、notesオブジェクトに「ノートナンバーの値」を追加、noteOffの時は、notesからノートナンバーの値を削除する処理をします。
notesには、ノートオンになっている最中のノートナンバーが蓄積されていきます。
※図では、バッファサイズが512ということにしています。
次に、後半部分のMIDI出力を作成する部分です。
MIDI出力は、今のバッファで発音中のノートのデュレーションが終了して次のノートに遷移するかどうかを判定してMIDI出力を行います。
[1]の条件は、ノートのデュレーションがこのバッファ処理の間に完了するかどうかを判定します。time変数は、前回のバッファブロックの処理時までに経過したnoteDurationの経過サンプル数が格納されています。そのため、条件として、ブロック全体のサンプル数の時間を足して、その長さがnoteDurationより長いか、短いかで今回のバッファブロックの間にノートの発音が終了するかどうかがわかります。
if ((time + numSamples) >= noteDuration)//[1]ノートの長さがこのバッファブロックで終了するかを判定します。
{
//[2]このブロック内の何サンプル目でnoteOffかを算出します。
auto offset = juce::jmax (0, juce::jmin ((int) (noteDuration - time), numSamples - 1));
if (lastNoteValue > 0)//[3]鳴らしているノートナンバーがある場合NoteOffします。
{
midiMessages.addEvent (juce::MidiMessage::noteOff (1, lastNoteValue), offset);
lastNoteValue = -1;
}
if (notes.size() > 0)//[4]和音の場合、次に鳴らすノートを指定します。
{
currentNote = (currentNote + 1) % notes.size();
lastNoteValue = notes[currentNote];
midiMessages.addEvent (juce::MidiMessage::noteOn (1, lastNoteValue, (juce::uint8) 127), offset);
}
}
time = (time + numSamples) % noteDuration;//[1]の条件判定に利用する変数timeを計算します。
}
[1]の条件は、具体的な数値で考えるとわかりやすいです。
例)バッファブロックが256として設定して、noteDurationが500のノートの発音を行う場合
1回目のprocessBlock:
time + numSamples = 0 + 256サンプル = 256サンプル なので、noteDurationの500には到達しません。
剰余演算で、(time + numsamples)%500 = 256なので、timeに256が格納され、timeは256という、経過サンプル数が格納されます。
2回目のprocessBlock:
time + numSamples = 256 + 256サンプル = 512サンプルなので、noteDurationの500を、今回のバッファブロック処理で越えてきます。
剰余演算で、(time + numsamples)%500 = 268サンプルが、次のnoteDurationの経過時間としてtimeに格納されます。
→time変数は、バッファブロックで経過したノートの長さをサンプル数で表現しているものととらえます。
[2]では、ノートオフ信号を発生させるサンプル数をoffsetという変数に求めています。「noteDuration-time」の値は、今回のバッファブロックのノートが終了するサンプル数となります。
[3]の条件は、lastNoteValueが0より多きい場合の処理です。lastNoteValueには、前バッファブロックで鳴らしていたノートナンバーが保持されていますので、今回のバッファブロック処理でこのノートをオフさせます。この条件で、noteOffメッセージをoffsetサンプル数目に送出するMIDIイベントをMIDIバッファに追加しています。ここで、lastNoteValueを-1に設定して初期化しておきます。
[4]では、和音の場合、notesに複数のノートナンバーが格納されていますので、これを条件として、次のノートを鳴らす処理を行います。
currentNote変数は、notesに格納されている数値へのインデックスの役割を担い、1を足して、「%notes.size()」とnotesオブジェクトに格納されている数値で剰余演算を取ることで、「0からサイズ数」までの数値を繰り返すようになります。そして、lastNoteValue変数にnotes[currentNote]を格納することで、次に発音するノートナンバーを得ることができます。このノートナンバーをoffsetのサンプル数からnoteOnにします。
juce::AudioProcessorEditor* Arpeggiator01AudioProcessor::createEditor()
{
//Editorの引数にparametersを追加しました。
return new Arpeggiator01AudioProcessorEditor (*this, parameters);
}
createEditor関数でのエディタのオブジェクトの生成に、引数を追加しました。
エディタ側
APVTSのパラメータをGUIのスライダーに紐づけます。
PluginEditor.h
[1]~[4]のプログラムで、APVTSのパラメータの設定を行います。[3]はチュートリアルでのパラメータの追加方法なので、コメントアウトしています。
class Arpeggiator01AudioProcessorEditor : public juce::AudioProcessorEditor
{
public:
Arpeggiator01AudioProcessorEditor (Arpeggiator01AudioProcessor&, juce::AudioProcessorValueTreeState& vts);//[1]コンストラクタ引数にvstを追加します。
//...略...
//[2]アタッチメントオブジェクトをメンバに追加します。
typedef juce::AudioProcessorValueTreeState::SliderAttachment SliderAttachment;
private:
//[3]次のコメントアウトは、チュートリアルでのパラメータの追加なので削除します。
//Arpeggiator01AudioProcessor& audioProcessor;
//[4]privateなメンバを追加しました。
juce::AudioProcessorValueTreeState& valueTreeState;
juce::Slider speedSlider;
std::unique_ptr<SliderAttachment> speedAttachment;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Arpeggiator01AudioProcessorEditor)
};
PluginEditor.cpp
コンストラクタの引数にvtsの初期化を追加、プロセッサ側のオブジェクトpの初期化は削除しました。
[2]はスライダーの初期化です。
Arpeggiator01AudioProcessorEditor::Arpeggiator01AudioProcessorEditor (Arpeggiator01AudioProcessor& p , juce::AudioProcessorValueTreeState& vts)//[1]引数にvstを追加します。
: AudioProcessorEditor (&p), valueTreeState(vts)//[2]メンバイニシャライザ、vstの初期化と、pの削除です。, audioProcessor (p)
{
//[2]スライダーを可視化してパラメータに紐付けます。
addAndMakeVisible(speedSlider);
speedAttachment.reset(new SliderAttachment(valueTreeState, "speed", speedSlider));
setSize (400, 300);
}
GUIの背景色の塗りつぶしをpaint関数で行います。
void Arpeggiator01AudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
}
resized関数にGUIへのスライダーの配置プログラムを追加します。
void Arpeggiator01AudioProcessorEditor::resized()
{
speedSlider.setBounds(10, 10, 200, 30);
}
音声処理の部分は意外と複雑でした。