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

チュートリアル: マルチポリフォニックシンセサイザーの構築

📚 Source Page

MPE 規格の基本と、MPE に対応したシンセサイザーの実装方法を学びます。ROLI Seaboard Rise にアプリケーションを接続しましょう!

レベル: 中級
プラットフォーム: Windows, macOS, Linux
クラス: MPESynthesiser, MPEInstrument, MPENote, MPEValue, SmoothedValue

はじめに

このチュートリアルのデモプロジェクトをダウンロードしてください: PIP | ZIP 。プロジェクトを解凍し、最初のヘッダーファイルを Projucer で開きます。

この手順でヘルプが必要な場合は、チュートリアル: Projucer Part 1: Projucer の使い方を参照してください。

ヒント

いくつかの箇所で参考にしているため、先にチュートリアル: MIDI シンセサイザーの構築を読んでおくと役立ちます。

デモプロジェクト

デモプロジェクトは、JUCE/examples ディレクトリにある MPEDemo プロジェクトの簡略版です。このチュートリアルを最大限に活用するには、MPE 対応コントローラーが必要です。MPEMIDI Polyphonic Expression の略で、オーディオ製品間で多次元データを通信できるようにする新しい仕様です。

このような MPE 対応デバイスの例としては、ROLI の Seaboard シリーズ(Seaboard RISE など)があります。

警告

コントローラーが Seaboard RISE のように MIDI チャンネルプレッシャーとコンティニュアスコントローラー 74(timbre)を送信しない限り、シンセサイザーの音量が非常に小さく聞こえる場合があります。

コンピューターに Seaboard RISE を接続すると、デモアプリケーションのウィンドウは次のスクリーンショットのようになります:

The demo application
The demo application

MIDI 入力のいずれかを有効にする必要があります(ここでは Seaboard RISE がオプションとして表示されています)。

ビジュアライザー

MPE 対応デバイスで演奏されたノートは、ウィンドウの下部に表示されます。次のスクリーンショットに示されています:

The visualiser
The visualiser

MPE の重要な特徴の 1 つは、特定のコントローラーキーボードからのすべてのノートが同じ MIDI チャンネルに割り当てられるのではなく、新しい MIDI ノートイベントごとに独自の MIDI チャンネルが割り当てられることです。これにより、コントロールチェンジメッセージ、ピッチベンドメッセージなどによって、各ノートを個別に制御できます。JUCE の MPE 実装では、演奏中のノートは MPENote オブジェクトで表されます。MPENote オブジェクトは、次のデータをカプセル化します:

  • ノートの MIDI チャンネル。
  • ノートの初期 MIDI ノート値。
  • ノートオンベロシティ(または strike)。
  • ノートのピッチベンド値: このノートの MIDI チャンネルで受信した MIDI ピッチベンドメッセージから導出されます。
  • ノートのプレッシャー: このノートの MIDI チャンネルで受信した MIDI チャンネルプレッシャーメッセージから導出されます。
  • ノートの timbre: 通常、このノートの MIDI チャンネルでコントローラー 74 のコントローラーメッセージから導出されます。
  • ノートオフベロシティ(または lift): これはノートオフイベントが受信された後、演奏音が停止するまでのみ有効です。

ノートが演奏されていない場合、ビジュアライザーは従来の MIDI キーボードレイアウトを表します。デモアプリケーションのビジュアライザーでは、各ノートは次のように表されます:

  • グレーの塗りつぶされた円は、ノートオンベロシティを表します(ベロシティが高いほど円が大きくなります)。
  • ノートの MIDI チャンネルは、この円内の「+」記号の上に表示されます;
  • 初期 MIDI ノート名は「+」記号の下に表示されます。
  • 重ねて表示される白い円は、このノートの現在のプレッシャーを表します(これも、プレッシャーが高いほど円が大きくなります)。
  • ノートの水平位置は、元のノートとこのノートに適用されたピッチベンドから導出されます。
  • ノートの垂直位置は、ノートの timbre パラメーター(このノートの 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] クラスはグラフィカルユーザーインターフェースからこれを設定する手段を提供します(チュートリアル: AudioDeviceManager クラスを参照)。MidiMessageCollector [5] クラスを使用すると、オーディオコールバックでタイムスタンプ付き MIDI メッセージのブロックにメッセージを簡単に収集できます(チュートリアル: MIDI シンセサイザーの構築を参照)。

