メインコンテンツまでスキップ

チュートリアルMIDIシンセサイザーを作る

📚 Source Page

このチュートリアルでは、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.

デモ・プロジェクト

このデモ・プロジェクトでは、シンプルなサイン波シンセサイザーを演奏するのに使えるオンスクリーン・キーボードを紹介している。

MidiKeyboardComponentを含むアプリケーションウィンドウ。
MidiKeyboardComponentを含むアプリケーションウィンドウ。

コンピュータのキーボードのキーを使って、画面上のキーボードをコントロールすることができます(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);
}

もう一度アプリケーションを実行すると、次のようになるはずです:

MIDI入力デバイスリストを表示するアプリケーション・ウィンドウ
MIDI入力デバイスリストを表示するアプリケーション・ウィンドウ

もちろん、表示されるデバイスはシステム構成によって異なります。

注記

この修正版アプリケーションのソースコードはSynthUsingMidiInputTutorial_02.hファイルにある。

エクササイズ

各ボイスのテールオフの長さをコントロールするスライダーを追加してみてください。また、アタックの長さをコントロールするスライダーを追加することもできます。

概要

このチュートリアルではSynthesiserクラスです。このチュートリアルを読めば、次のことができるようになるでしょう:

こちらも参照