Skip to content

Commit

Permalink
More Add generic CrossoverFilter class (#464)
Browse files Browse the repository at this point in the history
* LinearPhaseEQ: make sure stopTimer is called before destruction

* StandardEQParameters: fix shadow warning

* Add generic CrossoverFilter class

* Add tests

* Apply clang-format

* Fixing size_t conversions for GCC

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
jatinchowdhury18 and github-actions[bot] authored Nov 2, 2023
1 parent efcd44b commit 16c145a
Show file tree
Hide file tree
Showing 10 changed files with 319 additions and 82 deletions.
51 changes: 25 additions & 26 deletions examples/ForwardingTestPlugin/InternalPlugins/BandSplitPlugin.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
#include "BandSplitPlugin.h"

namespace
{
const juce::ParameterID freqTag = { "freq", 100 };
const juce::ParameterID orderTag = { "order", 100 };
const juce::ParameterID modeTag = { "mode", 100 };
} // namespace

BandSplitPlugin::BandSplitPlugin() = default;

void BandSplitPlugin::prepareToPlay (double sampleRate, int samplesPerBlock)
Expand All @@ -19,9 +12,8 @@ void BandSplitPlugin::prepareToPlay (double sampleRate, int samplesPerBlock)
filter8.prepare (spec);
filter12.prepare (spec);

lowBuffer.setMaxSize (numChannels, samplesPerBlock);
midBuffer.setMaxSize (numChannels, samplesPerBlock);
highBuffer.setMaxSize (numChannels, samplesPerBlock);
for (auto& buffer : outBuffers)
buffer.setMaxSize (numChannels, samplesPerBlock);
}

void BandSplitPlugin::processAudioBlock (juce::AudioBuffer<float>& buffer)
Expand All @@ -31,14 +23,16 @@ void BandSplitPlugin::processAudioBlock (juce::AudioBuffer<float>& buffer)

// input buffer is used for low frequency signal
auto&& bufferView = chowdsp::BufferView<float> { buffer };
lowBuffer.setCurrentSize (numChannels, numSamples);
highBuffer.setCurrentSize (numChannels, numSamples);
for (auto& outBuffer : outBuffers)
outBuffer.setCurrentSize (numChannels, numSamples);

const auto processFilter = [this, &bufferView] (auto& filter)
{
filter.setLowCrossoverFrequency (*state.params.freqLowParam);
filter.setHighCrossoverFrequency (*state.params.freqHighParam);
filter.processBlock (bufferView, lowBuffer, midBuffer, highBuffer);
filter.setCrossoverFrequency (0, *state.params.freqLowParam);
filter.setCrossoverFrequency (1, *state.params.freqMidParam);
filter.setCrossoverFrequency (2, *state.params.freqHighParam);

filter.processBlock (bufferView, { outBuffers[0], outBuffers[1], outBuffers[2], outBuffers[3] });
};

const auto orderIndex = state.params.orderParam->getIndex();
Expand All @@ -57,22 +51,27 @@ void BandSplitPlugin::processAudioBlock (juce::AudioBuffer<float>& buffer)
const auto modeIndex = state.params.modeParam->getIndex();
if (modeIndex == 1) // solo low
{
chowdsp::BufferMath::applyGain (midBuffer, 0.0f);
chowdsp::BufferMath::applyGain (highBuffer, 0.0f);
for (auto bufferIndex : { 1, 2, 3 })
outBuffers[(size_t) bufferIndex].clear();
}
else if (modeIndex == 2) // solo mid-low
{
for (auto bufferIndex : { 0, 2, 3 })
outBuffers[(size_t) bufferIndex].clear();
}
else if (modeIndex == 2) // solo mid
else if (modeIndex == 3) // solo mid-high
{
chowdsp::BufferMath::applyGain (lowBuffer, 0.0f);
chowdsp::BufferMath::applyGain (highBuffer, 0.0f);
for (auto bufferIndex : { 0, 1, 3 })
outBuffers[(size_t) bufferIndex].clear();
}
else if (modeIndex == 3) // solo high
else if (modeIndex == 4) // solo high
{
chowdsp::BufferMath::applyGain (lowBuffer, 0.0f);
chowdsp::BufferMath::applyGain (midBuffer, 0.0f);
for (auto bufferIndex : { 0, 1, 2 })
outBuffers[(size_t) bufferIndex].clear();
}