AudioDeviceManager オブジェクトを最初にリストすることが重要です。これを AudioDeviceSelectorComponent オブジェクトのコンストラクターに渡すためです:

MainComponent()
: audioSetupComp (audioDeviceManager, 0, 0, 0, 256,
true, // showMidiInputOptions must be true
true,
true,
false)

AudioDeviceSelectorComponent コンストラクターに渡されるもう 1 つの重要な引数に注意してください: 利用可能な MIDI 入力を表示するには、showMidiInputOptionstrue にする必要があります。

チュートリアル: AudioDeviceManager クラスと同様の方法で AudioDeviceManager オブジェクトをセットアップしますが、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 メンバー --- オーディオコールバックでメッセージをシンセサイザーに渡します。

オーディオコールバック

オーディオコールバックが行われる前に、audioDeviceAboutToStart() 関数で synthmidiCollector メンバーにデバイスのサンプルレートを通知する必要があります:

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
{
// すべての出力チャンネルをクリア
for (auto i = 0; i < numOutputChannels; ++i)
{
if (outputChannelData[i] != nullptr)
{
juce::FloatVectorOperations::clear (outputChannelData[i], numSamples);
}
}

// MIDI メッセージをコレクターから取得
juce::MidiBuffer incomingMidi;
midiCollector.removeNextBlockOfMessages (incomingMidi, numSamples);

// シンセサイザーにオーディオをレンダリングさせる
synth.renderNextBlock (juce::AudioBuffer<float> (outputChannelData, numOutputChannels, numSamples),
incomingMidi, 0, numSamples);
}

オーディオコールバックの実装の詳細については、チュートリアル: MIDI シンセサイザーの構築を参照してください。

MPEInstrument クラス

前述のように、visualiserInstrument メンバーはビジュアライザーを駆動するために使用されます。このクラスは、再生中のノートの状態を維持するために MPEInstrument クラスを使用します。この状態は、MPEInstrument::Listener インターフェースを実装することで、Visualiser クラスによって監視されます:

class Visualiser : public juce::Component,
private juce::MPEInstrument::Listener
{

ここでは Visualiser::noteAdded()Visualiser::notePressureChanged()Visualiser::notePitchbendChanged()Visualiser::noteTimbreChanged()Visualiser::noteKeyStateChanged() 関数を実装する必要があります。MPEInstrument::getMostRecentNote() 関数と MPEInstrument::getNote() 関数を使用して、MPEInstrument オブジェクトから MPENote オブジェクトの参照を取得します。

注記

演習: ビジュアライザーを更新して、ノートオフベロシティ(lift)が異なる色で表示されるようにします。速いリフトは明るい色で、遅いリフトは暗い色で表示する必要があります。

MPESynthesiser クラス

このアプリケーションで最も重要なことは、オーディオコールバックで synth メンバーを使用して実際のオーディオを生成することです。MPESynthesiser クラスは Synthesiser クラスに似ています(チュートリアル: MIDI シンセサイザーの構築を参照)が、MPE に対応するように調整されています。これを使用するには、MPESynthesiserVoice クラスを継承するクラスを作成する必要があります。

このアプリケーションには MPEDemoSynthVoice クラスがあります:

class MPEDemoSynthVoice : public juce::MPESynthesiserVoice
{
public:

MainComponent コンストラクターでは、いくつかの MPEDemoSynthVoice オブジェクトをシンセサイザーに追加して、ボイスがポリフォニーを実装できるようにする必要があります:

for (auto i = 0; i < 15; ++i)
synth.addVoice (new MPEDemoSynthVoice());

synth.setVoiceStealingEnabled (false);
ヒント

この数は通常、MPE デバイスがサポートする同時ノートの数を反映する必要があります。Seaboard RISE の場合、これは 15 です。

MPESynthesiserVoice クラス

MPEDemoSynthVoice クラスは、いくつかのメンバー変数を宣言します。これらのほとんどは、SmoothedValue テンプレートクラスでラップされています(チュートリアル: カスケードゲインクラスの構築を参照):

private:
//==============================================================================
juce::SmoothedValue<double> level, timbre, frequency;

double phase = 0.0;
double phaseDelta = 0.0;
double tailOff = 0.0;

// 一部のパラメーター
static constexpr auto maxLevel = 0.05;
static constexpr auto maxLevelDb = 31.0;
static constexpr auto smoothingLengthInSeconds = 0.01;

MPEDemoSynthVoice::prepare() 関数は、現在のサンプルレートで SmoothedValue オブジェクトを初期化します:

void prepare (const juce::dsp::ProcessSpec& spec)
{
level .reset (spec.sampleRate, smoothingLengthInSeconds);
timbre .reset (spec.sampleRate, smoothingLengthInSeconds);
frequency .reset (spec.sampleRate, smoothingLengthInSeconds);
}

ノートの開始

MPEDemoSynthVoice::noteStarted() 関数は、このボイスがノートを開始するときに呼び出されます:

void noteStarted() override
{
jassert (currentlyPlayingNote.isValid());
jassert (currentlyPlayingNote.keyState == juce::MPENote::keyDown
|| currentlyPlayingNote.keyState == juce::MPENote::keyDownAndSustained);

// MPENote の情報は currentlyPlayingNote メンバーで公開され、いつでもアクセスできます。
// カスタムボイスでこれを使用できます。
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<double>::pi * cyclesPerSample;

tailOff = 0.0;
}

ここでは、MPESynthesiserVoice::currentlyPlayingNote メンバーにアクセスして、新しいノートの MPENote 情報を取得できます。すでに述べたように、MPENote クラスには MPENote::pressureMPENote::pitchbendMPENote::timbre などのメンバーがあります。

MPENote クラスには、現在のノートの周波数を計算する便利な関数 MPENote::getFrequencyInHertz() もあります。これには、ノートのピッチベンド値を考慮したものです。

ノートの停止

MPEDemoSynthVoice::noteStopped() 関数は、ノートが停止するときに呼び出されます:

void noteStopped (bool allowTailOff) override
{
jassert (currentlyPlayingNote.keyState == juce::MPENote::off);

if (allowTailOff)
{
// このフラグを設定してテールオフを開始します。レンダリングコールバックがこれを検出し、
// フェードアウトを行い、完了したら clearCurrentNote() を呼び出します。

if (tailOff == 0.0) // stopNote メソッドは複数回呼び出される可能性があるため、
// まだテールオフを開始していない場合にのみ開始する必要があります。
tailOff = 1.0;
}
else
{
// すぐに演奏を停止するように指示されているので、すべてをリセットします..
clearCurrentNote();
phaseDelta = 0.0;
}
}
ヒント

これは、チュートリアル: MIDI シンセサイザーの構築SineWaveVoice::stopNote() 関数に非常に似ています。ここには MPE 固有のものはありません。

注記

演習: ノートオフベロシティ(lift)がノートのリリースレートを変更できるように、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<float>& 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;
}
}
}
}

