チュートリアル: MIDIシンセサイザーの構築
このチュートリアルでは、MIDI入力に応答するポリフォニックサイン波シンセサイザーを実装します。これは、Synthesiserクラスと関連クラスを利用します。
レベル: 中級
プラットフォーム: Windows, macOS, Linux
クラス: Synthesiser, SynthesiserVoice, SynthesiserSound, AudioSource, MidiMessageCollector
はじめに
JUCEでのMIDI入力の処理とサイン波の生成方法に精通している必要があります。Tutorial: Handling MIDI eventsとTutorial: Build a sine wave synthesiserを参照してください。
このチュートリアルのデモプロジェクトをダウンロードしてください: PIP | ZIP。プロジェクトを解凍し、Projucerで最初のヘッダーファイルを開いてください。
この手順でヘルプが必要な場合は、Tutorial: Projucer Part 1: Getting started with the Projucerを参照してください。
デモプロジェクト
このデモプロジェクトは、シンプルなサイン波シンセサイザーを演奏するために使用できる画面上のキーボードを表示します。

コンピュータキーボードのキー(A、S、D、Fなどのキーを使用して音楽ノートC、D、E、Fなどを制御)を使用して、画面上のキーボードを制御できます。これにより、シンセサイザーをポリフォニックに演奏できます。
Synthesiserクラス
このチュートリアルでは、JUCE Synthesiserクラスを使用してポリフォニックシンセサイザーを実装します。これは、独自のアプリケーションで独自のサウンドでシンセサイザーをカスタマイズするために必要なすべての基本要素を示しています。これを機能させるために必要なさまざまなクラスがあり、標準のMainContentComponentクラスに加えて、これらは次のとおりです:
SynthAudioSource: これは、Synthesiserクラス自体を含む、SynthAudioSourceと呼ばれるカスタムAudioSourceクラスを実装します。これは、シンセサイザーからのすべての音声を出力します。SineWaveVoice: これは、SineWaveVoiceと呼ばれるカスタムSynthesiserVoiceクラスです。ボイスクラスは、Synthesiserオブジェクト内の他の鳴っているボイスとミックスして、シンセサイザーのボイスの1つをレンダリングします。ボイスクラスの単一のインスタンスは、1つのボイスをレンダリングします。SineWaveSound: これには、SineWaveSoundと呼ばれるカスタムSynthesiserSoundクラスが含まれています。サウンドクラスは、ボイスとして作成できるサウンドの説明です。たとえば、これにはサンプラーボイスのサンプルデータやウェーブテーブルシンセサイザーのウェーブテーブルデータが含まれる場合があります。
キーボードのセットアップ
MainContentComponentクラスには、次のデータメンバーが含まれています。
juce::MidiKeyboardState keyboardState;
SynthAudioSource synthAudioSource;
juce::MidiKeyboardComponent keyboardComponent;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};
synthAudioSourceとkeyboardComponentメンバーは、MainContentComponentコンストラクタで初期化されます。
MainContentComponent()
: synthAudioSource (keyboardState),
keyboardComponent (keyboardState, juce::MidiKeyboardComponent::horizontalKeyboard)
{
addAndMakeVisible (keyboardComponent);
setAudioChannels (0, 2);
setSize (600, 160);
startTimer (400);
}
MidiKeyboardComponentクラスの詳細については、Tutorial: Handling MIDI eventsを参照してください。
アプリケーションの開始直後にコンピュータのキーボードからキーボードを演奏できるように、キーボードフォーカスを取得します。これを行うために、400ms後に起動するシンプルなタイマーを使用します:
void timerCallback() override
{
keyboardComponent.grabKeyboardFocus();
stopTimer();
}
AudioAppComponent関数
アプリケーションは、AudioAppComponentを使用してシンプルなオーディオアプリケーションをセットアップします(最も基本的なアプリケーションについては、Tutorial: Build a white noise generatorを参照してください)。必要な3つの純粋仮想関数は、カスタムAudioSourceクラスの対応する関数を呼び出すだけです:
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override
{
synthAudioSource.prepareToPlay (samplesPerBlockExpected, sampleRate);
}
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
{
synthAudioSource.getNextAudioBlock (bufferToFill);
}
void releaseResources() override
{
synthAudioSource.releaseResources();
}
SynthAudioSourceクラス
SynthAudioSourceクラスは、もう少し作業を行います:
class SynthAudioSource : public juce::AudioSource
{
public:
SynthAudioSource (juce::MidiKeyboardState& keyState)
: keyboardState (keyState)
{
for (auto i = 0; i < 4; ++i) // [1]
synth.addVoice (new SineWaveVoice());
synth.addSound (new SineWaveSound()); // [2]
}
void setUsingSineWaveSound()
{
synth.clearSounds();
}
void prepareToPlay (int /*samplesPerBlockExpected*/, double sampleRate) override
{
synth.setCurrentPlaybackSampleRate (sampleRate); // [3]
}
void releaseResources() override {}
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
{
bufferToFill.clearActiveBufferRegion();
juce::MidiBuffer incomingMidi;
keyboardState.processNextMidiBuffer (incomingMidi, bufferToFill.startSample, bufferToFill.numSamples, true); // [4]
synth.renderNextBlock (*bufferToFill.buffer, incomingMidi, bufferToFill.startSample, bufferToFill.numSamples); // [5]
}
private:
juce::MidiKeyboardState& keyboardState;
juce::Synthesiser synth;
};
- [1]: シンセサイザーにいくつかのボイスを追加します。追加されたボイスの数によって、シンセサイザーのポリフォニーが決まります。
- [2]: シンセサイザーが再生できるサウンドを知ることができるように、サウンドを追加します。
- [3]: シンセサイザーは、音声出力のサンプルレートを知る必要があります。
- [4]:
getNextAudioBlock()関数では、MidiKeyboardStateオブジェクトからMIDIデータのバッファを取得します。 - [5]: これらのMIDIのバッファは、ノートオンおよびノートオフメッセージ(および他のMIDIチャンネルボイスメッセージ)のタイムスタンプを使用してボイスをレンダリングするために使用されるシンセサイザーに渡されます。
SynthesiserVoiceオブジェクトは、1つのSynthesiserオブジェクトにのみ追加する必要があります。Synthesiserオブジェクトは、ボイスの寿命を管理します。
SynthesiserSoundオブジェクトは、必要に応じてSynthesiserオブジェクト間で共有できます。SynthesiserSoundクラスは、ReferenceCountedObjectクラスの一種であるため、SynthesiserSoundオブジェクトの寿命は自動的に処理されます。
SynthesiserSoundオブジェクトへのポインタを保持する必要がある場合は、このメモリ管理が機能するように、YourSoundClass::Ptr変数に格納する必要があります。
SineWaveSoundクラス
サウンドクラスは非常にシンプルで、データを含む必要さえありません。特定のMIDIチャンネルとそのチャンネル上の特定のノートまたはノート範囲でこのサウンドを再生する必要があるかどうかを報告するだけです。シンプルなケースでは、appliesToNote()とappliesToChannel()関数の両方に対してtrueを返すだけです。前述のように、サウンドクラスは、サウンドを作成するために必要なデータ(ウェーブテーブルなど)を格納する場所である可能性があります。
struct SineWaveSound : public juce::SynthesiserSound
{
SineWaveSound() {}
bool appliesToNote (int) override { return true; }
bool appliesToChannel (int) override { return true; }
};
サイン波ボイスの状態
SineWaveVoiceクラスはもう少し複雑です。シンセサイザーのボイスの1つの状態を維持する必要があります。サイン波の場合、これらのデータメンバーが必要です:
private:
double currentAngle = 0.0, angleDelta = 0.0, level = 0.0, tailOff = 0.0;
};
最初の3つについては、Tutorial: Build a sine wave synthesiserを参照してください。tailOffメンバーは、各ボイスに振幅エンベロープへのわずかにソフトなリリースを与えるために使用されます。これにより、各ボイスは急激に停止するのではなく、最後に少しフェードアウトします。

