すごくシンプルにディレイを実装してみました。
色々と応用が利きそうで楽しみだね〜
JUCEチュートリアルの「Create a string model with delay lines」で、リングバッファのディレイラインと、ディレイエフェクトの実装例を学びました。今回は、このディレイの部分を抜き出して、個別のエフェクトとして実装してみたいと思い、Projucerのプラグインテンプレートから、個別に実装してみました。もっともシンプルな実装を実現してみたいと思います。
こんな人の役に立つかも
・JUCEを勉強している人
・JUCEでVSTプラグインを作成したい人
・JUCEでディレイを実装したい人
こんな音が出ます
Logicのエレピの音源に今回のディレイエフェクトをかけた様子です。適当に弾いています。左チャンネルと、右チャンネルで違うディレイタイムを設定しています。今回、パラメータはプログラム内固定で実装しています。
プロジェクトの準備
Projucerの「basic」テンプレートから作成しました。プロジェクト名は、「TheDelay」としました。以前作成した、シンプルなハイパスフィルターと同様のプロジェクトの準備作業となります。
もちろん、プロジェクトには、DSPモジュールのインポートを忘れずに行っておきました。
実装
必要なクラスの導入
ディレイエフェクトの効果を得るためのDelayLineとDelayというクラスを2つ、ヘッダーに実装します。
DelayLineクラス
JUCEチュートリアル、「Create a string model with delay lines」で実装したDelayLineクラスです。ディレイ、とついていても、イメージ的には、循環して永遠に続くバッファ、のようなイメージです。信号の遅延のために保管するようなバッファなので、ディレイラインと呼んでいるようです。ディレイラインの仕組みについての考察等は、こちらの記事で行いましたので、ご参照ください。
今回は、このDelayLineクラスを利用するので、PluginProcessor.hにコピペしました。(位置はインクルードのすぐ下にコピペしました。)
#pragma once
#include <JuceHeader.h>
//以下のDelayLineクラスをコピペしました。
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());
return rawData[(leastRecentIndex + 1 + delayInSamples) % size()];
}
/** Set the specified sample in the delay line */
void set(size_t delayInSamples, Type newValue) noexcept
{
jassert(delayInSamples >= 0 && delayInSamples < size());
rawData[(leastRecentIndex + 1 + delayInSamples) % size()] = newValue;
}
/** Adds a new value to the delay line, overwriting the least recently added sample */
void push(Type valueToAdd) noexcept
{
rawData[leastRecentIndex] = valueToAdd;
leastRecentIndex = leastRecentIndex == 0 ? size() - 1 : leastRecentIndex - 1;
}
private:
std::vector<Type> rawData;
size_t leastRecentIndex = 0;
};
Delayクラス
Delayクラスは、DelayLineクラスの循環バッファを利用して、ディレイエフェクトの効果を付加するようなクラスです。以前の記事もご参照ください。
今回は、先ほどのDelayLineクラスの下にコピペしました。
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>::makeFirstOrderHighPass(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);
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];
for (size_t i = 0; i < numSamples; ++i)
{
auto delayedSample = filter.processSample(dline.get(delayTime));
auto inputSample = input[i];
auto dlineInputSample = std::tanh(inputSample + feedback * delayedSample);
dline.push(dlineInputSample);
auto outputSample = inputSample + wetLevel * delayedSample;
output[i] = outputSample;
}
}
}
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()
{
auto delayLineSizeSamples = (size_t)std::ceil(maxDelayTime * sampleRate);
for (auto& dline : delayLines)
dline.resize(delayLineSizeSamples);
}
//==============================================================================
void updateDelayTime() noexcept
{
for (size_t ch = 0; ch < maxNumChannels; ++ch)
delayTimesSample[ch] = (size_t)juce::roundToInt(delayTimes[ch] * sampleRate);
}
};
PluginProcessor.h
プラグインのAudioProcessorクラスに、先ほど作成したDelayクラスのインスタンスを定義します。この時、ProcessorChainクラスに含めてDelayクラスのインスタンスを作成することで、他のエフェクト(DSPモジュール)を追加するときに、容易になるようにしました。
class TheDelayAudioProcessor : public juce::AudioProcessor
{
//...略...
private:
//以下のenumと、processorChainを追加しました。
enum
{
DelayIndex
};
juce::dsp::ProcessorChain<Delay<float>> processorChain;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TheDelayAudioProcessor)
};
ProcessorChainクラスは、各種DSPを数珠繋ぎにできる便利なクラスです。利用したい、すべてのDSPモジュールに対してprepareしたり、processしたりを個別モジュール毎に記載しなくてよくなります。例えば、この後に音量調整をしたいときは、juce::dsp::Gainモジュールを簡単に追加したりできますので、今回もこのprocessorChainを利用します。
音声処理
PluginProcessorに実装する、音声処理に関する実装です。
prepareToPlay関数
void TheDelayAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
//以下の内容を追加しました。
processorChain.prepare({ sampleRate, (juce::uint32) samplesPerBlock, 2 });
}
ProcessorChainクラスのprepareは、「dsp::ProcessSpec」構造体を受けますので、構造体の形にのっとって値を渡します。構造体のメンバは、「double sampleRate」「uint32 maximumBlockSize」「uint32 numChannels」で構成されています。sampleRateは、prepareToPlayの引数をそのまま渡します。maximumBlockSizeは、prepareToPlayの第二引数を渡します。そして、今回はステレオ対応ということで、チャンネル数2を渡します。これらのメンバは、「{}」でくくり、構造体として渡しました。
processBlock関数
音声処理のprocessBlock関数です。[1]以下のプログラムを追加しています。今回は、Delayクラスのprocess内で、チャンネル毎の処理ループを回していますので、もともとあったチャンネル毎のfor文は消しました。
void TheDelayAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
juce::ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();
for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
buffer.clear (i, 0, buffer.getNumSamples());
//[1]以下の内容を追加(デフォルトのチャンネル毎のfor文から変更)しました。
auto block = juce::dsp::AudioBlock<float>(buffer).getSubBlock((size_t)0, (size_t)buffer.getNumSamples());
auto context = juce::dsp::ProcessContextReplacing<float>(block);
processorChain.process(context);
}
ProcessorChainクラスのprocess関数は、引数に「ProcessContext」の参照を取ります。渡すためのcontextをprocessBlockのAudioBufferから作成します。まずは、blockというAudioBlockクラスのインスタンスを作成します。引数のAudioBufferクラスの参照、bufferに入ってきたデータをAudioBlockに一度格納するというイメージです。getSubBlock関数で、AudioBufferの0番目からサンプル数の最後の数までを取得して、AudioBlockクラスのblockを作成しました。次に、blockをProcessContextReplacing構造体にします。contextという名称のインスタンスに入れて、これをprocess関数への引数とすることで、processorChainに音声バッファを渡すことができました。ここの部分は、チュートリアル 「Create a string model with delay lines」 のAudioEngineクラスの「renderNextSubBlock」関数の渡し方を参考にしました。