2個目のオシレータを追加します。
アナログシンセみたいに、デチューンしたオシレータ重ねるやつだね〜
JUCEチュートリアル、Introduction to DSPを進めていきます。前回、エクササイズの内容で、矩形波まで実装したのですが、ホワイトノイズを忘れていましたので、ホワイトノイズにも挑戦してみました。また、次の項目「Adding a second oscillator」の、2個目のオシレータを追加する項目も進めていきます。
公式のチュートリアルはこちらです。
こんな人の役にたつかも
・JUCEチュートリアル、Introduction to DSPをやっている人
・JUCEでシンセサイザーが作成したい人
・JUCEで2つのオシレータを実装したい人
ホワイトノイズに挑戦
CustomOscillatorクラスのコンストラクタのosc.initialise関数に与えるラムダ式を以下のようにしました。juce::Randomクラスを利用すると、乱数が得られますので、利用してみました。
osc.initialise([](Type x)
{
auto val = juce::Random::getSystemRandom().nextFloat();
//auto scaled_val = juce::jmap(val, 0.0f, 1.0f, -0.8f, 0.8f);
//DBG(scaled_val);
return val;
}, 18000);
テーブル数を128とかにすると、サンプルの数が少なすぎて、音程感を感じてしまいます。テーブルのサンプル数を多くしていくと、よりホワイトノイズに近づいていきました。今回は、適当に18000としています。
Random::getSystemRandom().nextFloat()を呼び出すと、0~1までの範囲のfloatの乱数が得られます。そして、それをそのままreturnで返すことで、18000個の乱数が得られます。
ただ、押すキーによってサンプル再生の時間が変化するため、キーによっては音程感を感じてしまうようなところもあります。ホワイトノイズは、キーに関係なく発音するようにしないとダメなような気がします。
また、コメントアウトした、scaled_valの値では、-0.8〜0.8までの値に変換しています。本当は-1から1の間までのランダムな値だと思うのですが、こちらの方が、キーによる音程感がよくわかるような結果になりました。
どちらの実装にしろ、ホワイトノイズジェネレータは音程に関係ない再生を行わないといけないと思いました。
オシレータを追加する
まずは、オシレータをノコギリ波に戻します。
osc.initialise ([] (Type x)
{
return juce::jmap (x,
Type (-juce::MathConstants<double>::pi),
Type (juce::MathConstants<double>::pi),
Type (-1),
Type (1));
}, 2);
PluginProcessor.hの「Voice」クラスに以下のプログラムを追加していきます。Voiceクラスは、MPESynthesiserVoiceクラスを継承しているクラスです。
まずは、privateなメンバに追加のオシレータを定義していきます。
オシレータと同様に、ProcessorChainとインデックスでDSPのプロセッサを管理しています。
private:
//...略...
enum
{
osc1Index,
osc2Index,//[1]追加します。
masterGainIndex
};
juce::dsp::ProcessorChain<CustomOscillator<float>,
CustomOscillator<float>, //[2]追加します。
juce::dsp::Gain<float>> processorChain;
static constexpr size_t lfoUpdateRate = 100;
};
[1]のように2個目のオシレータのインデックスを追加します。そして、processorChainには、CustomOscillatorとGainをつないでいますので、もう一つ間にオシレータを追加します。
void noteStarted() override
{
auto velocity = getCurrentlyPlayingNote().noteOnVelocity.asUnsignedFloat();
auto freqHz = (float)getCurrentlyPlayingNote().getFrequencyInHertz();
processorChain.get<osc1Index>().setFrequency(freqHz, true);
processorChain.get<osc1Index>().setLevel(velocity);
//以下の部分に、2個目のオシレータの項目を追加します。
processorChain.get<osc2Index>().setFrequency (freqHz * 1.01f, true);
processorChain.get<osc2Index>().setLevel (velocity);
}
発音を開始するときのnoteStarted関数に、追加したオシレータへ周波数と音量を設定します。それぞれ、ノートのベロシティと、キーナンバーから周波数と音量を設定します。呼び出されている関数は、前回記事で実装したCustomOscillatorクラスの「setFrequency」と「setLevel」になります。
次に、2個目のオシレータがピッチベンドにも追従するように、notePitchbendChanged関数にも追記します。関数の名前的にも、ピッチベンドした時に動作するハンドラのような機能がありそうです。
void notePitchbendChanged() override
{
auto freqHz = (float)getCurrentlyPlayingNote().getFrequencyInHertz();
processorChain.get<osc1Index>().setFrequency(freqHz);
//追加しました。
processorChain.get<osc2Index>().setFrequency (freqHz * 1.01f);
}
getFrequencyInHertz関数は、ピッチベンドで周波数が変化します。ピッチベンドホイールなどでピッチベンドしたら、オシレータの周波数が変化するという仕組みになっています。
voiceクラス全体
今回実装したvoiceクラスの全体のコードを貼り付けます。
class Voice : public juce::MPESynthesiserVoice
{
public:
Voice()
{
auto& masterGain = processorChain.get<masterGainIndex>();
masterGain.setGainLinear(0.7f);
}
//==============================================================================
void prepare(const juce::dsp::ProcessSpec& spec)
{
tempBlock = juce::dsp::AudioBlock<float>(heapBlock, spec.numChannels, spec.maximumBlockSize);
processorChain.prepare(spec);
}
//==============================================================================
void noteStarted() override
{
auto velocity = getCurrentlyPlayingNote().noteOnVelocity.asUnsignedFloat();
auto freqHz = (float)getCurrentlyPlayingNote().getFrequencyInHertz();
processorChain.get<osc1Index>().setFrequency(freqHz, true);
processorChain.get<osc1Index>().setLevel(velocity);
//オシレータ2
processorChain.get<osc2Index>().setFrequency (freqHz * 1.01f, true);
processorChain.get<osc2Index>().setLevel (velocity);
}
//==============================================================================
void notePitchbendChanged() override
{
auto freqHz = (float)getCurrentlyPlayingNote().getFrequencyInHertz();
processorChain.get<osc1Index>().setFrequency(freqHz);
//オシレータ2
processorChain.get<osc2Index>().setFrequency (freqHz * 1.01f);
}
//==============================================================================
void noteStopped(bool) override
{
clearCurrentNote();
}
//==============================================================================
void notePressureChanged() override {}
void noteTimbreChanged() override {}
void noteKeyStateChanged() override {}
//==============================================================================
void renderNextBlock(juce::AudioBuffer<float>& outputBuffer, int startSample, int numSamples) override
{
auto block = tempBlock.getSubBlock(0, (size_t)numSamples);
block.clear();
juce::dsp::ProcessContextReplacing<float> context(block);
processorChain.process(context);
juce::dsp::AudioBlock<float>(outputBuffer)
.getSubBlock((size_t)startSample, (size_t)numSamples)
.add(tempBlock);
}
private:
//==============================================================================
juce::HeapBlock<char> heapBlock;
juce::dsp::AudioBlock<float> tempBlock;
enum
{
osc1Index,
osc2Index,//オシレータ2
masterGainIndex
};
juce::dsp::ProcessorChain<CustomOscillator<float>,
CustomOscillator<float>,//オシレータ2
juce::dsp::Gain<float>> processorChain;
static constexpr size_t lfoUpdateRate = 100;
};