// sum bands back together
chowdsp::BufferMath::copyBufferData (lowBuffer, bufferView);
chowdsp::BufferMath::addBufferData (midBuffer, bufferView);
chowdsp::BufferMath::addBufferData (highBuffer, bufferView);
chowdsp::BufferMath::copyBufferData (outBuffers[0], bufferView);
for (size_t i = 1; i < outBuffers.size(); ++i)
chowdsp::BufferMath::addBufferData (outBuffers[i], bufferView);
}
27 changes: 17 additions & 10 deletions examples/ForwardingTestPlugin/InternalPlugins/BandSplitPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ struct BandSplitParams : chowdsp::ParamHolder
BandSplitParams()
{
add (freqLowParam,
freqMidParam,
freqHighParam,
orderParam,
modeParam);
Expand All @@ -20,11 +21,18 @@ struct BandSplitParams : chowdsp::ParamHolder
200.0f
};

chowdsp::FreqHzParameter::Ptr freqMidParam {
juce::ParameterID { "freq_mid", 100 },
"Mid Crossover Frequency",
chowdsp::ParamUtils::createNormalisableRange (20.0f, 20000.0f, 2000.0f),
1000.0f
};

chowdsp::FreqHzParameter::Ptr freqHighParam {
juce::ParameterID { "freq_high", 100 },
"High Crossover Frequency",
chowdsp::ParamUtils::createNormalisableRange (20.0f, 20000.0f, 2000.0f),
2000.0f
8000.0f
};

chowdsp::ChoiceParameter::Ptr orderParam {
Expand All @@ -37,7 +45,7 @@ struct BandSplitParams : chowdsp::ParamHolder
chowdsp::ChoiceParameter::Ptr modeParam {
juce::ParameterID { "mode", 100 },
"Mode",
juce::StringArray { "Through", "Solo Low", "Solo Mid", "Solo High" },
juce::StringArray { "Through", "Solo Low", "Solo Mid-Low", "Solo Mid-High", "Solo High" },
0
};
};
Expand All @@ -55,15 +63,14 @@ class BandSplitPlugin : public chowdsp::PluginBase<chowdsp::PluginStateImpl<Band
juce::AudioProcessorEditor* createEditor() override { return nullptr; }

private:
chowdsp::ThreeWayCrossoverFilter<float, 1> filter1;
chowdsp::ThreeWayCrossoverFilter<float, 2> filter2;
chowdsp::ThreeWayCrossoverFilter<float, 4> filter4;
chowdsp::ThreeWayCrossoverFilter<float, 8> filter8;
chowdsp::ThreeWayCrossoverFilter<float, 12> filter12;
static constexpr auto numBands = 4;
chowdsp::CrossoverFilter<float, 1, numBands> filter1;
chowdsp::CrossoverFilter<float, 2, numBands> filter2;
chowdsp::CrossoverFilter<float, 4, numBands> filter4;
chowdsp::CrossoverFilter<float, 8, numBands> filter8;
chowdsp::CrossoverFilter<float, 12, numBands> filter12;

