JUCEのDSPチュートリアルで学んでいきたいと思います。
FFTのところはやったけれど、最初からはやっていなかったね~
JUCEチュートリアル、Introduction to DSPを進めています。本チュートリアルには、ベースとなるデモアプリがありますので、今回は、チュートリアルを進めるためのプログラムの準備を行います。
公式のチュートリアルはこちらです。
ダウンロードできるファイルの「DSPIntroductionTutorial_01.h」は、これからチュートリアルで実装を追加していく基本プログラムです。今回は、このプログラムを、Projucerのプラグインテンプレートで動作するようにしていきます。
こんな感じで、オンスクリーンのMIDIキーボードと、波形出力の領域が表示されるようなアプリがビルドできます。このアプリのプログラムに追加を行いチュートリアルが進んでいきます。
こんな人の役に立つかも
・JUCEプログラミングを勉強している人
・JUCEチュートリアル、「Introduction to DSP」をやっている人
プロジェクトの準備
Projucerのプラグインのテンプレを選択、プロジェクト名は「DSPIntroduction01」としました。
プロジェクトには、DSPモジュールを追加し、Plugin MIDI Inputをオンにしておきます。
実装
PluginProcessor.h
次のクラスを盛大にコピペします。テンプレと思ってコピペします。(ほかのチュートリアルの範疇となります。)
盛大に端折ります。
C++のテンプレートという機能が利用されています。こちらの記事がわかりやすかったので、リンクをはっておきます。
template <typename Type>
class CustomOscillator
{
public:
//==============================================================================
CustomOscillator() {}
//==============================================================================
void setFrequency(Type newValue, bool force = false)
{
juce::ignoreUnused(newValue, force);
}
//==============================================================================
void setLevel(Type newValue)
{
juce::ignoreUnused(newValue);
}
//==============================================================================
void reset() noexcept {}
//==============================================================================
template <typename ProcessContext>
void process(const ProcessContext& context) noexcept
{
juce::ignoreUnused(context);
}
//==============================================================================
void prepare(const juce::dsp::ProcessSpec& spec)
{
juce::ignoreUnused(spec);
}
private:
//==============================================================================
};
//==============================================================================
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);
}
//==============================================================================
void notePitchbendChanged() override
{
auto freqHz = (float)getCurrentlyPlayingNote().getFrequencyInHertz();
processorChain.get<osc1Index>().setFrequency(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
{
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,
masterGainIndex
};
juce::dsp::ProcessorChain<CustomOscillator<float>, juce::dsp::Gain<float>> processorChain;
static constexpr size_t lfoUpdateRate = 100;
};
//==============================================================================
class AudioEngine : public juce::MPESynthesiser
{
public:
static constexpr auto maxNumVoices = 4;
//==============================================================================
AudioEngine()
{
for (auto 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);
}
private:
//==============================================================================
void renderNextSubBlock(juce::AudioBuffer<float>& outputAudio, int startSample, int numSamples) override
{
MPESynthesiser::renderNextSubBlock(outputAudio, startSample, numSamples);
}
};
//==============================================================================
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] });
}
};
次に、プラグイン本体のクラスです。先ほど追加したクラスを利用したメンバを追加しています。
class DSPIntroduction01AudioProcessor : public juce::AudioProcessor
{
public:
//...略...
//[1]追加しました。
juce::MidiMessageCollector& getMidiMessageCollector() noexcept { return midiMessageCollector; }
AudioBufferQueue<float>& getAudioBufferQueue() noexcept { return audioBufferQueue; }
private:
//==============================================================================
//[2]追加しました。
AudioEngine audioEngine;
juce::MidiMessageCollector midiMessageCollector;
AudioBufferQueue<float> audioBufferQueue;
ScopeDataCollector<float> scopeDataCollector{ audioBufferQueue };
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DSPIntroduction01AudioProcessor)
};
[1]と[2]のメンバを追加しました。
PluginProcessor.cpp
PrepareToPlay関数
prepareToPlay関数には、次の内容を追加しています。
void DSPIntroduction01AudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
audioEngine.prepare({ sampleRate, (juce::uint32) samplesPerBlock, 2 });
midiMessageCollector.reset(sampleRate);
}
ProcessBlock関数
[1]以下の内容をprocessBlock関数に追加しました。
void DSPIntroduction01AudioProcessor::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
エディタ側の実装です。オンスクリーンキーボードなどを準備しています。[1]のように、privateなメンバを追加しました。
class DSPIntroduction01AudioProcessorEditor : public juce::AudioProcessorEditor
{
//...略...
private:
//[1]以下のメンバを追加しました。
DSPIntroduction01AudioProcessor& dspProcessor;
juce::MidiKeyboardState midiKeyboardState;
juce::MidiKeyboardComponent midiKeyboardComponent{ midiKeyboardState, juce::MidiKeyboardComponent::horizontalKeyboard };
ScopeComponent<float> scopeComponent;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DSPIntroduction01AudioProcessorEditor)
};
PluginEditor.cpp
コンストラクタ
コンストラクタでは、[1]のメンバイニシャライザを追加しました。デフォルトではaudioProcessorですが、本チュートリアルではdspProcessorという名称になっていたので、変更しています。
DSPIntroduction01AudioProcessorEditor::DSPIntroduction01AudioProcessorEditor (DSPIntroduction01AudioProcessor& p)
: AudioProcessorEditor(&p),
//[1]メンバイニシャライザを以下のようにしました。
dspProcessor(p),
scopeComponent(dspProcessor.getAudioBufferQueue())
{
//[2]各種初期化です。
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());
}
[2]で、各種初期化を行います。オンスクリーンキーボードがmidiKeyBoardComponet、シグナルの波形表示領域がscopeComponentとなっているようです。
デコンストラクタ
デコンストラクタでオンスクリーンキーボードのリスナーを取り除きます。
DSPIntroduction01AudioProcessorEditor::~DSPIntroduction01AudioProcessorEditor()
{
//[1]追加しました。
midiKeyboardState.removeListener(&dspProcessor.getMidiMessageCollector());
}
paint関数
GUI描画は背景色の描画のみです。
void DSPIntroduction01AudioProcessorEditor::paint (juce::Graphics& g)
{
//[1]背景塗りつぶしのみに変更しました。
g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
}
resized関数
resized関数で、[1]のように、GUIの配置を行います。
void DSPIntroduction01AudioProcessorEditor::resized()
{
//[1]次の内容を記載します。
auto area = getLocalBounds();
midiKeyboardComponent.setBounds(area.removeFromTop(80).reduced(8));
}
scopeComponentは、それ自体が自身のpaint関数内に描画処理しています。(PluginProcessor.h内に定義)
ここまでの実装で、チュートリアルを進めていく基本的なプログラムができました。ここをスタートとしてチュートリアルを進めていきます。