Skip to content

Instantly share code, notes, and snippets.

@Xenakios
Created May 31, 2022 18:47
Show Gist options
  • Save Xenakios/2012c6ea3cfba12dec1ba93aa214cb72 to your computer and use it in GitHub Desktop.
Save Xenakios/2012c6ea3cfba12dec1ba93aa214cb72 to your computer and use it in GitHub Desktop.
#pragma once
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
End User License Agreement: www.juce.com/juce-6-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#include "JuceHeader.h"
namespace xenakios
{
namespace dsp
{
/**
A simple compressor with standard threshold, ratio, attack time and release time
controls.
Modified by Xenakios 31st May 2022 to support sidechain input
@tags{DSP}
*/
template <typename SampleType>
class Compressor
{
public:
//==============================================================================
/** Constructor. */
Compressor();
//==============================================================================
/** Sets the threshold in dB of the compressor.*/
void setThreshold(SampleType newThreshold);
/** Sets the ratio of the compressor (must be higher or equal to 1).*/
void setRatio(SampleType newRatio);
/** Sets the attack time in milliseconds of the compressor.*/
void setAttack(SampleType newAttack);
/** Sets the release time in milliseconds of the compressor.*/
void setRelease(SampleType newRelease);
//==============================================================================
/** Initialises the processor. */
void prepare(const juce::dsp::ProcessSpec& spec);
/** Resets the internal state variables of the processor. */
void reset();
//==============================================================================
/** Processes the input and output samples supplied in the processing context. */
template <typename ProcessContext>
void process(const ProcessContext& context) noexcept
{
// for clarity, deal with the sidechained and non-sidechained cases completely separately here, but code
// could of course be deduplicated a bit later
if (!usesSideChain)
{
const auto& inputBlock = context.getInputBlock();
auto& outputBlock = context.getOutputBlock();
const auto numChannels = outputBlock.getNumChannels();
const auto numSamples = outputBlock.getNumSamples();
jassert(inputBlock.getNumChannels() == numChannels);
jassert(inputBlock.getNumSamples() == numSamples);
if (context.isBypassed)
{
outputBlock.copyFrom(inputBlock);
return;
}
for (size_t channel = 0; channel < numChannels; ++channel)
{
auto* inputSamples = inputBlock.getChannelPointer(channel);
auto* outputSamples = outputBlock.getChannelPointer(channel);
for (size_t i = 0; i < numSamples; ++i)
outputSamples[i] = processSample((int)channel, inputSamples[i]);
}
}
else
{
const auto& inputBlock = context.getInputBlock();
auto& outputBlock = context.getOutputBlock();
const auto numInChannels = inputBlock.getNumChannels();
// num in and out channels seems to be identical, so have to do this hack here...
const auto numOutChannels = outputBlock.getNumChannels() / 2;
const auto numSamples = outputBlock.getNumSamples();
jassert(inputBlock.getNumSamples() == numSamples);
/*
// ignore dealing with bypass for now, since we don't have enough output channels
if (context.isBypassed)
{
outputBlock.copyFrom(inputBlock);
return;
}
*/
for (size_t channel = 0; channel < numOutChannels; ++channel)
{
auto* inputSamples = inputBlock.getChannelPointer(channel);
auto* scSamples = inputBlock.getChannelPointer(channel + 2);
auto* outputSamples = outputBlock.getChannelPointer(channel);
for (size_t i = 0; i < numSamples; ++i)
outputSamples[i] = processSampleWithSideChain((int)channel, inputSamples[i], scSamples[i]);
}
}
}
/** Performs the processing operation on a single sample at a time. */
SampleType processSample(int channel, SampleType inputValue);
SampleType processSampleWithSideChain(int channel, SampleType inputValue, SampleType sideChainValue)
{
// Ballistics filter with peak rectifier
auto env = envelopeFilter.processSample(channel, sideChainValue);
// VCA
auto gain = (env < threshold) ? static_cast<SampleType> (1.0)
: std::pow(env * thresholdInverse, ratioInverse - static_cast<SampleType> (1.0));
// Output
return gain * inputValue;
}
void setSidechainEnabled(bool b)
{
usesSideChain = b;
}
bool getSidechainEnabled() const
{
return usesSideChain;
}
private:
//==============================================================================
void update();
//==============================================================================
SampleType threshold, thresholdInverse, ratioInverse;
juce::dsp::BallisticsFilter<SampleType> envelopeFilter;
bool usesSideChain = false;
double sampleRate = 44100.0;
SampleType thresholddB = 0.0, ratio = 1.0, attackTime = 1.0, releaseTime = 100.0;
};
template class Compressor<float>;
template class Compressor<double>;
// namespace dsp
//==============================================================================
template <typename SampleType>
Compressor<SampleType>::Compressor()
{
update();
}
//==============================================================================
template <typename SampleType>
void Compressor<SampleType>::setThreshold(SampleType newThreshold)
{
thresholddB = newThreshold;
update();
}
template <typename SampleType>
void Compressor<SampleType>::setRatio(SampleType newRatio)
{
jassert(newRatio >= static_cast<SampleType> (1.0));
ratio = newRatio;
update();
}
template <typename SampleType>
void Compressor<SampleType>::setAttack(SampleType newAttack)
{
attackTime = newAttack;
update();
}
template <typename SampleType>
void Compressor<SampleType>::setRelease(SampleType newRelease)
{
releaseTime = newRelease;
update();
}
//==============================================================================
template <typename SampleType>
void Compressor<SampleType>::prepare(const juce::dsp::ProcessSpec& spec)
{
jassert(spec.sampleRate > 0);
jassert(spec.numChannels > 0);
sampleRate = spec.sampleRate;
// This needlessly sets up double the number of ballistics filter channels when using sidechaining, but that
// only uses a bit of memory, we never actually process samples through the extra ones
// optimize if seems important enough to bother with...
envelopeFilter.prepare(spec);
update();
reset();
}
template <typename SampleType>
void Compressor<SampleType>::reset()
{
envelopeFilter.reset();
}
//==============================================================================
template <typename SampleType>
SampleType Compressor<SampleType>::processSample(int channel, SampleType inputValue)
{
// Ballistics filter with peak rectifier
auto env = envelopeFilter.processSample(channel, inputValue);
// VCA
auto gain = (env < threshold) ? static_cast<SampleType> (1.0)
: std::pow(env * thresholdInverse, ratioInverse - static_cast<SampleType> (1.0));
// Output
return gain * inputValue;
}
template <typename SampleType>
void Compressor<SampleType>::update()
{
threshold = juce::Decibels::decibelsToGain(thresholddB, static_cast<SampleType> (-200.0));
thresholdInverse = static_cast<SampleType> (1.0) / threshold;
ratioInverse = static_cast<SampleType> (1.0) / ratio;
envelopeFilter.setAttackTime(attackTime);
envelopeFilter.setReleaseTime(releaseTime);
}
}
} // namespace xenakios
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment