JUCEチュートリアル、Introduction to DSP、デモアプリ作成のための準備

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内に定義)

ここまでの実装で、チュートリアルを進めていく基本的なプログラムができました。ここをスタートとしてチュートリアルを進めていきます。

よかったらシェアしてね!
目次
閉じる