シンセづくりのチュートリアルも本格的になってきました。
synthesiserクラスというものがあるんだね~
しばらくMIDIの項目で、MIDIの入力などについて勉強をしていました。一通りチュートリアルも完了しましたので、Synthの項目の続きである、「Build a MIDI synthesiser」のチュートリアルを進めていきたいと思います。
今回は、チュートリアルの概要と、デモアプリ(01の例)を「Audio」テンプレートから作成した場合のヘッダーファイルについて進めていきたいと思います。
こんな人の役に立つかも
・JUCEプログラミングを勉強している人
・JUCEの「Build a MIDI synthesiser」チュートリアルを進めている人
・JUCEでシンセを作成したい人
デモプログラム
概要
準備されているデモプログラムをダウンロードすると、「SynthUsingMidiInputTutorial_01.h」と「SynthUsingMidiInputTutorial_02.h」というファイルがあります。
「01」では、まずGUI上のMIDIキーボード、または、パソコンのキーボードの入力を取得して発音するような仕組みのシンセサイザーが作成できます。チュートリアルでは、「The synthesiser class」の項目までの内容をカバーしているとみられます。
「02」では、外部MIDI入力(MIDIコントローラー)を受け付けてシンセを演奏できるようにしていくようです。
まずは、「01」で作成できるシンセサイザーのプログラムを実装していきたいと思います。Projucerの「Audio」テンプレートに追加して実装を行っていきます。Projucerでのプロジェクトの作り方などは、以前の記事もご参照ください。
デモアプリ
次のようにGUI上にMIDIキーボードが出現して、マウスクリックのキーのサイン波が発音されます。
PCのキーボードの「A」「W」「S」…を白鍵と黒鍵に見立てた入力も可能です。
JUCEのsynthesiserクラスというものを利用して、このようなアプリを作成していきます。
MainComponent.hについて
今回のアプリでは、MainComponent.hに、MainComponentクラス以外の3つのクラスを追加します。
①SineWaveSound構造体
SynthsiserSoundクラスを継承しています。SynthsiserSoundクラスは、Synthesiserクラスが発音の際に使用する情報を返すようなクラスとのことです。
②SineWaveVoice構造体
SynthesiserVoiceクラスを継承しています。Synthesiserオブジェクトのボイスのレンダリング、をして、Synthesiserオブジェクトの他の音声とミキシングをする、とのことです。Synthesiserオブジェクトの発音を担当する、という感じでよいのでしょうか。
③SynthAudioSourceクラス
AudioSourceクラスを継承しています。このクラスは、getNextAudioBlock関数などをもっています。音声出力の基本となるクラスで、MainComponetnクラスと同じように、音声出力などができるようになるクラスです。
まずは、動作させるために、次の3つのクラスをMainComponent.hにコピペしていきます。挙動の詳細はチュートリアルの流れに沿って後程見ていきたいと思います。
SineWaveSound
struct SineWaveSound : public juce::SynthesiserSound
{
SineWaveSound() {}
bool appliesToNote(int) override { return true; }
bool appliesToChannel(int) override { return true; }
};
SineWaveVoice
struct SineWaveVoice : public juce::SynthesiserVoice
{
SineWaveVoice() {}
bool canPlaySound(juce::SynthesiserSound* sound) override
{
return dynamic_cast<SineWaveSound*> (sound) != nullptr;
}
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;
}
void stopNote(float /*velocity*/, bool allowTailOff) override
{
if (allowTailOff)
{
if (tailOff == 0.0)
tailOff = 1.0;
}
else
{
clearCurrentNote();
angleDelta = 0.0;
}
}
void pitchWheelMoved(int) override {}
void controllerMoved(int, int) override {}
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;
}
}
}
}
private:
double currentAngle = 0.0, angleDelta = 0.0, level = 0.0, tailOff = 0.0;
};
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;
};
MainComponentクラス
MainComponentクラスには、以下のように追加しました。
class MainComponent : public juce::AudioAppComponent,
//[1]タイマーを追加します。
private juce::Timer
{
public:
//...略...
private:
//[2]タイマーのコールバック関数です。
void timerCallback() override;
//[3]privateなメンバを追加しました。
juce::MidiKeyboardState keyboardState;
SynthAudioSource synthAudioSource;
juce::MidiKeyboardComponent keyboardComponent;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};
[1]と[2]でタイマーによるコールバックを定義します。PCのキーボードのフォーカスを取得(入力キーの取得)をするために利用しているようです。後程、実装していきます。
[3]には、privateなメンバを定義しました。「keyboardState」と「keyboardComponent」については、GUIのキーボードを作成するためのものです。以前のMIDI入力に関する記事もご参照ください。
synthAudioSourceは、先ほど定義したSynthAudioSourceクラスのオブジェクトになります。MainComponentクラスの中に、getNextAudioBlock関数などを持った小さなアプリのオブジェクトを作成するようなイメージになりますね。
C++のstruct
C++でstructはアクセシビリティのデフォルトがpublicであるクラスということで、チュートリアルでもclassと呼んでいる理由がわかりました。次のサイトが参考になりました。
structがCとは違う点は注意ですね。