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などをコントロールします)。これにより、シンセサイザーをポリフォニックに演奏できます。
シンセシザークラス
このチュートリアルでは、JUCESynthesiserクラスを使用して、ポリフォニック・シンセシスを実装できます。これは、独自のアプリケーションで独自のサウンドを使ってシンセサイザーをカスタマイズするため に必要なすべての基本要素を示しています。これを動作させるために必要なクラスはいろいろあり、標準のMainContentComponent
クラスがある:
SynthAudioSource
を実装している。AudioSourceというクラスがあります。SynthAudioSource
を含む。Synthesiserクラスそのものです。これはシンセサイザーからすべてのオーディオを出力する。SineWaveVoice
これはカスタムSynthesiserVoiceというクラスがあります。SineWaveVoice
.ボイスクラスは、シンセサイザーのボイスの1つを、他のボイスとミックスしてレンダリングします。Synthesiserオブジェクトを生成します。1つのボイスクラスのインスタンスが1つのボイスをレンダリングします。SineWaveSound
カスタムSynthesiserSoundというクラスがあります。SineWaveSound
.サウンドクラスは、ボイスとし て作成できるサウンドを記述したものです。例えば、サンプラーボイスのサンプルデータや、ウェーブテーブルシンセサイザーのウェーブテーブルデータが含まれます。
キーボードの設定
私たちの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);
}
参照Tutorial: Handling MIDI eventsの詳細についてはMidiKeyboardComponentクラスである。
コンピュータのキーボードからキーボードの演奏を開始するために、アプリケーションの起動直後にキーボード・フォーカスを取得します。そのために、400ミリ秒後に起動する単純なタイマーを使います:
void timerCallback() override
{
keyboardComponent.grabKeyboardFocus();
stopTimer();
}
AudioAppComponent 関数
アプリケーションはAudioAppComponent簡単なオーディオ・アプリケーションをセットアップする (Tutorial: Build a white noise generator最も基本的なアプリケーションの場合)。必要な3つpure virtual関数は、カスタム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()
関数からMIDIデータのバッファを取り出します。MidiKeyboardStateオブジェクトがある。 - [5]これらのMIDIバッファはシンセサイザーに渡され、ノートオンとノートオフのタイムスタンプ(および他のMIDIチャンネルの音色メッセージ)を使って音色をレンダリングします。
SynthesiserVoiceオブジェクトを追加する必要があります。Synthesiserオブジェクトになる。そのSynthesiserオブジェクトがヴォイスのライフタイムを管理する。
SynthesiserSoundオブジェクトはSynthesiserオブジェクトを使用できます。そのSynthesiserSoundクラスはReferenceCountedObjectクラスの寿命はSynthesiserSoundオブジェクトは自動的に処理される。
へのポインタを保持する必要がある場合は、次のようにする。SynthesiserSoundオ ブジェクトに格納する必要があります。YourSoundClass::Ptr
このメモリ管理が機能するように、この変数を使用する。
SineWaveSoundクラス
私たちのサウンドクラスはとてもシンプルで、データを含む必要すらありません。ただ、このサウンドが特定のMIDIチャンネルとそのチャンネルの特定の音符または音域で再生されるべきかどうかを報告する必要があるだけです。この単純なケースではtrue
ともにappliesToNote()
そしてappliesToChannel()
関数を使用します。前述したように、サウンド・クラスには、サウンドを作成するのに必要なデータ(ウェーブテーブルなど)を格納できる。
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;
};
参照Tutorial: Build a sine wave synthesiser最初の3つに関する情報はこちら。そのtailOff
メンバーを使って、それぞれの声を少しソフトにする。リリースをその振幅エンベロープに加えます。これにより、各音声は突然止まるのではなく、最後にわずかにフェードアウトする。

再生可能な音の確認
についてSynthesiserVoice::canPlaySound()関数をオーバーライドして、ボイスがサウンドを再生できるかどうかを返さなければならない。単にtrue
を使用する方法を説明する。dynamic_cast
で、渡されたサウンド・クラスのタイプをチェックする。
bool canPlaySound (juce::SynthesiserSound* sound) override
{
return dynamic_cast (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::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などのデスクトップ・プラットフォームで試す必要があるだろう。
MIDI入力をSynthAudioSourceに提供する
まずMidiMessageCollectorオブジェクトをSynthAudioSource
クラスです。これは、MIDIメッセージを送信できる場所とSynthAudioSource
クラスが使用できる:
juce::MidiMessageCollector midiCollector;
};
MIDIデータのタイムスタンプを処理するためにMidiMessageCollectorクラスはオーディオ・サンプル・レートを知る必要があります。これをSynthAudioSource::prepareToPlay()
機能[10]:
void prepareToPlay (int /*samplesPerBlockExpected*/, double sampleRate) override
{
synth.setCurrentPlaybackSampleRate (sampleRate);
midiCollector.reset (sampleRate); // [10]
}
次に、オーディオの各ブロックに対応するMIDIメッセージをMidiMessageCollector::removeNextBlockOfMessages()機能[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);
}
これにアクセスする必要があるMidiMessageCollectorオブジェクトの外側からSynthAudioSource
クラスにアクセッサを追加します。SynthAudioSource
クラスはこんな感じだ:
juce::MidiMessageCollector* getMidiCollector()
{
return &midiCollector;
}
我々のMainContentComponent
クラスに次のように追加する。MidiMessageCollectorオブジェクトをMidiInputCallbackオブジェクトをアプリケーションのAudioDeviceManagerオブジェクトがある。
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;
}
を追加していることに注意してください。MidiMessageCollectorオブジェクトをSynthAudioSource
オブジェクトをMidiInputCallbackオブジェクト[13]を指定されたMIDI入力デバイスに適用します。また、以前のMidiInputCallbackユーザーがコンボボックスを使って選択したデバイスを変更した場合、以前に選択したMIDI入力デバイスのオブジェクトを返します。[12]。
私たちは、これを次のように位置づける必要がある。ComboBoxオブジェクトの位置を調整する。MidiKeyboardComponentオブジェクトをresized()
関数である:
void resized() override
{
midiInputList .setBounds (200, 10, getWidth() - 210, 20);
keyboardComponent.setBounds (10, 40, getWidth() - 20, getHeight() - 50);
}
もう一度アプリケーションを実行すると、次のようになるはずです:

もちろん、表示されるデバイスはシステム構成によって異なります。
この修正版アプリケーションのソースコードはSynthUsingMidiInputTutorial_02.h
ファイルにある。
各ボイスのテール オフの長さをコントロールするスライダーを追加してみてください。また、アタックの長さをコントロールするスライダーを追加することもできます。
概要
このチュートリアルではSynthesiserクラスです。このチュートリアルを読めば、次のことができるようになるでしょう:
- を設定する。Synthesiserクラス内のAudioSourceオーディオを生成するためのサブクラスです。
- 適切なSynthesiserVoiceそしてSynthesiserSoundクラスに追加する。Synthesiserオブジェクトがある。
- オンスクリーン・キーボー ドと外部MIDIソースからのMIDIメッセージをSynthesiserオブジェクトがある。