チュートリアルマルチ・ポリフォニック・シンセサイザーを作る
MPE規格の基礎とMPEに対応したシンセサイザーの実装方法を学びます。アプリケーションをROLI Seaboard Riseに接続します!
レベル:中級
プラットフォーム:Windows, macOS, Linux
クラス: MPESynthesiser,MPEInstrument,MPENote,MPEValue,SmoothedValue
スタート
このチュートリアルのデモ・プロジェクトのダウンロードはこちらから:PIP|ZIP.プロジェクトを解凍し、最初のヘッダーファイルをProjucerで開く。
このステップにヘルプが必要な場合は、以下を参照してください。Tutorial: Projucer Part 1: Getting started with the Projucer.
を読むと役に立つだろう。Tutorial: Build a MIDI synthesiserこれは多くの場所で基準点として使われているからだ。
デモ・プロジェクト
デモ・プロジェクトはMPEDemo
のプロジェクトである。JUCE/examples
ディレクトリに移動します。このチュートリアルを最大限に活用するためにはMPE互換性のあるコントローラー。MPEを表す。MIDIポリフォニック・エクスプレッションこれは、オーディオ製品間で多次元データの通信を可能にする新しい仕様である。
そのような例をいくつか挙げよう。MPE互換性のある機器は、ROLIのSeaboardシリーズ(例えば、以下のようなもの)である。Seaboard RISE).
シンセサイザーは、コントローラーがMIDIチャンネル・プレッシャーとコンティニュアス・コントローラー74(ベル)のやり方でSeaboard RISEはそうする。
を持っている。Seaboard RISEあなたのコンピューターに接続されたデモ・アプリケーションのウィンドウは、以下のスクリーンショットのようになっているはずです:
MIDI入力を1つ有効にする必要があります。Seaboard RISEはオプションとして表示される)。
ビジュアライザー
で演奏されたすべての音符は、その音符に対応する。MPE対応するデバイスがウィンドウの下部に表示されます。これは以下のスクリーンショットに示されています:
の重要な特徴のひとつである。MPEは、特定のコントローラー・キーボードからのすべてのノートが同じMIDI チャンネルに割り当てられるのではなく、それぞれの新しいMIDI ノート・イベントに独自のMIDI チャンネルが割り当てられます。これにより、コントロール・チェンジ・メッセージやピッチ・ベンド・メッセージなどによって、個々のノートを独立してコントロールできるようになります。JUCEの実装ではMPE演奏音はMPENoteオブジェクト。或いはMPENoteオブジェクトは以下のデータをカプセル化する:
- ノートのMIDIチャンネル
- MIDIノートの初期値
- ノートオン・ベロシティストライキ).
- ノートのピッチベンド値:このノートのMIDIチャンネルで受信したMIDIピッチベンドメッセージから得られます。
- ノートの音圧:このノートのMIDIチャンネルで受信したMIDIチャンネル音圧メッセージから得られます。
- についてベル通常、このノートのMIDIチャンネルにあるコントローラー74のコントローラー・メッセージに由来する。
- ノートオフ速度(またはリフト):ノート・オフ・イベントを受信した後、再生音が止まるまで有効。
ノートを演奏していない状態では、ビジュアライザーが従来の MIDI キーボードレイアウトを表していることがわかります。デモアプリケーションのビジュアライザーでは、各ノートを以下のように表現しています:
- 灰色で塗りつぶされた円はノートオンの速度を表す(速度が速いほど円は大きくなる)。
- ノートのMIDIチャンネルは、この円内の "+"シンボルの上に表示されます;
- 初期MIDIノート名は "+"記号の下に表示されます。
- 重ねて表示されている白い円は、このノートの現在の圧力を表している(ここでも、圧力が高いほど円は大きくなる)。
- ノ ートの水平位置は、元のノートと、このノートに適用されたピッチベンドから決定されます。
- ノートの垂直方向の位置はベルこのノートのMIDIチャンネルのMIDIコントローラー74から)。
その他の設定
の他の側面をさらに掘り下げる前に。MPEこのアプリケーションで実証されている仕様の他に、このアプリケーションが使っているものをいくつか見てみよう。
まず第一にMainComponent
クラスはAudioIODeviceCallback [1]そしてMidiInputCallback [2]クラスである:
class MainComponent : public juce::Component,
private juce::AudioIODeviceCallback, // [1]
private juce::MidiInputCallback // [2]
{
public:
また、このクラスには重要なメンバーもいる。MainComponent
クラスである:
juce::AudioDeviceManager audioDeviceManager; // [3]
juce::AudioDeviceSelectorComponent audioSetupComp; // [4]
Visualiser visualiserComp;
juce::Viewport visualiserViewport;
juce::MPEInstrument visualiserInstrument;
juce::MPESynthesiser synth;
juce::MidiMessageCollector midiCollector; // [5]
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};
についてAudioDeviceManager [3]クラスはコンピュータのオーディオとMIDIのコンフィギュレーションを処理します。AudioDeviceSelectorComponent [4]クラスは、グラフィカル・ユーザー・インターフェースからこれを設定する手段を与えてくれる (Tutorial: The AudioDeviceManager class).そのMidiMessageCollector [5]クラスを使用すると、オーディオ・コールバックでタイムスタンプ付きMIDIメッセージのブロックにメッセージを簡単に集めることができます (Tutorial: Build a MIDI synthesiser).
が重要である。AudioDeviceManagerのコンストラクタに渡すので、オブジェクトが最初にリストされる。AudioDeviceSelectorComponentオブジェクトがある:
MainComponent()
: audioSetupComp (audioDeviceManager, 0, 0, 0, 256,
true, // showMidiInputOptions must be true
true, true, false)
に渡されるもう一つの重要な引数に注目してほしい。AudioDeviceSelectorComponentコンストラクタshowMidiInputOptions
でなければならない。true
をクリックして、使用可能なMIDI入力を表示します。
をセットアップした。AudioDeviceManagerオブジェクトと同様の方法でTutorial: The AudioDeviceManager classしかし、MIDI入力コールバックも追加する必要がある。[6]:
audioDeviceManager.initialise (0, 2, nullptr, true, {}, nullptr);
audioDeviceManager.addMidiInputDeviceCallback ({}, this); // [6]
audioDeviceManager.addAudioCallback (this);
MIDI入力コールバック
についてhandleIncomingMidiMessage()
は、ユーザーインターフェイスのアクティブなMIDI入力のいずれかからMIDIメッセージが受信されるたびに呼び出されます:
void handleIncomingMidiMessage (juce::MidiInput* /*source*/,
const juce::MidiMessage& message) override
{
visualiserInstrument.processNextMidiEvent (message);
midiCollector.addMessageToQueue (message);
}
ここでは、それぞれのMIDIメッセージを両方に渡す:
- 私たちの
visualiserInstrument
ビジュアライザー・ディスプレイの駆動に使用されるメンバー。 - その
midiCollector
メンバーは、オーディオ・コールバックでシンセサイザーにメッセージを渡す。
オーディオ・コールバック
オーディオ・コールバックが行われる前に、次のことを知らせる必要がある。synth
そしてmidiCollector
デバイスのサンプルレートのaudioDeviceAboutToStart()
関数である:
void audioDeviceAboutToStart (juce::AudioIODevice* device) override
{
auto sampleRate = device->getCurrentSampleRate();
midiCollector.reset (sampleRate);
synth.setCurrentPlaybackSampleRate (sampleRate);
}
についてaudioDeviceIOCallbackWithContext()
関数はMPEに特有なことは何もしないようだ:
void audioDeviceIOCallbackWithContext (const float* const* /*inputChannelData*/,
int /*numInputChannels*/,
float* const* outputChannelData,
int numOutputChannels,
int numSamples,
const juce::AudioIODeviceCallbackContext& /*context*/) override
{
// make buffer
juce::AudioBuffer buffer (outputChannelData, numOutputChannels, numSamples);
// clear it to silence
buffer.clear();
juce::MidiBuffer incomingMidi;
// get the MIDI messages for this audio block
midiCollector.removeNextBlockOfMessages (incomingMidi, numSamples);
// synthesise the block
synth.renderNextBlock (buffer, incomingMidi, 0, numSamples);
}
実際、これはSynthAudioSource::getNextAudioBlock()
関数Tutorial: Build a MIDI synthesiser.
MPEコアクラス
すべてMPE特定の処理はMPEクラスである:MPEInstrument,MPESynthesiser,MPESynthesiserVoice,MPEValueそしてMPENote(先に述べたように)。
MPEInstrument クラス
についてMPEInstrumentクラスは、MPE仕様に従って、現在演奏中の音符の状態を保持する。このクラスはMPEInstrumentオブジェクトには、1つ以上のリスナーをアタッチすることができ、ノートに変更が発生するとそれをブロードキャストすることができます。必要なことはMPEInstrumentオブジェクトにMIDIデータを入力し、残りを処理する。
の中でMainComponent
コンストラクタでMPEInstrumentでレガシーモードを選択し、デフォルトのピッチベンド範囲を24半音に設定します:
visualiserInstrument.enableLegacyMode (24);
この特別なモードは、MPE以外のMIDI機器との後方互換性のためのもので、楽器は現在のMPEゾーン・レイアウトを無視します。
参照Tutorial: Understanding MPE zonesを使った、より柔軟なアプローチを紹介する。ゾーンそしてゾーン・レイアウト.
の中でMainComponent::handleIncomingMidiMessage()
関数にMIDIメッセージを渡す。visualiserInstrument
オブジェクトがある:
visualiserInstrument.processNextMidiEvent (message);
この例ではMPEInstrumentオブジェクトを直接作成する必要があります。音声合成を行うために、別にMPEInstrumentオブジェクトになる。そのMPESynthesiserオブジェクトにはMPEInstrumentこのオブジェクトはシンセサイザーを駆動するために使用される。
MPESynthesiser クラス
を設定した。MPESynthesiserと同じ構成でvisualiserInstrument
オブジェクト(レガシー・モード、ピッチベンド・レンジは24半音):
synth.enableLegacyMode (24);
synth.setVoiceStealingEnabled (false);
についてMPESynthesiserクラスはボイス・スティーリングを処理することもできますが、ここではこれをオフにしています。ボイス・スティーリングを有効にすると、シンセはボイスが足りなくなって別の音を演奏する必要がある場合、既存のボイスを引き継ごうとします。
ですでに見てきたとおりである。MainComponent::audioDeviceAboutToStart()
関数を設定する必要がある。MPESynthesiserオブジェクトのサンプル・レートを正しく動作させます:
synth.setCurrentPlaybackSampleRate (sampleRate);
そして、すでに見たようにMainComponent::audioDeviceIOCallback()
関数に渡すだけです。MidiBufferオブジェクトに、合成処理を実行するために使用したいメッセージが格納されている:
synth.renderNextBlock (buffer, incomingMidi, 0, numSamples);
MPESynthesiserVoice クラス
一般的にはMPESynthesiserそしてMPEInstrumentクラスをそのまま使うことができる(ただし、いくつかの動作をオーバーライドする必要がある場合は、どちらのクラスも基本クラスとして使うことができる)。最も重要なクラスはマストを使うためにオーバーライドする。MPESynthesiserクラスはMPESynthesiserVoiceクラスです。これは実際にシンセサイザーのボイスからオーディオ信号を生成する。
これはSynthesiserVoiceクラスで使用される。Synthesiserクラスを実装するようにカスタマイズされている。MPEの仕様を参照してください。参照Tutorial: Build a MIDI synthesiser.
音声クラスのコードはMPEDemoSynthVoice
クラスを実装しています。ここではMPEDemoSynthVoice
クラスを継承する。MPESynthesiserVoiceクラスである:
class MPEDemoSynthVoice : public juce::MPESynthesiserVoice
{
生成する音色のレベル、音色、周波数をコントロールするための値を記録するためのメンバー変数がいくつかある。特にSmoothedValueこのクラスは、値の変化に起因する信号の不連続性を平滑化するのに非常に便利である (Tutorial: Build a sine wave synthesiser).
juce::SmoothedValue level, timbre, frequency;
double phase = 0.0;
double phaseDelta = 0.0;
double tailOff = 0.0;
// some useful constants
static constexpr auto maxLevel = 0.05;
static constexpr auto maxLevelDb = 31.0;
static constexpr auto smoothingLengthInSeconds = 0.01;
};
声の開始と停止
を使用する上で重要なことである。MPESynthesiserVoiceクラスにアクセスすることである。MPESynthesiserVoice::currentlyPlayingNote(保護されている)MPENoteメンバをオーバーライドして、さまざまなコールバック時にノートの制御情報にアクセスできるようにします。例えばMPESynthesiserVoice::noteStarted()関数はこのようになる:
void noteStarted() override
{
jassert (currentlyPlayingNote.isValid());
jassert (currentlyPlayingNote.keyState == juce::MPENote::keyDown
|| currentlyPlayingNote.keyState == juce::MPENote::keyDownAndSustained);
// get data from the current MPENote
level .setTargetValue (currentlyPlayingNote.pressure.asUnsignedFloat());
frequency.setTargetValue (currentlyPlayingNote.getFrequencyInHertz());
timbre .setTargetValue (currentlyPlayingNote.timbre.asUnsignedFloat());
phase = 0.0;
auto cyclesPerSample = frequency.getNextValue() / currentSampleRate;
phaseDelta = 2.0 * juce::MathConstants::pi * cyclesPerSample;
tailOff = 0.0;
}
以下の "5つの次元 "がMPENoteオブジェクトをMPEValueオブジェクトがある:
- ノートオン速度にある。MPENote::noteOnVelocityメンバー
- ピッチベンドにある。MPENote::pitchbendメンバー
- 圧力にある。MPENote::pressureメンバー
- ベルにある。MPENote::timbreメンバー
- ノートオフ速度にある。MPENote::noteOffVelocityメンバー
MPEValueオブジェクトを使用すると、7ビットまたは14ビットのMIDI値ソースから値を簡単に作成し、これらの値を0〜1または-1〜+1の範囲の浮動小数点値として取得することができます。
についてMPEValueクラスは、14ビットの範囲を使用して値を内部に格納します。
についてMainComponent::noteStopped()
関数は、ノート・エンベロープの「リリース」をトリガーする(または、要求があればすぐに止める):
void noteStopped (bool allowTailOff) override
{
jassert (currentlyPlayingNote.keyState == juce::MPENote::off);
if (allowTailOff)
{
// start a tail-off by setting this flag. The render callback will pick up on
// this and do a fade out, calling clearCurrentNote() when it's finished.
if (tailOff == 0.0) // we only need to begin a tail-off if it's not already doing so - the
// stopNote method could be called more than once.
tailOff = 1.0;
}
else
{
// we're being told to stop playing immediately, so reset everything..
clearCurrentNote();
phaseDelta = 0.0;
}
}
とよく似ている。SineWaveVoice::stopNote()
関数Tutorial: Build a MIDI synthesiser.ここにはMPE特有のものはない。
を変更する。MainComponent::noteStopped()
関数を使って、ノートオフ速度(リフト)でノートのリリース速度を変更することができます。リフトを速くすると、リリース時間が短くなります。
パラメータの変更
この音符の音圧、ピッチベンド、音色のいずれかが変更されると、それを知らせるコールバックがある:
void notePressureChanged() override
{
level.setTargetValue (currentlyPlayingNote.pressure.asUnsignedFloat());
}
void notePitchbendChanged() override
{
frequency.setTargetValue (currentlyPlayingNote.getFrequencyInHertz());
}
void noteTimbreChanged() override
{
timbre.setTargetValue (currentlyPlayingNote.timbre.asUnsignedFloat());
}
ここでもMPESynthesiserVoice::currentlyPlayingNoteメンバーを使用して、これらの各パラメーターの現在値を取得する。
オーディオの 生成
についてMainComponent::renderNextBlock()
実際にオーディオ信号を生成し、このボイスの信号を渡されたバッファにミックスする:
void renderNextBlock (juce::AudioBuffer& outputBuffer,
int startSample,
int numSamples) override
{
if (phaseDelta != 0.0)
{
if (tailOff > 0.0)
{
while (--numSamples >= 0)
{
auto currentSample = getNextSample() * (float) tailOff;
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
outputBuffer.addSample (i, startSample, currentSample);
++startSample;
tailOff *= 0.99;
if (tailOff <= 0.005)
{
clearCurrentNote();
phaseDelta = 0.0;
break;
}
}
}
else
{
while (--numSamples >= 0)
{
auto currentSample = getNextSample();
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
outputBuffer.addSample (i, startSample, currentSample);
++startSample;
}
}
}
}