MainComponent::getNextSample() を呼び出して波形を生成します:

float getNextSample() noexcept
{
auto levelDb = (level.getNextValue() - 1.0) * maxLevelDb;
auto amplitude = std::pow (10.0f, 0.05f * levelDb) * maxLevel;

// timbre は、サイン波とスクエア波の間のブレンドに使用されます。
auto f1 = std::sin (phase);
auto f2 = std::copysign (1.0, f1);
auto a2 = timbre.getNextValue();
auto a1 = 1.0 - a2;

auto nextSample = float (amplitude * ((a1 * f1) + (a2 * f2)));

auto cyclesPerSample = frequency.getNextValue() / currentSampleRate;
phaseDelta = 2.0 * juce::MathConstants<double>::pi * cyclesPerSample;
phase = std::fmod (phase + phaseDelta, 2.0 * juce::MathConstants<double>::pi);

return nextSample;
}

endcode

これは、timbre パラメーターの値に基づいて、サイン波と(バンドリミットされていない)スクエア波の間で単純にクロスフェードします。

注記

演習: timbre パラメーターに応じて、1 オクターブ離れた 2 つのサイン波の間でクロスフェードするように MPEDemoSynthVoice クラスを変更します。

まとめ

このチュートリアルでは、JUCE の MPE ベースのクラスのいくつかを紹介しました。次のことがわかったはずです:

  • MPE とは何か。
  • MPE 対応デバイスは、各ノートを独自の MIDI チャンネルに割り当てること。
  • MPENote クラスが、MIDI チャンネル、元のノート番号、ベロシティ、ピッチベンドなどのノートに関する情報を保存する方法。
  • MPEInstrument クラスが現在再生中のノートの状態を維持すること。
  • MPESynthesiser クラスには、シンセサイザーを駆動するために使用する MPEInstrument オブジェクトが含まれていること。
  • シンセサイザーのオーディオコードを実装するには、MPESynthesiserVoice クラスを継承するクラスを実装する必要があること。

関連項目