再生できるサウンドのチェック
SynthesiserVoice::canPlaySound()関数をオーバーライドして、ボイスがサウンドを再生できるかどうかを返す必要があります。この場合は単にtrueを返すこともできますが、この例では、渡されるサウンドクラスのタイプをチェックするためにdynamic_castを使用する方法を示しています。
bool canPlaySound (juce::SynthesiserSound* sound) override
{
return dynamic_cast<SineWaveSound*> (sound) != nullptr;
}
ボイスの開始
ボイスは、SynthesiserVoice::startNote()関数を呼び出すことによって、所有するシンセサイザーによって開始されます。これをオーバーライドする必要があります:
void startNote (int midiNoteNumber, float velocity, juce::SynthesiserSound*, int /*currentPitchWheelPosition*/) override
{
currentAngle = 0.0;
level = velocity * 0.15;
tailOff = 0.0;
auto cyclesPerSecond = juce::MidiMessage::getMidiNoteInHertz (midiNoteNumber);
auto cyclesPerSample = cyclesPerSecond / getSampleRate();
angleDelta = cyclesPerSample * 2.0 * juce::MathConstants<double>::pi;
}
これも、Tutorial: Build a sine wave synthesiserからよく知っているはずです。tailOff値は、各ボイスの開始時にゼロに設定されます。また、MIDIノートオンイベントのベロシティを使用して、ボイスのレベルを制御します。
ボイスのレンダリング
SynthesiserVoice::renderNextBlock()関数をオーバーライドして、音声を生成する必要があります。
void renderNextBlock (juce::AudioSampleBuffer& outputBuffer, int startSample, int numSamples) override
{
if (angleDelta != 0.0)
{
if (tailOff > 0.0) // [7]
{
while (--numSamples >= 0)
{
auto currentSample = (float) (std::sin (currentAngle) * level * tailOff);
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
outputBuffer.addSample (i, startSample, currentSample);
currentAngle += angleDelta;
++startSample;
tailOff *= 0.99; // [8]
if (tailOff <= 0.005)
{
clearCurrentNote(); // [9]
angleDelta = 0.0;
break;
}
}
}
else
{
while (--numSamples >= 0) // [6]
{
auto currentSample = (float) (std::sin (currentAngle) * level);
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
outputBuffer.addSample (i, startSample, currentSample);
currentAngle += angleDelta;
++startSample;
}
}
}
}
- [6]: このループは、キーが押されている間のボイスの通常の状態で使用されます。AudioSampleBuffer::addSample()関数を使用していることに注意してください。これは、
currentSample値をインデックスstartSampleの値と混合します。これは、シンセサイザーがすべてのボイスを反復処理するためです。各ボイスは、出力をバッファの現在の内容とミックスする責任があります。 - [7]: キーが離されると、
tailOff値はゼロより大きくなります。合成アルゴリズムは似ています。 - [8]: シンプルな指数減衰エンベロープ形状を使用します。
- [9]:
tailOff値が小さい場合、ボイスが終了したと判断します。この時点で、ボイスがリセットされて再利用できるように、SynthesiserVoice::clearCurrentNote()関数を呼び出す必要があります。
startSample引数に注意することは非常に重要です。シンセサイザーは、出力ブロックの途中でrenderNextBlock()関数を呼び出す可能性が非常に高いです。これは、ノートが任意のサンプルで開始される可能性があるためです。これらの開始時間は、受信したMIDIデータのタイムスタンプに基づいています。
ボイスの停止
ボイスは、所有するシンセサイザーがSynthesiserVoice::stopNote()関数を呼び出すことによって停止されます。これをオーバーライドする必要があります:
void stopNote (float /*velocity*/, bool allowTailOff) override
{
if (allowTailOff)
{
if (tailOff == 0.0)
tailOff = 1.0;
}
else
{
clearCurrentNote();
angleDelta = 0.0;
}
}
これには、MIDIノートオフメッセージからのベロシティ情報が含まれる場合がありますが、多くの場合、これを無視できます。すぐにボイスを停止するように求められる場合があります。その場合、すぐにSynthesiserVoice::clearCurrentNote()関数を呼び出します。通常の状況では、シンセサイザーはボイスが自然に終了することを許可します。この場合、シンプルなテールオフエンベロープがあります。tailOffメンバーを1.0に設定することで、テールオフをトリガーします。
演習: ボイスが急激に開始しないように、ボイスにより遅いアタックを追加してみてください。
外部MIDI入力の追加
画面上のキーボードに加えて、外部MIDIソースがシンセサイザーを制御できるように機能を追加しましょう。
おそらく、モバイルプラットフォームではなく、macOS、Windows、Linuxなどのデスクトッププラットフォームのいずれかで試す必要があります。
SynthAudioSourceへのMIDI入力の提供
まず、SynthAudioSourceクラスのメンバーとしてMidiMessageCollectorオブジェクトを追加します。これにより、MIDIメッセージを送信でき、SynthAudioSourceクラスがそれらを使用できる場所が提供されます:
juce::MidiMessageCollector midiCollector;
};
MIDIデータのタイムスタンプを処理するために、MidiMessageCollectorクラスは音声サンプルレートを知る必要があります。これをSynthAudioSource::prepareToPlay()関数で設定します[10]:
void prepareToPlay (int /*samplesPerBlockExpected*/, double sampleRate) override
{
synth.setCurrentPlaybackSampleRate (sampleRate);
midiCollector.reset (sampleRate); // [10]
}
次に、MidiMessageCollector::removeNextBlockOfMessages()関数を使用して、各音声ブロックのMIDIメッセージを取得できます[11]:
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
{
bufferToFill.clearActiveBufferRegion();
juce::MidiBuffer incomingMidi;
midiCollector.removeNextBlockOfMessages (incomingMidi, bufferToFill.numSamples); // [11]
keyboardState.processNextMidiBuffer (incomingMidi, bufferToFill.startSample, bufferToFill.numSamples, true);
synth.renderNextBlock (*bufferToFill.buffer, incomingMidi, bufferToFill.startSample, bufferToFill.numSamples);
}
SynthAudioSourceクラスの外部からこのMidiMessageCollectorオブジェクトにアクセスする必要があるため、次のようにSynthAudioSourceクラスにアクセサを追加します:
juce::MidiMessageCollector* getMidiCollector()
{
return &midiCollector;
}
MainContentComponentクラスでは、このMidiMessageCollectorオブジェクトを、アプリケーションのAudioDeviceManagerオブジェクトにMidiInputCallbackオブジェクトとして追加します。
MIDI入力のリスト表示
ユーザーにMIDI入力デバイスのリストを表示するために、Tutorial: Handling MIDI eventsのコードを使用します。MainContentComponentクラスにいくつかのメンバーを追加します:
juce::ComboBox midiInputList;
juce::Label midiInputListLabel;
int lastInputIndex = 0;
次に、次のコードをMainContentComponentコンストラクタに追加します。
addAndMakeVisible (midiInputListLabel);
midiInputListLabel.setText ("MIDI Input:", juce::dontSendNotification);
midiInputListLabel.attachToComponent (&midiInputList, true);
auto midiInputs = juce::MidiInput::getAvailableDevices();
addAndMakeVisible (midiInputList);
midiInputList.setTextWhenNoChoicesAvailable ("No MIDI Inputs Enabled");
juce::StringArray midiInputNames;
for (auto input : midiInputs)
midiInputNames.add (input.name);
midiInputList.addItemList (midiInputNames, 1);
midiInputList.onChange = [this] { setMidiInput (midiInputList.getSelectedItemIndex()); };
for (auto input : midiInputs)
{
if (deviceManager.isMidiInputDeviceEnabled (input.identifier))
{
setMidiInput (midiInputs.indexOf (input));
break;
}
}
if (midiInputList.getSelectedId() == 0)
setMidiInput (0);
上記のコードで呼び出されるsetMidiInput()関数を追加します:
void setMidiInput (int index)
{
auto list = juce::MidiInput::getAvailableDevices();
deviceManager.removeMidiInputDeviceCallback (list[lastInputIndex].identifier,
synthAudioSource.getMidiCollector()); // [12]
auto newInput = list[index];
if (!deviceManager.isMidiInputDeviceEnabled (newInput.identifier))
deviceManager.setMidiInputDeviceEnabled (newInput.identifier, true);
deviceManager.addMidiInputDeviceCallback (newInput.identifier, synthAudioSource.getMidiCollector()); // [13]
midiInputList.setSelectedId (index + 1, juce::dontSendNotification);
lastInputIndex = index;
}
SynthAudioSourceオブジェクトからMidiMessageCollectorオブジェクトを、指定されたMIDI入力デバイスのMidiInputCallbackオブジェクトとして追加していることに注意してください[13]。また、ユーザーがコンボボックスを使用して選択したデバイスを変更した場合に備えて、以前に選択したMIDI入力デバイスの以前のMidiInputCallbackオブジェクトを削除する必要があります[12]。
resized()関数で、このComboBoxオブジェクトを配置し、MidiKeyboardComponentオブジェクトの位置を調整する必要があります:
void resized() override
{
midiInputList.setBounds (200, 10, getWidth() - 210, 20);
keyboardComponent.setBounds (10, 40, getWidth() - 20, getHeight() - 50);
}
アプリケーションを再度実行すると、次のように表示されるはずです:

もちろん、リストされるデバイスは、特定のシステム構成によって異なります。
このアプリケーションの変更版のソースコードは、デモプロジェクトのSynthUsingMidiInputTutorial_02.hファイルにあります。
演習: 各ボイスのテールオフの長さを制御するスライダーを追加してみてください。以前の演習でアタックを追加した場合は、アタックの長さを制御するスライダーも追加できます。
まとめ
このチュートリアルでは、Synthesiserクラスを紹介しました。このチュートリアルを読んだ後、次のことができるようになります:
- 音声を生成するためにAudioSourceサブクラス内にSynthesiserクラスをセットアップする。
- 適切なSynthesiserVoiceおよびSynthesiserSoundクラスを作成し、Synthesiserオブジェクトに追加する。
- 画面上のキーボードと外部MIDIソースからMIDIメッセージをSynthesiserオブジェクトに送信する。