chowdsp::Buffer<float> lowBuffer;
chowdsp::Buffer<float> midBuffer;
chowdsp::Buffer<float> highBuffer;
std::array<chowdsp::Buffer<float>, numBands> outBuffers;

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BandSplitPlugin)
};
6 changes: 6 additions & 0 deletions modules/dsp/chowdsp_eq/EQ/chowdsp_LinearPhaseEQ.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
namespace chowdsp::EQ
{
template <typename PrototypeEQ, int defaultFIRLength>
LinearPhaseEQ<PrototypeEQ, defaultFIRLength>::~LinearPhaseEQ()
{
stopTimer();
}

template <typename PrototypeEQ, int defaultFIRLength>
int LinearPhaseEQ<PrototypeEQ, defaultFIRLength>::getIRSize (double sampleRate)
{
Expand Down
3 changes: 2 additions & 1 deletion modules/dsp/chowdsp_eq/EQ/chowdsp_LinearPhaseEQ.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class LinearPhaseEQ : private juce::HighResolutionTimer
public:
/** Default constructor. */
LinearPhaseEQ() = default;
~LinearPhaseEQ() override;

/** Implement this function to update the prototype EQ parameters. */
std::function<void (PrototypeEQ&, const ProtoEQParams&)> updatePrototypeEQParameters = nullptr;
Expand Down Expand Up @@ -86,7 +87,7 @@ class LinearPhaseEQ : private juce::HighResolutionTimer
void processBlocksInternal (const AudioBlock<const float>& inputBlock, AudioBlock<float>& outputBlock) noexcept;

PrototypeEQ prototypeEQ;
EQParams<ProtoEQParams> params;
EQParams<ProtoEQParams> params {};

std::vector<std::unique_ptr<ConvolutionEngine<>>> engines;
std::unique_ptr<IRTransfer> irTransfer;
Expand Down
5 changes: 3 additions & 2 deletions modules/dsp/chowdsp_eq/EQ/chowdsp_StandardEQParameters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
namespace chowdsp::EQ
{
template <size_t NumBands>
StandardEQParameters<NumBands>::StandardEQParameters (EQParameterHandles&& paramHandles, const juce::String& name)
: ParamHolder (name),
StandardEQParameters<NumBands>::StandardEQParameters (EQParameterHandles&& paramHandles,
const juce::String& paramHolderName)
: ParamHolder (paramHolderName),
eqParams (std::move (paramHandles))
{
for (auto& bandParams : eqParams)
Expand Down
166 changes: 166 additions & 0 deletions modules/dsp/chowdsp_filters/Other/chowdsp_CrossoverFilter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#pragma once

namespace chowdsp
{
/** An N-way crossover filter based using internal Linkwitz-Riley filters */
template <typename T, int Order, int NumBands>
class CrossoverFilter
{
static_assert (NumBands > 2, "NumBands must be > 2!");

public:
CrossoverFilter() = default;

/** Prepares the filter to process a new stram of audio. */
void prepare (const juce::dsp::ProcessSpec& spec)
{
lowerBandsCrossover.prepare (spec);
highCutFilter.prepare (spec);
for (auto& filter : apHighCutFilter)
filter.prepare (spec);

tempBuffer.setMaxSize ((int) spec.numChannels, (int) spec.maximumBlockSize);
}

/** Resets the filter state. */
void reset()
{
lowerBandsCrossover.reset();
highCutFilter.reset();
for (auto& filter : apHighCutFilter)
filter.reset();
}

/**
* Sets the crossover frequency for a given crossover in Hz.
*
* The lowest frequency crossover will have index 0, and the
* highest frequency crossover will have frequency NumBands - 2.
*/
void setCrossoverFrequency (int crossoverIndex, T freqHz)
{
jassert (crossoverIndex <= NumBands - 2);

if (crossoverIndex == NumBands - 2)
{
highCutFilter.setCrossoverFrequency (freqHz);
for (auto& filter : apHighCutFilter)
filter.setCrossoverFrequency (freqHz);
}
else
{
lowerBandsCrossover.setCrossoverFrequency (crossoverIndex, freqHz);
}
}

/**
* Processes all bands of the crossover filter. The output bands
* should be provided in order from lowest band to highest. The
* size of the output buffer span must be equal to NumBands.
*/
[[maybe_unused]] void processBlock (const BufferView<const T>& bufferIn,
nonstd::span<const BufferView<T>> buffersOut) noexcept
{
jassert ((int) buffersOut.size() == NumBands);

tempBuffer.setCurrentSize (bufferIn.getNumChannels(), bufferIn.getNumSamples());

if constexpr (Order == 1)
{
auto lowerBandBuffers = buffersOut.template first<(size_t) NumBands - 1>();
lowerBandsCrossover.processBlock (bufferIn, lowerBandBuffers);
BufferMath::copyBufferData (lowerBandBuffers.back(), tempBuffer); // Order-1 LR filter does not allow pointer aliasing, so we copy to a temp buffer here.
highCutFilter.processBlock (tempBuffer, lowerBandBuffers.back(), buffersOut.back());
}
else
{
auto lowerBandBuffers = buffersOut.template first<(size_t) NumBands - 1>();
lowerBandsCrossover.processBlock (bufferIn, lowerBandBuffers);
highCutFilter.processBlock (lowerBandBuffers.back(), lowerBandBuffers.back(), buffersOut.back());

// an allpass LR-filter with the same crossover as the high-cut frequency
// this puts the low band back in-phase with the high- and mid-bands.
for (auto [buffer, filter] : chowdsp::zip (buffersOut.template first<(size_t) NumBands - 2>(), apHighCutFilter))
{
filter.processBlock (buffer, buffer, tempBuffer);
BufferMath::addBufferData (tempBuffer, buffer);
}
}
}

/**
* Processes all bands of the crossover filter. The output bands
* should be provided in order from lowest band to highest. The
* size of the output buffer list must be equal to NumBands.
*/
[[maybe_unused]] void processBlock (const BufferView<const T>& bufferIn,
std::initializer_list<BufferView<T>>&& buffersOut) noexcept
{
processBlock (bufferIn, { buffersOut.begin(), buffersOut.end() });
}

private:
CrossoverFilter<T, Order, NumBands - 1> lowerBandsCrossover {};
LinkwitzRileyFilter<T, Order> highCutFilter {};
std::array<LinkwitzRileyFilter<T, Order>, (size_t) NumBands - 2> apHighCutFilter {};

Buffer<T> tempBuffer {};

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CrossoverFilter)
};

/** An 2-way crossover filter based using an internal Linkwitz-Riley filter */
template <typename T, int Order>
class CrossoverFilter<T, Order, 2>
{
public:
CrossoverFilter() = default;

/** Prepares the filter to process a new stram of audio. */
void prepare (const juce::dsp::ProcessSpec& spec)
{
filter.prepare (spec);
}

/** Resets the filter state. */
void reset()
{
filter.reset();
}

/** Sets the crossover frequency in Hz. crossoverIndex must be equal to 0 */
void setCrossoverFrequency ([[maybe_unused]] int crossoverIndex, T freqHz)
{
jassert (crossoverIndex == 0);
filter.setCrossoverFrequency (freqHz);
}

/**
* Processes all bands of the crossover filter. The output bands
* should be provided in order from lowest band to highest. The
* size of the output buffer span must be equal to 2.
*/
[[maybe_unused]] void processBlock (const BufferView<const T>& bufferIn,
nonstd::span<const BufferView<T>> buffersOut) noexcept
{
jassert ((int) buffersOut.size() == 2);
filter.processBlock (bufferIn, buffersOut[0], buffersOut[1]);
}

/**
* Processes all bands of the crossover filter. The output bands
* should be provided in order from lowest band to highest. The
* size of the output buffer list must be equal to 2.
*/
[[maybe_unused]] void processBlock (const BufferView<const T>& bufferIn,
std::initializer_list<BufferView<T>>&& buffersOut) noexcept
{
processBlock (bufferIn, { buffersOut.begin(), buffersOut.end() });
}

private:
LinkwitzRileyFilter<T, Order> filter {};

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CrossoverFilter)
};
} // namespace chowdsp
Loading

0 comments on commit 16c145a

Please sign in to comment.