Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix: Fix comb-filter effect on oversampled dry/wet; Fix link-in-out knob positioning; Fix oversampled filter artefacts #100

Merged
merged 19 commits into from
Aug 20, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Proper Clipper cpp split
  • Loading branch information
vvvar committed Jul 30, 2023
commit f2bbcbc2d5894e58aafca0ae856491267d8fb26f
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ target_sources(${CMAKE_PROJECT_NAME}
PRIVATE

# DSP
source/processor/Clipper.cpp
source/processor/LevelMeter.cpp

# GUI
Expand Down
2 changes: 1 addition & 1 deletion source/editor/analyser/AnalyserComponent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

#include "../ColourScheme.h"
#include "../Utils.h"
#include "processor/ClippingFunctions.h"
#include "processor/Sigmoid.h"

namespace pe {
namespace gui {
Expand Down
2 changes: 1 addition & 1 deletion source/editor/analyser/cliptype/ClipTypeComponent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

#include "../../ColourScheme.h"
#include "../../Utils.h"
#include "processor/ClippingFunctions.h"
#include "processor/Sigmoid.h"

namespace pe::gui {
namespace {
Expand Down
191 changes: 191 additions & 0 deletions source/processor/Clipper.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#include "Clipper.h"

#include <cmath>

#include "Sigmoid.h"

namespace pe::processor {

namespace {
/*! \brief Calculate frequency of cutoff based on sample rate */
template <typename T>
[[nodiscard]] T calculateCutoff(double const sampleRate) {
// Start cutting of from Nyquist with smoothing
// so that we have a steep cutoff that ends at the end of the spectrum
auto const nyquistFrequency = static_cast<T>(sampleRate) / static_cast<T>(2.0);
auto const smoothing = static_cast<T>(0.98);
return static_cast<T>(nyquistFrequency * smoothing);
}

[[nodiscard]] juce::dsp::ProcessSpec createOversampledSpec(juce::dsp::ProcessSpec const& src, size_t const oversamplingFactor) {
auto const xOversample = static_cast<unsigned int>(::pow(2, oversamplingFactor));
auto const oversampledSampleRate = src.sampleRate * xOversample;
auto const maximumBlockSize = src.maximumBlockSize * xOversample;
return juce::dsp::ProcessSpec{
.sampleRate = oversampledSampleRate, .maximumBlockSize = maximumBlockSize, .numChannels = src.numChannels};
}
} // namespace

template <typename SampleType>
Clipper<SampleType>::Clipper(size_t const oversampleFactorToUse)
: oversamplingFactor(oversampleFactorToUse),
oversampler(2, oversampleFactorToUse, juce::dsp::Oversampling<SampleType>::FilterType::filterHalfBandFIREquiripple) {}

template <typename SampleType>
Clipper<SampleType>::Clipper() : Clipper(0) {}

template <typename SampleType>
void Clipper<SampleType>::prepare(juce::dsp::ProcessSpec const& spec) {
//-----------------------------------------------------------
// Setup oversampling and obtain oversampled context to configure the rest of the chain
oversampler.reset();
oversampler.numChannels = spec.numChannels;
oversampler.initProcessing(spec.maximumBlockSize);
const auto oversampledSpec = createOversampledSpec(spec, oversamplingFactor);
//-----------------------------------------------------------
// Setup pre-clipping filter - add proper filter and prepare
*preFilter.state = *juce::dsp::IIR::Coefficients<SampleType>::makeLowPass(oversampledSpec.sampleRate,
calculateCutoff<SampleType>(oversampledSpec.sampleRate));
preFilter.prepare(oversampledSpec);
//-----------------------------------------------------------
// Setup pre-gain
preGain.prepare(oversampledSpec);
preGain.setRampDurationSeconds(0.1f);
//-----------------------------------------------------------
// Setup dry/wet
dryWet.prepare(oversampledSpec);
dryWet.setMixingRule(juce::dsp::DryWetMixingRule::balanced);
//-----------------------------------------------------------
// Prepare wave shaper
waveShaper.prepare(oversampledSpec);
//-----------------------------------------------------------
// Prepare post-gain
postGain.prepare(oversampledSpec);
postGain.setRampDurationSeconds(0.1f);
//-----------------------------------------------------------
// Prepare post-filter
*postFilter.state = *juce::dsp::IIR::Coefficients<SampleType>::makeLowPass(oversampledSpec.sampleRate,
calculateCutoff<SampleType>(oversampledSpec.sampleRate));
postFilter.prepare(oversampledSpec);
}

template <typename SampleType>
void Clipper<SampleType>::reset() {
oversampler.reset();
preFilter.reset();
preGain.reset();
dryWet.reset();
waveShaper.reset();
postGain.reset();
postFilter.reset();
}

template <typename SampleType>
void Clipper<SampleType>::process(juce::dsp::ProcessContextReplacing<SampleType> const& context) {
//-----------------------------------------------------------
// Create oversampled context
auto oversampledAudioBlock = oversampler.processSamplesUp(context.getInputBlock());
auto oversampledContext = juce::dsp::ProcessContextReplacing<SampleType>(oversampledAudioBlock);
//-----------------------------------------------------------
// Process the signal using oversampled context
//-----------------------------------------------------------
// First, filter everything above Nyquist freq. Otherwise,
// running these freq's through wave shaping function will
// produce aliasing(even on oversampled signal)
preFilter.process(oversampledContext);
//-----------------------------------------------------------
// Then, boost gain so that signal will activate sigmoid
// function in wave shaper(and limit it)
preGain.process(oversampledContext);
//-----------------------------------------------------------
// Right before we've clipped the signal - remember dry signal.
// We will use it to mix with clipped signal later on
dryWet.pushDrySamples(oversampledContext.getInputBlock());
//-----------------------------------------------------------
// Finally, we can pass samples to the sigmoid and limit them
waveShaper.process(oversampledContext);
//-----------------------------------------------------------
// Immediately mix them with dry signal to avoid any comb
// filter effect because gain and filter has slight delays
dryWet.mixWetSamples(oversampledContext.getOutputBlock());
//-----------------------------------------------------------
// Bring original gain back
postGain.process(oversampledContext);
//-----------------------------------------------------------
// Filter anything above Nyquist till the end of the spectrum
// so that we will not abruptly cut them while down sampling
postFilter.process(oversampledContext);
//-----------------------------------------------------------
// Finally, down sample everything to the original sample rate
oversampler.processSamplesDown(context.getOutputBlock());
}

template <typename SampleType>
void Clipper<SampleType>::setThreshold(SampleType const threshold) {
// Sigmoid function activates at 0.0db
// so that threshold cannot be more than that
jassert(threshold <= 0.0f);
// Gain signal up so sigmoid function can limit it.
// We will do it before applying a sigmoid function
preGain.setGainDecibels(std::fabs(threshold));
// Gain down to make it's level tha same it was before clip
// We will do it after we have applied a sigmoid function
postGain.setGainDecibels(threshold);
}

template <typename SampleType>
void Clipper<SampleType>::setClippingType(ClippingType const type) {
switch (type) {
using enum pe::processor::ClippingType;
case LOGARITHMIC:
waveShaper.functionToUse = processor::logiclip;
break;
case HARD:
waveShaper.functionToUse = processor::hardclip;
break;
case QUINTIC:
waveShaper.functionToUse = processor::quintic;
break;
case CUBIC:
waveShaper.functionToUse = processor::cubicBasic;
break;
case HYPERBOLIC_TAN:
waveShaper.functionToUse = processor::tanclip;
break;
case ALGEBRAIC:
waveShaper.functionToUse = processor::algClip;
break;
case ARCTANGENT:
waveShaper.functionToUse = processor::arcClip;
break;
case SIN:
waveShaper.functionToUse = processor::sinclip;
break;
case LIMIT:
waveShaper.functionToUse = processor::limitclip;
break;
default:
waveShaper.functionToUse = processor::hardclip;
break;
}
}

template <typename SampleType>
void Clipper<SampleType>::setDryWetProportion(SampleType const proportion) {
// 0.0 - fully dry(un-clipped)
// 1.0 - fully wet(clipped)
// Everything above/beyond - incorrect
jassert(proportion >= static_cast<SampleType>(0.0) && proportion <= static_cast<SampleType>(1.0));
dryWet.setWetMixProportion(proportion);
}

template <typename SampleType>
[[nodiscard]] size_t Clipper<SampleType>::getOversamplingFactor() const {
return oversamplingFactor;
}

} // namespace pe::processor

//-----------------------------------------------------------
// Explicit template instantiation because c++ ¯\_(ツ)_/¯
template class pe::processor::Clipper<float>;
Loading