チュートリアルを進めるために、デモアプリの実装を行います。
サンプルダウンロードできるけど、Projucerから作れるようになりたいよね~
「Add distortion through waveshaping and convolution」を進めるにあたり、基本となるプラグインがzipで配布されています。このファイルは、ヘッダファイル一つにまとめられて記載されているので、ProjucerのPlugi-Inテンプレート「Basic」へプログラムを移植していきます。
次のように、画面上のキーボードを押すと、波形が出力されるような基本アプリが完成します。
このチュートリアルは、あらかじめ実装されたいくつかのテンプレートクラスなどがあります。その基本的な部分はzipファイルとしてプログラムをダウンロードして利用していきます。しかし、すべてがヘッダーファイルに記載されているので、Projucerで作成できる「Basic」というプラグインの構造に置き換えていきたいと思います。
こんな人の役に立つかも
・JUCEのDSPチュートリアルを勉強している人
・JUCEチュートリアルの「Add distortion through waveshaping and convolution」を進めている人
・チュートリアルを進めるにあたり、Projucerテンプレートからデモアプリを実装したい人
プロジェクトの準備
プロジェクト名は、「DSPConvolutionTutorial」としました。
DSPモジュールも忘れずにプロジェクトに加えておきます。
デモアプリの実装
PluginProcessor.h
まずは、音声処理を行うプロセッサ側のプログラムです。
今回もチュートリアルに実装されたクラステンプレートなどの部分を盛大に貼り付けます。プラグイン本体の「〇〇AudioProcessorクラス」の前に貼り付けました。〇〇は、作成プロジェクトの名前が入ります。
template <typename Type>
class CustomOscillator
{
public:
//==============================================================================
CustomOscillator()
{
setWaveform(Waveform::sine);
auto& gain = processorChain.template get<gainIndex>();
gain.setRampDurationSeconds(3e-2);
gain.setGainLinear(Type(0));
}
//==============================================================================
enum class Waveform
{
sine,
saw
};
void setWaveform(Waveform waveform)
{
switch (waveform)
{
case Waveform::sine:
processorChain.template get<oscIndex>().initialise([](Type x)
{
return std::sin(x);
}, 128);
break;
case Waveform::saw:
processorChain.template get<oscIndex>().initialise([](Type x)
{
return juce::jmap(x, Type(-juce::double_Pi), Type(juce::double_Pi), Type(-1), Type(1));
}, 128);
break;
default:
jassertfalse;
break;
}
}
//==============================================================================
void setFrequency(Type newValue, bool force = false)
{
processorChain.template get<oscIndex>().setFrequency(newValue, force);
}
void setLevel(Type newValue)
{
processorChain.template get<gainIndex>().setGainLinear(newValue);
}
void reset() noexcept
{
processorChain.reset();
}
//==============================================================================
template <typename ProcessContext>
void process(const ProcessContext& context) noexcept
{
auto&& outBlock = context.getOutputBlock();
auto blockToUse = tempBlock.getSubBlock(0, outBlock.getNumSamples());
juce::dsp::ProcessContextReplacing<float> tempContext(blockToUse);
processorChain.process(tempContext);
outBlock.copyFrom(context.getInputBlock()).add(blockToUse);
}
//==============================================================================
void prepare(const juce::dsp::ProcessSpec& spec)
{
tempBlock = juce::dsp::AudioBlock<float>(heapBlock, spec.numChannels, spec.maximumBlockSize);
processorChain.prepare(spec);
}
private:
//==============================================================================
juce::HeapBlock<char> heapBlock;
juce::dsp::AudioBlock<float> tempBlock;
enum
{
oscIndex,
gainIndex,
};
juce::dsp::ProcessorChain<juce::dsp::Oscillator<Type>, juce::dsp::Gain<Type>> processorChain;
};
//==============================================================================
template <typename Type>
class CabSimulator
{
public:
//==============================================================================
CabSimulator()
{
auto dir = juce::File::getCurrentWorkingDirectory();
int numTries = 0;
while (!dir.getChildFile("Resources").exists() && numTries++ < 15)
dir = dir.getParentDirectory();
}
//==============================================================================
void prepare(const juce::dsp::ProcessSpec& spec)
{
juce::ignoreUnused(spec);
}
//==============================================================================
template <typename ProcessContext>
void process(const ProcessContext& context) noexcept
{
juce::ignoreUnused(context);
}
//==============================================================================
void reset() noexcept {}
private:
//==============================================================================
};
//==============================================================================
template <typename Type>
class Distortion
{
public:
//==============================================================================
Distortion() {}
//==============================================================================
void prepare(const juce::dsp::ProcessSpec& spec)
{
juce::ignoreUnused(spec);
}
//==============================================================================
template <typename ProcessContext>
void process(const ProcessContext& context) noexcept
{
juce::ignoreUnused(context);
}
//==============================================================================
void reset() noexcept {}
private:
//==============================================================================
};
//==============================================================================
class Voice : public juce::MPESynthesiserVoice
{
public:
Voice()
{
lfo.initialise([](float x)
{
return std::sin(x);
}, 128);
lfo.setFrequency(3.0f);
auto waveform = CustomOscillator<float>::Waveform::saw;
processorChain.get<osc1Index>().setWaveform(waveform);
processorChain.get<osc2Index>().setWaveform(waveform);
auto& masterGain = processorChain.get<masterGainIndex>();
masterGain.setGainLinear(0.7f);
auto& filter = processorChain.get<filterIndex>();
filter.setMode(juce::dsp::LadderFilter<float>::Mode::LPF24);
filter.setResonance(0.7f);
filter.setCutoffFrequencyHz(500.0f);
}
//==============================================================================
void prepare(const juce::dsp::ProcessSpec& spec)
{
lfo.prepare({ spec.sampleRate / lfoDownsamplingRatio, spec.maximumBlockSize, 1 });
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);
processorChain.get<osc2Index>().setFrequency(1.01f * freqHz, true);
processorChain.get<osc2Index>().setLevel(velocity);
}
//==============================================================================
void notePitchbendChanged() override
{
auto freqHz = (float)getCurrentlyPlayingNote().getFrequencyInHertz();
processorChain.get<osc1Index>().setFrequency(freqHz);
processorChain.get<osc2Index>().setFrequency(1.01f * freqHz);
}
//==============================================================================
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
{
for (int i = 0; i < numSamples; ++i)
{
if (--lfoProcessingIndex == 0)
{
lfoProcessingIndex = lfoDownsamplingRatio;
auto lfoOut = lfo.processSample(0.0f);
auto cutoffHz = juce::jmap(lfoOut, -1.0f, 1.0f, 100.0f, 4e3f);
processorChain.get<filterIndex>().setCutoffFrequencyHz(cutoffHz);
}
}
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,
filterIndex,
masterGainIndex
};
juce::dsp::ProcessorChain<CustomOscillator<float>, CustomOscillator<float>,
juce::dsp::LadderFilter<float>, juce::dsp::Gain<float>> processorChain;
static constexpr size_t lfoDownsamplingRatio = 128;
size_t lfoProcessingIndex = lfoDownsamplingRatio;
juce::dsp::Oscillator<float> lfo;
};
//==============================================================================
class AudioEngine : public juce::MPESynthesiser
{
public:
static constexpr size_t maxNumVoices = 4;
//==============================================================================
AudioEngine()
{
for (size_t i = 0; i < maxNumVoices; ++i)
addVoice(new Voice);
setVoiceStealingEnabled(true);
}
//==============================================================================
void prepare(const juce::dsp::ProcessSpec& spec) noexcept
{
setCurrentPlaybackSampleRate(spec.sampleRate);
for (auto* v : voices)
dynamic_cast<Voice*> (v)->prepare(spec);
fxChain.prepare(spec);
}
private:
//==============================================================================
enum
{
distortionIndex,
cabSimulatorIndex,
reverbIndex
};
juce::dsp::ProcessorChain<Distortion<float>, CabSimulator<float>, juce::dsp::Reverb> fxChain;
//==============================================================================
void renderNextSubBlock(juce::AudioBuffer<float>& outputAudio, int startSample, int numSamples) override
{
MPESynthesiser::renderNextSubBlock(outputAudio, startSample, numSamples);
auto block = juce::dsp::AudioBlock<float>(outputAudio).getSubBlock((size_t)startSample, (size_t)numSamples);
auto context = juce::dsp::ProcessContextReplacing<float>(block);
fxChain.process(context);
}
};
//==============================================================================
template <typename SampleType>
class AudioBufferQueue
{
public:
//==============================================================================
static constexpr size_t order = 9;
static constexpr size_t bufferSize = 1U << order;
static constexpr size_t numBuffers = 5;
//==============================================================================
void push(const SampleType* dataToPush, size_t numSamples)
{
jassert(numSamples <= bufferSize);
int start1, size1, start2, size2;
abstractFifo.prepareToWrite(1, start1, size1, start2, size2);
jassert(size1 <= 1);
jassert(size2 == 0);
if (size1 > 0)
juce::FloatVectorOperations::copy(buffers[(size_t)start1].data(), dataToPush, (int)juce::jmin(bufferSize, numSamples));
abstractFifo.finishedWrite(size1);
}
//==============================================================================
void pop(SampleType* outputBuffer)
{
int start1, size1, start2, size2;
abstractFifo.prepareToRead(1, start1, size1, start2, size2);
jassert(size1 <= 1);
jassert(size2 == 0);
if (size1 > 0)
juce::FloatVectorOperations::copy(outputBuffer, buffers[(size_t)start1].data(), (int)bufferSize);
abstractFifo.finishedRead(size1);
}
private:
//==============================================================================
juce::AbstractFifo abstractFifo{ numBuffers };
std::array<std::array<SampleType, bufferSize>, numBuffers> buffers;
};
//==============================================================================
template <typename SampleType>
class ScopeDataCollector
{
public:
//==============================================================================
ScopeDataCollector(AudioBufferQueue<SampleType>& queueToUse)
: audioBufferQueue(queueToUse)
{}
//==============================================================================
void process(const SampleType* data, size_t numSamples)
{
size_t index = 0;
if (state == State::waitingForTrigger)
{
while (index++ < numSamples)
{
auto currentSample = *data++;
if (currentSample >= triggerLevel && prevSample < triggerLevel)
{
numCollected = 0;
state = State::collecting;
break;
}
prevSample = currentSample;
}
}
if (state == State::collecting)
{
while (index++ < numSamples)
{
buffer[numCollected++] = *data++;
if (numCollected == buffer.size())
{
audioBufferQueue.push(buffer.data(), buffer.size());
state = State::waitingForTrigger;
prevSample = SampleType(100);
break;
}
}
}
}
private:
//==============================================================================
AudioBufferQueue<SampleType>& audioBufferQueue;
std::array<SampleType, AudioBufferQueue<SampleType>::bufferSize> buffer;
size_t numCollected;
SampleType prevSample = SampleType(100);
static constexpr auto triggerLevel = SampleType(0.05);
enum class State { waitingForTrigger, collecting } state{ State::waitingForTrigger };
};
//==============================================================================
template <typename SampleType>
class ScopeComponent : public juce::Component,
private juce::Timer
{
public:
using Queue = AudioBufferQueue<SampleType>;
//==============================================================================
ScopeComponent(Queue& queueToUse)
: audioBufferQueue(queueToUse)
{
sampleData.fill(SampleType(0));
setFramesPerSecond(30);
}
//==============================================================================
void setFramesPerSecond(int framesPerSecond)
{
jassert(framesPerSecond > 0 && framesPerSecond < 1000);
startTimerHz(framesPerSecond);
}
//==============================================================================
void paint(juce::Graphics& g) override
{
g.fillAll(juce::Colours::black);
g.setColour(juce::Colours::white);
auto area = getLocalBounds();
auto h = (SampleType)area.getHeight();
auto w = (SampleType)area.getWidth();
// Oscilloscope
auto scopeRect = juce::Rectangle<SampleType>{ SampleType(0), SampleType(0), w, h / 2 };
plot(sampleData.data(), sampleData.size(), g, scopeRect, SampleType(1), h / 4);
// Spectrum
auto spectrumRect = juce::Rectangle<SampleType>{ SampleType(0), h / 2, w, h / 2 };
plot(spectrumData.data(), spectrumData.size() / 4, g, spectrumRect);
}
//==============================================================================
void resized() override {}
private:
//==============================================================================
Queue& audioBufferQueue;
std::array<SampleType, Queue::bufferSize> sampleData;
juce::dsp::FFT fft{ Queue::order };
using WindowFun = juce::dsp::WindowingFunction<SampleType>;
WindowFun windowFun{ (size_t)fft.getSize(), WindowFun::hann };
std::array<SampleType, 2 * Queue::bufferSize> spectrumData;
//==============================================================================
void timerCallback() override
{
audioBufferQueue.pop(sampleData.data());
juce::FloatVectorOperations::copy(spectrumData.data(), sampleData.data(), (int)sampleData.size());
auto fftSize = (size_t)fft.getSize();
jassert(spectrumData.size() == 2 * fftSize);
windowFun.multiplyWithWindowingTable(spectrumData.data(), fftSize);
fft.performFrequencyOnlyForwardTransform(spectrumData.data());
static constexpr auto mindB = SampleType(-160);
static constexpr auto maxdB = SampleType(0);
for (auto& s : spectrumData)
s = juce::jmap(juce::jlimit(mindB, maxdB, juce::Decibels::gainToDecibels(s) - juce::Decibels::gainToDecibels(SampleType(fftSize))), mindB, maxdB, SampleType(0), SampleType(1));
repaint();
}
//==============================================================================
static void plot(const SampleType* data,
size_t numSamples,
juce::Graphics& g,
juce::Rectangle<SampleType> rect,
SampleType scaler = SampleType(1),
SampleType offset = SampleType(0))
{
auto w = rect.getWidth();
auto h = rect.getHeight();
auto right = rect.getRight();
auto center = rect.getBottom() - offset;
auto gain = h * scaler;
for (size_t i = 1; i < numSamples; ++i)
g.drawLine({ juce::jmap(SampleType(i - 1), SampleType(0), SampleType(numSamples - 1), SampleType(right - w), SampleType(right)),
center - gain * data[i - 1],
juce::jmap(SampleType(i), SampleType(0), SampleType(numSamples - 1), SampleType(right - w), SampleType(right)),
center - gain * data[i] });
}
};
このコピペで次のクラスを追加しました。
①CustomOscillator:発音する波形を定義します。今回は、サイン波とのこぎり波をenumで切り替えるような設定になっています。
②CabSimulator:チュートリアルで実装を進めるクラスになります。畳み込み処理でギターキャビネットをシミュレートする、という項目で利用するようです。
③Distortion:チュートリアルで実装を進めるクラスになります。ウェーブシェーピングで歪みを追加するような機能を実装するさいにチュートリアルで利用します。
④Voice:発音する1ボイス分の処理をまとめるようなクラスです。この中でCustomOscillatorを2つ使い、フィルターを通して音量を調節して1ボイスを作成するような処理を定義しています。MPESynthesiserVoiceクラスを継承しています。
⑤AudioEngine:発音ボイス数分のVoiceクラスを準備して、 processBlock関数でバッファ毎の音声処理を行います。今回の音声処理全体をまとめ上げるようなクラスです。プラグイン本体クラスからは、このAudioEngineクラスのprocessBlockを呼び出して処理をします。
⑥AudioBufferQueue:表示用の音声データのキューを定義するクラスです。
⑦ScopeDataCollector:⑥のキューに、表示用の音声データの取得するクラスです。キューの状態をみて新しい音声データを取得するかどうかを判定します。先のAudioQueueクラスのインスタンスをScopeComponentと共有して音声表示へのデータ提供を制御します。
⑧ScopeComponent:音声データの波形表示と、周波数領域に変換した表示を行います。エディタ側でインスタンスとして保持して、音声キューを渡して利用します。1秒間に30回の音声データ表示を行います。
次に、プラグイン本体の〇〇AudioProcessorクラス(〇〇には、プロジェクト名が入っています。)にメンバを追加します。
class DSPConvolutionTutorialAudioProcessor : public juce::AudioProcessor
{
public:
//...略...
//[1]次の関数を実装しました。
juce::MidiMessageCollector& getMidiMessageCollector() noexcept { return midiMessageCollector; }
AudioBufferQueue<float>& getAudioBufferQueue() noexcept { return audioBufferQueue; }
private:
//[2]privateなメンバを追加しました。
AudioEngine audioEngine;
juce::MidiMessageCollector midiMessageCollector;
AudioBufferQueue<float> audioBufferQueue;
ScopeDataCollector<float> scopeDataCollector{ audioBufferQueue };
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DSPConvolutionTutorialAudioProcessor)
};
[2]のように、privateなメンバとして、AudioEngineクラス、MidiMessageCollectorクラス、AudioBufferQueueクラス、ScopeDataCollectorクラスのオブジェクトをメンバとして加えます。いくつかは、先ほど貼り付けしたクラスになります。これらのオブジェクトが基本的なデモアプリの機能を提供してくれます。
[1]では、publicな関数として、「midiMessageCollector」と「audioBufferQueue」の参照を返すような関数を準備しておきます。
PluginProcessor.cpp
prepareToPlay関数
prepareToPlay関数を以下の内容とします。
void DSPConvolutionTutorialAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
//次の内容を追加しました。
audioEngine.prepare({ sampleRate, (juce::uint32) samplesPerBlock, 2 });
midiMessageCollector.reset(sampleRate);
}
processBlock関数
processBlockでは、[1]以降を以下の内容に書き換えました。音声処理は、audioEngineのprocessBlockを呼び出すのみです。また、scopeDataCollectorをよびだして、表示用の音声をキューに蓄積します。
void DSPConvolutionTutorialAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
juce::ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();
//[1]以下の内容に置き換えました。
midiMessageCollector.removeNextBlockOfMessages(midiMessages, buffer.getNumSamples());
for (int i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
buffer.clear(i, 0, buffer.getNumSamples());
audioEngine.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());
scopeDataCollector.process(buffer.getReadPointer(0), (size_t)buffer.getNumSamples());
}
PluginEditor.h
ここから、GUI表示などのエディタ側のプログラムです。プラグイン本体のエディタクラス「〇〇AudioProcessorEditor」クラスのprivateなメンバを追加します。
class DSPConvolutionTutorialAudioProcessorEditor : public juce::AudioProcessorEditor
{
public:
//...略...
private:
//[1]以下のprivateなメンバを追加しました。
DSPConvolutionTutorialAudioProcessor& dspProcessor;//[1-1]
juce::MidiKeyboardState midiKeyboardState;
juce::MidiKeyboardComponent midiKeyboardComponent{ midiKeyboardState, juce::MidiKeyboardComponent::horizontalKeyboard };
ScopeComponent<float> scopeComponent;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DSPConvolutionTutorialAudioProcessorEditor)
};
[1]には、テンプレート初期値で、audioProcessorというオブジェクトがありますが、これは削除して、[1]以下のメンバに変更します。
PluginEditor.cpp
コンストラクタ
[1]と[2]のメンバイニシャライザを変更、追加しました。[1]では、先ほど定義したdspProcessorに変更します。[2]は波形表示用のScopeComponentを初期化します。
DSPConvolutionTutorialAudioProcessorEditor::DSPConvolutionTutorialAudioProcessorEditor (DSPConvolutionTutorialAudioProcessor& p)
: AudioProcessorEditor (&p), dspProcessor(p)//[1]audioProcessorをdspProcessorに変更しました。
, scopeComponent(dspProcessor.getAudioBufferQueue())//[2]追加しました。
{
//[3]以下の内容としました。
addAndMakeVisible(midiKeyboardComponent);
addAndMakeVisible(scopeComponent);
setSize(400, 300);
auto area = getLocalBounds();
scopeComponent.setTopLeftPosition(0, 80);
scopeComponent.setSize(area.getWidth(), area.getHeight() - 100);
midiKeyboardComponent.setMidiChannel(2);
midiKeyboardState.addListener(&dspProcessor.getMidiMessageCollector());
}
そのほかの初期化、各コンポーネントの可視化、Midiキーボードの初期化などを行っています。
デコンストラクタ
midiキーボードのリスナーをデコンストラクタで削除します。
DSPConvolutionTutorialAudioProcessorEditor::~DSPConvolutionTutorialAudioProcessorEditor()
{
//以下の内容を追加しました。
midiKeyboardState.removeListener(&dspProcessor.getMidiMessageCollector());
}
paint関数
GUIの描画では、背景色を塗りつぶす処理のみに変更します。
void DSPConvolutionTutorialAudioProcessorEditor::paint (juce::Graphics& g)
{
//[1]fillAll以外の内容を削除しました。
g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
}
resized関数
midiキーボードをアプリ上部に配置します。
void DSPConvolutionTutorialAudioProcessorEditor::resized()
{
//以下の内容に変更しました。
auto area = getLocalBounds();
midiKeyboardComponent.setBounds(area.removeFromTop(80).reduced(8));
}
ここまでの実装で、チュートリアルを進めるアプリの準備ができました。次回からは引き続き、チュートリアルの実装を進めていきたいと思います。