ディレイの実装ですが、まずは準備が大切ですね。
デモアプリの構成を理解すると、色々と応用できるかも〜
JUCEチュートリアル、「Create a string model with delay lines」を進めていきます。前回、物理モデリングという合成方法の構成技術要素などを調査しました。次に、一番根本的な技術要素である「ディレイライン」という仕組みを作成していきたいです。ただ、実装を行う前に、波形表示や、オシレータといった機能がすでに実装されたアプリを前提としています。公式チュートリアルでは、「DSPDelayLineTutorial_01.h」というファイルにまとめられています。
今回は、チュートリアルを始める段階までの準備をProjucerのプラグインテンプレートに移植していきたいと思います。
公式チュートリアルはこちらになります。
こんな人の役に立つかも
・JUCE「Create a string model with delay lines」チュートリアルを進めている人
・Projucerテンプレートで「Create a string model with delay lines」チュートリアルの実装を行なっていきたい人
Projucerでのプロジェクト作成
ProjucerのPlug-inのテンプレートからプロジェクトを作成します。今回は、DSPDelayLineTutorialというプロジェクト名にしました。
プロジェクト作成後は、DSPモジュールをModulesに追加しておきます。
デモアプリの実装
今回のチュートリアルは、一つ前のチュートリアルである「Add distortion through waveshaping and convolution」の続きとなっています。デモアプリの基礎となるクラスも同一のものとなります。
以前の記事で、デモアプリの準備記事を記載しております。こちらを参考に、オシレータやオーディオエンジン、スコープといった、デモアプリの基本的な要素となっているクラスを準備します。
Add distortion through waveshaping and convolution、デモアプリの実装の準備
上記、デモアプリの実装では、プロジェクト名が異なるので、「PluginProcessor.h」のdspProcessorの参照するクラスは、「DSPDelayLineTutorialAudioProcessor」と変更する必要があります。(プロジェクト名をDSPDelayLineTutorialとしている場合)
そして、この段階では、Distortionクラスやリバーブなど、前回アプリで中身を実装したクラスは空の状態ですが、今回のディレイには関係がないので、このまま進めていきたいと思います。
※2021/10/07追記
また、VoiceクラスはLFOやオシレータ2が追加されているので、シンプルに一つのオシレータがのこぎり波を出力するような設定に変更します。Voiceクラスを以下の内容に変更しました。(コピペです^^)
class Voice : public juce::MPESynthesiserVoice
{
public:
Voice()
{
auto waveform = CustomOscillator<float>::Waveform::saw;
processorChain.get<oscIndex>().setWaveform (waveform);
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<oscIndex>().setFrequency (freqHz, true);
processorChain.get<oscIndex>().setLevel (velocity);
}
//==============================================================================
void notePitchbendChanged () override
{
auto freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz();
processorChain.get<oscIndex>().setFrequency (freqHz);
}
//==============================================================================
void noteStopped (bool) override
{
processorChain.get<oscIndex>().setLevel (0.0f);
}
//==============================================================================
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);
// silence detector
bool active = false;
for (size_t ch = 0; ch < block.getNumChannels(); ++ch)
{
auto* channelPtr = block.getChannelPointer (ch);
for (int i = 0; i < numSamples; ++i)
{
if (channelPtr[i] != 0.0f)
{
active = true;
break;
}
}
}
if (active)
{
juce::dsp::AudioBlock<float> (outputBuffer)
.getSubBlock ((size_t) startSample, (size_t) numSamples)
.add (tempBlock);
}
else
{
clearCurrentNote();
}
}
private:
//==============================================================================
juce::HeapBlock<char> heapBlock;
juce::dsp::AudioBlock<float> tempBlock;
enum
{
oscIndex,
masterGainIndex
};
juce::dsp::ProcessorChain<CustomOscillator<float>, juce::dsp::Gain<float>> processorChain;
};
PluginProcessor.h
上記、デモアプリの基礎ができたら、今回追加で必要になる、ディレイ関係のクラスを追加していきます。
次のクラスを、「PluginProcessor.h」ファイルのDistortionクラスのすぐ下にコピペしました。(どこでも大丈夫です)
template <typename Type>
class DelayLine
{
public:
void clear() noexcept
{
std::fill (rawData.begin(), rawData.end(), Type (0));
}
size_t size() const noexcept
{
return rawData.size();
}
void resize (size_t newValue)
{
rawData.resize (newValue);
leastRecentIndex = 0;
}
Type back() const noexcept
{
return rawData[leastRecentIndex];
}
Type get (size_t delayInSamples) const noexcept
{
jassert (delayInSamples >= 0 && delayInSamples < size());
juce::ignoreUnused (delayInSamples);
return Type (0);
}
/** Set the specified sample in the delay line */
void set (size_t delayInSamples, Type newValue) noexcept
{
jassert (delayInSamples >= 0 && delayInSamples < size());
ignoreUnused (delayInSamples, newValue);
}
/** Adds a new value to the delay line, overwriting the least recently added sample */
void push (Type valueToAdd) noexcept
{
ignoreUnused (valueToAdd);
}
private:
std::vector<Type> rawData;
size_t leastRecentIndex = 0;
};
//==============================================================================
template <typename Type, size_t maxNumChannels = 2>
class Delay
{
public:
//==============================================================================
Delay()
{
setMaxDelayTime (2.0f);
setDelayTime (0, 0.7f);
setDelayTime (1, 0.5f);
setWetLevel (0.8f);
setFeedback (0.5f);
}
//==============================================================================
void prepare (const juce::dsp::ProcessSpec& spec)
{
jassert (spec.numChannels <= maxNumChannels);
sampleRate = (Type) spec.sampleRate;
updateDelayLineSize();
updateDelayTime();
filterCoefs = juce::dsp::IIR::Coefficients<Type>::makeFirstOrderLowPass (sampleRate, Type (1e3));
for (auto& f : filters)
{
f.prepare (spec);
f.coefficients = filterCoefs;
}
}
//==============================================================================
void reset() noexcept
{
for (auto& f : filters)
f.reset();
for (auto& dline : delayLines)
dline.clear();
}
//==============================================================================
size_t getNumChannels() const noexcept
{
return delayLines.size();
}
//==============================================================================
void setMaxDelayTime (Type newValue)
{
jassert (newValue > Type (0));
maxDelayTime = newValue;
updateDelayLineSize();
}
//==============================================================================
void setFeedback (Type newValue) noexcept
{
jassert (newValue >= Type (0) && newValue <= Type (1));
feedback = newValue;
}
//==============================================================================
void setWetLevel (Type newValue) noexcept
{
jassert (newValue >= Type (0) && newValue <= Type (1));
wetLevel = newValue;
}
//==============================================================================
void setDelayTime (size_t channel, Type newValue)
{
if (channel >= getNumChannels())
{
jassertfalse;
return;
}
jassert (newValue >= Type (0));
delayTimes[channel] = newValue;
updateDelayTime();
}
//==============================================================================
template <typename ProcessContext>
void process (const ProcessContext& context) noexcept
{
auto& inputBlock = context.getInputBlock();
auto& outputBlock = context.getOutputBlock();
auto numSamples = outputBlock.getNumSamples();
auto numChannels = outputBlock.getNumChannels();
jassert (inputBlock.getNumSamples() == numSamples);
jassert (inputBlock.getNumChannels() == numChannels);
juce::ignoreUnused (numSamples);
for (size_t ch = 0; ch < numChannels; ++ch)
{
auto* input = inputBlock .getChannelPointer (ch);
auto* output = outputBlock.getChannelPointer (ch);
auto& dline = delayLines[ch];
auto delayTime = delayTimesSample[ch];
auto& filter = filters[ch];
juce::ignoreUnused (input, output, dline, delayTime, filter);
}
}
private:
//==============================================================================
std::array<DelayLine<Type>, maxNumChannels> delayLines;
std::array<size_t, maxNumChannels> delayTimesSample;
std::array<Type, maxNumChannels> delayTimes;
Type feedback { Type (0) };
Type wetLevel { Type (0) };
std::array<juce::dsp::IIR::Filter<Type>, maxNumChannels> filters;
typename juce::dsp::IIR::Coefficients<Type>::Ptr filterCoefs;
Type sampleRate { Type (44.1e3) };
Type maxDelayTime { Type (2) };
//==============================================================================
void updateDelayLineSize() {}
//==============================================================================
void updateDelayTime() noexcept {}
};
次に、AudioEngineクラスは、エフェクトの処理をプロセッサチェインで接続しているクラスですが、ここにディレイ関連のクラスを追加することで、音声処理にディレイクラスの音声処理が適用されるようになります。
class AudioEngine : public juce::MPESynthesiser
{
public:
//...略...
private:
enum
{
distortionIndex,
cabSimulatorIndex,
delayIndex,//ここを追加しておきます。
reverbIndex
};
//juce::dsp::ProcessorChain<Distortion<float>, CabSimulator<float>, juce::dsp::Reverb> fxChain;
//Delayを追加した以下のProcessorChainに変更します。
juce::dsp::ProcessorChain<Distortion<float>, CabSimulator<float>,Delay<float>, juce::dsp::Reverb> fxChain;
//...略...
};
これで、空のディレイクラス(効果はまだない)を通ってサイン波が発音されるようなデモアプリができました。
※音を出力する際は、実装ミスなどで突然大音量が出る可能性がありますので、最初は音量を絞っておき、小さな音量で確認してから音量を上げることをお勧めいたします。