Skip to content

Commit

Permalink
Add OvershootLimiter processor (#460)
Browse files Browse the repository at this point in the history
  • Loading branch information
jatinchowdhury18 authored Oct 12, 2023
1 parent 7046f87 commit 4936768
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 6 deletions.
15 changes: 13 additions & 2 deletions examples/SignalGenerator/SignalGeneratorPlugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ void SignalGeneratorPlugin::prepareToPlay (double sampleRate, int samplesPerBloc
westCoastFolder.prepare ((int) spec.numChannels);
waveMultiplyFolder.prepare ((int) spec.numChannels);
adaaSineClipper.prepare ((int) spec.numChannels);
clipGuard.prepare (spec);

int resampleRatio = 2;
for (auto* r : { &resample2, &resample3, &resample4 })
Expand Down Expand Up @@ -144,38 +145,47 @@ void SignalGeneratorPlugin::processAudioBlock (juce::AudioBuffer<float>& buffer)
if (waveshapeIndex == 0)
{
// no waveshaper
clipGuard.setCeiling (100.0f);
}
else if (waveshapeIndex == 1)
{
adaaHardClipper.process (upsampledContext);
clipGuard.setCeiling (1.0f);
}
else if (waveshapeIndex == 2)
{
adaaTanhClipper.process (upsampledContext);
clipGuard.setCeiling (1.0f);
}
else if (waveshapeIndex == 3)
{
adaaCubicClipper.process (upsampledContext);
clipGuard.setCeiling (1.0f);
}
else if (waveshapeIndex == 4)
{
adaa9thOrderClipper.process (upsampledContext);
clipGuard.setCeiling (1.0f);
}
else if (waveshapeIndex == 5)
{
fullWaveRectifier.process (upsampledContext);
clipGuard.setCeiling (100.0f);
}
else if (waveshapeIndex == 6)
{
westCoastFolder.process (upsampledContext);
clipGuard.setCeiling (100.0f);
}
else if (waveshapeIndex == 7)
{
waveMultiplyFolder.processBlock (upsampledBuffer);
clipGuard.setCeiling (100.0f);
}
else if (waveshapeIndex == 8)
{
adaaSineClipper.process (upsampledContext);
clipGuard.setCeiling (100.0f);
}

if (resampler == nullptr)
Expand All @@ -184,9 +194,10 @@ void SignalGeneratorPlugin::processAudioBlock (juce::AudioBuffer<float>& buffer)
}
else
{
auto&& bufferView = chowdsp::BufferView<float> { block };
chowdsp::BufferMath::copyBufferData (resampler->process (upsampledBlock), bufferView);
resampler->process (upsampledBuffer, block);
}

clipGuard.processBlock (block);
};

setUpSampleChoice();
Expand Down
10 changes: 6 additions & 4 deletions examples/SignalGenerator/SignalGeneratorPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ class SignalGeneratorPlugin : public chowdsp::PluginBase<chowdsp::PluginStateImp

using AAFilter = chowdsp::ButterworthFilter<12>;
juce::AudioBuffer<float> upsampledBuffer;
chowdsp::Downsampler<float, AAFilter> resample2;
chowdsp::Downsampler<float, AAFilter> resample3;
chowdsp::Downsampler<float, AAFilter> resample4;
chowdsp::Downsampler<float, AAFilter, false> resample2;
chowdsp::Downsampler<float, AAFilter, false> resample3;
chowdsp::Downsampler<float, AAFilter, false> resample4;

chowdsp::Downsampler<float, AAFilter>* resampler = nullptr;
chowdsp::Downsampler<float, AAFilter, false>* resampler = nullptr;
int previousUpSampleChoice = 0;

chowdsp::SharedLookupTableCache lookupTableCache;
Expand All @@ -108,5 +108,7 @@ class SignalGeneratorPlugin : public chowdsp::PluginBase<chowdsp::PluginStateImp
chowdsp::WaveMultiplier<float, 6> waveMultiplyFolder { &lookupTableCache.get() };
chowdsp::ADAASineClipper<float> adaaSineClipper { &lookupTableCache.get() };

chowdsp::OvershootLimiter<float> clipGuard { 64 };

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SignalGeneratorPlugin)
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#pragma once

namespace chowdsp
{
/**
* A simple limiter designed to be used to protect against overshoot.
*
* A typical use-case might be after a clipper or limiter being applied
* in an up-sampled process. Since the down-sampling process might introduce
* overshoot not present in the up-sampled signal, this limiter can be used
* to maintain the "ceiling" provided by the clipper/limiter after down-sampling.
*
* Note that this limiter is designed to provide something like 0.1 dB of gain
* reduction. Using it as you would a normal limiter (maybe 1-5 dB of gain reduction)
* probably won't work very well.
*/
template <typename SampleType>
class OvershootLimiter
{
public:
/** Constructs the limiter with a given lookahead characteristic */
explicit OvershootLimiter (int lookaheadInSamples = 32)
: lookaheadSamples (lookaheadInSamples)
{
}

/** Prepares the limiter */
void prepare (const juce::dsp::ProcessSpec spec)
{
makeupDelay.prepare (spec);
makeupDelay.setDelay ((SampleType) lookaheadSamples);
makeupGain.reset (lookaheadSamples);

reset();
}

/** Prepares the limiter state */
void reset()
{
makeupDelay.reset();
makeupGain.setCurrentAndTargetValue ((SampleType) 1);
lastBlockMakeupGain = (SampleType) 1;
}

/** Sets the limiter's "ceiling" (default value is 1) */
void setCeiling (SampleType newCeiling)
{
ceiling = newCeiling;
}

/** Returns the latency added by the limiter due to its "lookahead" characteristic */
[[nodiscard]] int getLatencySamples() const noexcept
{
return lookaheadSamples;
}

/** Processes a buffer of audio */
void processBlock (const BufferView<SampleType>& buffer) noexcept
{
auto signalAbsMax = (SampleType) ceiling;
for (auto [ch, data] : buffer_iters::channels (buffer))
{
signalAbsMax = juce::jmax (signalAbsMax,
FloatVectorOperations::findAbsoluteMaximum (data.data(), (int) data.size()));
}

const auto thisBlockMakeupGain = ceiling / signalAbsMax;
jassert (thisBlockMakeupGain <= (SampleType) 1);
makeupGain.setTargetValue (juce::jmin (thisBlockMakeupGain, lastBlockMakeupGain));
lastBlockMakeupGain = thisBlockMakeupGain;

BufferMath::applyGainSmoothed (buffer, makeupGain);
}

private:
const int lookaheadSamples = 32;
DelayLine<SampleType, DelayLineInterpolationTypes::None> makeupDelay { lookaheadSamples };
juce::SmoothedValue<SampleType, juce::ValueSmoothingTypes::Multiplicative> makeupGain { (SampleType) 1 };
SampleType lastBlockMakeupGain { (SampleType) 1 };

SampleType ceiling = (SampleType) 1;

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OvershootLimiter)
};
} // namespace chowdsp
1 change: 1 addition & 0 deletions modules/dsp/chowdsp_dsp_utils/chowdsp_dsp_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ BEGIN_JUCE_MODULE_DECLARATION
#include "Processors/chowdsp_LevelDetector.h"
#include "Processors/chowdsp_Panner.h"
#include "Processors/chowdsp_TunerProcessor.h"
#include "Processors/chowdsp_OvershootLimiter.h"

#if CHOWDSP_USING_JUCE
#include <juce_audio_processors/juce_audio_processors.h>
Expand Down
1 change: 1 addition & 0 deletions tests/dsp_tests/chowdsp_dsp_utils_test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ target_sources(chowdsp_dsp_utils_test
PannerTest.cpp
TunerTest.cpp
LevelDetectorTest.cpp
OvershootLimiterTest.cpp

BBDTest.cpp
PitchShiftTest.cpp
Expand Down
30 changes: 30 additions & 0 deletions tests/dsp_tests/chowdsp_dsp_utils_test/OvershootLimiterTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#include <CatchUtils.h>
#include <chowdsp_dsp_utils/chowdsp_dsp_utils.h>

TEST_CASE ("Overshoot Limiter Test", "[dsp][misc]")
{
constexpr double fs = 48000.0;
constexpr int N = 4800;

chowdsp::OvershootLimiter<float> limiter { 64 };
limiter.prepare ({ fs, (uint32_t) N, 1 });
REQUIRE (limiter.getLatencySamples() == 64);

auto signal = test_utils::makeSineWave (100.0f, (float) fs, N);

SECTION ("Ceiling == 1")
{
chowdsp::BufferMath::applyGain (signal, 1.1f);

limiter.setCeiling (1.0f);
limiter.processBlock (signal);
REQUIRE (chowdsp::BufferMath::getMagnitude (signal) <= 1.0f);
}

SECTION ("Ceiling == 0.5")
{
limiter.setCeiling (0.5f);
limiter.processBlock (signal);
REQUIRE (chowdsp::BufferMath::getMagnitude (signal) <= 0.5f);
}
}

0 comments on commit 4936768

Please sign in to comment.