commit c361adb5616c367fb89bac8dd79a13bc41133563
parent 6c58e3d1ff7d0872cbefe8a666cebdedfa8036bd
Author: jatinchowdhury18 <jatinchowdhury18@gmail.com>
Date: Fri, 3 Sep 2021 22:34:28 -0700
Add tape compression processor (#214)
* Set up placeholder UI for tape compression
* Rough implementation of compression processor
* Optimize and improve tape compression algorithm
* Fine-tune compression processing
* Apply clang-format
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Diffstat:
10 files changed, 241 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -3,6 +3,7 @@ All notable changes to this project will be documented in
this file.
## UNRELEASED
+- Added tape compression processor.
- Improved hysteresis engine performance by ~1.5x.
- Improved oversampling menu with choices for linear vs. minimum phase and real-time vs. offline rendering.
- Made oversampling choices preset-agnostic.
diff --git a/Plugin/Source/GUI/Assets/gui.xml b/Plugin/Source/GUI/Assets/gui.xml
@@ -28,7 +28,7 @@
</Style>
</Styles>
<View id="root" resizable="1" resize-corner="1" flex-direction="column"
- padding="0" width="580" height="620" background-color="FF8B3232"
+ padding="0" width="640" height="620" background-color="FF8B3232"
background-image="Background_svg" image-placement="stretch">
<View max-height="100" padding="0" margin="0" background-color="">
<View margin="2" padding="" background-color="00000000" flex-direction="column"
@@ -70,7 +70,8 @@
parameter="ifilt_onoff" name="Filters On/Off" tooltip="Turns the pre-processing filters on or off."/>
</View>
</View>
- <View display="tabbed" padding="0" background-color="FF31323A" lookAndFeel="MyLNF">
+ <View display="tabbed" padding="0" background-color="FF31323A" lookAndFeel="MyLNF"
+ flex-grow="1.5">
<View flex-direction="column" tab-color="" background-color="FF31323A"
padding="0" tab-caption="Tape" margin="0">
<View margin="0" padding="0" flex-grow="0.05" background-color="00000000"/>
@@ -85,6 +86,19 @@
parameter="hyst_onoff" name="Tape On/Off" tooltip="Turns the tape processing on or off."/>
</View>
<View flex-direction="column" tab-color="" background-color="FF31323A"
+ padding="0" tab-caption="Comp" margin="0">
+ <View margin="0" padding="0" flex-grow="0.05" background-color="00000000"/>
+ <Slider caption="Amount [dB]" parameter="comp_amt" class="Slider" name="Compression Amount"
+ padding="0" margin="0" tooltip="Controls the amount of tape compression applied by the effect."/>
+ <Slider caption="Attack [ms]" parameter="comp_attack" class="Slider"
+ name="Compression Attack" padding="0" margin="0" tooltip="Controls the attack speed of the tape compression."/>
+ <Slider caption="Release [ms]" parameter="comp_release" class="Slider"
+ name="Compression Release" padding="0" margin="0" tooltip="Controls the release speed of the tape compression."/>
+ <PowerButton flex-grow="1.0" margin="0" padding="0" background-color="00000000"
+ button-on-color="FFEAA92C" min-height="20" max-height="25" button-color="ff595c6b"
+ parameter="comp_onoff" name="Comp. On/Off" tooltip="Turns the tape compression on or off."/>
+ </View>
+ <View flex-direction="column" tab-color="" background-color="FF31323A"
padding="0" tab-caption="Tone" margin="0">
<View margin="0" padding="0" flex-grow="0.05" background-color="00000000"/>
<Slider caption="Treble" parameter="h_treble" class="Slider" name="Treble"
@@ -231,7 +245,7 @@
</View>
</View>
</View>
- <TooltipComp flex-grow="0.15" background-color="00000000" tooltip-name="FFEAA92C"
+ <TooltipComp flex-grow="0.13" background-color="00000000" tooltip-name="FFEAA92C"
tooltip-text="FFFFFFFF"/>
<View max-height="35" margin="0" padding="0" background-color="FF31323A"
flex-grow="0.1">
diff --git a/Plugin/Source/GUI/OnOff/OnOffManager.cpp b/Plugin/Source/GUI/OnOff/OnOffManager.cpp
@@ -11,6 +11,7 @@ const std::unordered_map<String, StringArray> triggerMap {
{ String ("chew_onoff"), StringArray ({ "Chew Depth", "Chew Frequency", "Chew Variance" }) },
{ String ("deg_onoff"), StringArray ({ "Depth", "Amount", "Variance", "Envelope", "0.1x" }) },
{ String ("flutter_onoff"), StringArray ({ "Flutter Depth", "Flutter Rate", "Wow Depth", "Wow Rate", "Wow Variance", "Wow Drift" }) },
+ { String ("comp_onoff"), StringArray ({ "Compression Amount", "Compression Attack", "Compression Release" }) },
};
void toggleEnableDisable (Component* root, StringArray& compNames, bool shouldBeEnabled)
diff --git a/Plugin/Source/PluginProcessor.cpp b/Plugin/Source/PluginProcessor.cpp
@@ -32,6 +32,7 @@ ChowtapeModelAudioProcessor::ChowtapeModelAudioProcessor()
vts (*this, nullptr, Identifier ("Parameters"), createParameterLayout()),
inputFilters (vts),
toneControl (vts),
+ compressionProcessor (vts),
hysteresis (vts, *this),
degrade (vts),
chewer (vts),
@@ -70,6 +71,7 @@ AudioProcessorValueTreeState::ParameterLayout ChowtapeModelAudioProcessor::creat
InputFilters::createParameterLayout (params);
ToneControl::createParameterLayout (params);
+ CompressionProcessor::createParameterLayout (params);
HysteresisProcessor::createParameterLayout (params);
LossFilter::createParameterLayout (params);
WowFlutterProcessor::createParameterLayout (params);
@@ -163,6 +165,7 @@ void ChowtapeModelAudioProcessor::prepareToPlay (double sampleRate, int samplesP
inGain.prepareToPlay (sampleRate, samplesPerBlock);
inputFilters.prepareToPlay (sampleRate, samplesPerBlock);
toneControl.prepare (sampleRate);
+ compressionProcessor.prepare (sampleRate, samplesPerBlock);
hysteresis.prepareToPlay (sampleRate, samplesPerBlock);
degrade.prepareToPlay (sampleRate, samplesPerBlock);
chewer.prepare (sampleRate, samplesPerBlock);
@@ -190,7 +193,7 @@ void ChowtapeModelAudioProcessor::releaseResources()
float ChowtapeModelAudioProcessor::calcLatencySamples() const noexcept
{
- return lossFilter.getLatencySamples() + hysteresis.getLatencySamples();
+ return lossFilter.getLatencySamples() + hysteresis.getLatencySamples() + compressionProcessor.getLatencySamples();
}
#ifndef JucePlugin_PreferredChannelConfigurations
@@ -244,6 +247,7 @@ void ChowtapeModelAudioProcessor::processBlock (AudioBuffer<float>& buffer, Midi
scope->pushSamplesIO (buffer, TapeScope::AudioType::Input);
toneControl.processBlockIn (buffer);
+ compressionProcessor.processBlock (buffer);
hysteresis.processBlock (buffer, midiMessages);
toneControl.processBlockOut (buffer);
chewer.processBlock (buffer);
diff --git a/Plugin/Source/PluginProcessor.h b/Plugin/Source/PluginProcessor.h
@@ -16,6 +16,7 @@
#include "MixGroups/MixGroupsController.h"
#include "Presets/PresetManager.h"
#include "Processors/Chew/ChewProcessor.h"
+#include "Processors/Compression/CompressionProcessor.h"
#include "Processors/Degrade/DegradeProcessor.h"
#include "Processors/DryWetProcessor.h"
#include "Processors/GainProcessor.h"
@@ -91,6 +92,7 @@ private:
GainProcessor inGain;
InputFilters inputFilters;
ToneControl toneControl;
+ CompressionProcessor compressionProcessor;
HysteresisProcessor hysteresis;
DegradeProcessor degrade;
ChewProcessor chewer;
diff --git a/Plugin/Source/Processors/CMakeLists.txt b/Plugin/Source/Processors/CMakeLists.txt
@@ -1,5 +1,6 @@
target_sources(CHOWTapeModel PRIVATE
Chew/ChewProcessor.cpp
+ Compression/CompressionProcessor.cpp
Degrade/DegradeProcessor.cpp
Hysteresis/HysteresisProcessing.cpp
diff --git a/Plugin/Source/Processors/Compression/CompressionProcessor.cpp b/Plugin/Source/Processors/Compression/CompressionProcessor.cpp
@@ -0,0 +1,124 @@
+#include "CompressionProcessor.h"
+
+CompressionProcessor::CompressionProcessor (AudioProcessorValueTreeState& vts)
+{
+ onOff = vts.getRawParameterValue ("comp_onoff");
+ amountParam = vts.getRawParameterValue ("comp_amt");
+ attackParam = vts.getRawParameterValue ("comp_attack");
+ releaseParam = vts.getRawParameterValue ("comp_release");
+}
+
+void CompressionProcessor::createParameterLayout (std::vector<std::unique_ptr<RangedAudioParameter>>& params)
+{
+ auto twoDecimalFloat = [] (float value, int) { return String (value, 2); };
+
+ params.push_back (std::make_unique<AudioParameterBool> ("comp_onoff", "Compression On/Off", false));
+
+ static NormalisableRange<float> amtRange { 0.0f, 9.0f };
+ amtRange.setSkewForCentre (3.0f);
+ params.push_back (std::make_unique<AudioParameterFloat> ("comp_amt", "Compression Amount", amtRange, 0.0f, String(), AudioProcessorParameter::genericParameter, twoDecimalFloat));
+
+ static NormalisableRange<float> attRange { 0.1f, 50.0f };
+ attRange.setSkewForCentre (10.0f);
+ params.push_back (std::make_unique<AudioParameterFloat> ("comp_attack", "Compression Attack", attRange, 5.0f, String(), AudioProcessorParameter::genericParameter, twoDecimalFloat));
+
+ static NormalisableRange<float> relRange { 10.0f, 1000.0f };
+ relRange.setSkewForCentre (100.0f);
+ params.push_back (std::make_unique<AudioParameterFloat> ("comp_release", "Compression Release", relRange, 200.0f, String(), AudioProcessorParameter::genericParameter, twoDecimalFloat));
+}
+
+void CompressionProcessor::prepare (double sr, int samplesPerBlock)
+{
+ oversample.initProcessing ((size_t) samplesPerBlock);
+ auto osFactor = oversample.getOversamplingFactor();
+ bypass.prepare (samplesPerBlock, bypass.toBool (onOff));
+
+ for (int ch = 0; ch < 2; ++ch)
+ {
+ slewLimiter[ch].prepare ({ sr, (uint32) samplesPerBlock, 1 });
+ dbPlusSmooth[ch].reset (sr, 0.05);
+ }
+
+ xDBVec.resize (osFactor * (size_t) samplesPerBlock, 0.0f);
+ compGainVec.resize (osFactor * (size_t) samplesPerBlock, 0.0f);
+}
+
+inline float compressionDB (float xDB, float dbPlus)
+{
+ auto window = 2.0f * dbPlus;
+
+ if (dbPlus <= 0.0f || xDB < -window)
+ return dbPlus;
+
+ return std::log (xDB + window + 1.0f) - dbPlus - xDB;
+}
+
+inline dsp::SIMDRegister<float> compressionDB (dsp::SIMDRegister<float> xDB, float dbPlus)
+{
+ using namespace chowdsp::SIMDUtils;
+
+ if (dbPlus <= 0.0f)
+ return (vec4) dbPlus;
+
+ auto window = 2.0f * dbPlus;
+ auto belowWin = vec4::lessThan (xDB, -window);
+ return ((logSIMD (xDB + window + 1.0f) - dbPlus - xDB) & ~belowWin) + ((vec4) dbPlus & belowWin);
+}
+
+void CompressionProcessor::processBlock (AudioBuffer<float>& buffer)
+{
+ if (! bypass.processBlockIn (buffer, bypass.toBool (onOff)))
+ return;
+
+ dsp::AudioBlock<float> block (buffer);
+ auto osBlock = oversample.processSamplesUp (block);
+
+ const auto numSamples = (int) osBlock.getNumSamples();
+ for (int ch = 0; ch < buffer.getNumChannels(); ++ch)
+ {
+ dbPlusSmooth[ch].setTargetValue (amountParam->load());
+
+ auto* x = osBlock.getChannelPointer ((size_t) ch);
+ FloatVectorOperations::copy (xDBVec.data(), x, numSamples);
+ FloatVectorOperations::abs (xDBVec.data(), xDBVec.data(), numSamples);
+
+ constexpr auto inc = dsp::SIMDRegister<float>::size();
+ size_t n = 0;
+ for (; n < (size_t) numSamples; n += inc)
+ {
+ auto xDB = dsp::SIMDRegister<float>::fromRawArray (&xDBVec[n]);
+
+ xDB = chowdsp::SIMDUtils::gainToDecibels (xDB);
+ auto compDB = compressionDB (xDB, dbPlusSmooth[ch].skip ((int) inc));
+ auto compGain = chowdsp::SIMDUtils::decibelsToGain (compDB);
+
+ xDB.copyToRawArray (&xDBVec[n]);
+ compGain.copyToRawArray (&compGainVec[n]);
+ }
+
+ // remaining samples that can't be vectorized
+ for (; n < (size_t) numSamples; ++n)
+ {
+ xDBVec[n] = Decibels::gainToDecibels (xDBVec[n]);
+ auto compDB = compressionDB (xDBVec[n], dbPlusSmooth[ch].getNextValue());
+ compGainVec[n] = Decibels::decibelsToGain (compDB);
+ }
+
+ // since the slew will be applied to the gain, we need to reverse the attack and release parameters!
+ slewLimiter[ch].setParameters (releaseParam->load(), attackParam->load());
+ for (size_t n = 0; n < (size_t) numSamples; ++n)
+ compGainVec[n] = jmin (compGainVec[n], slewLimiter[ch].processSample (compGainVec[n]));
+
+ FloatVectorOperations::multiply (x, compGainVec.data(), numSamples);
+ }
+
+ oversample.processSamplesDown (block);
+
+ bypass.processBlockOut (buffer, bypass.toBool (onOff));
+}
+
+float CompressionProcessor::getLatencySamples() const noexcept
+{
+ return onOff->load() == 1.0f ? oversample.getLatencyInSamples() // on
+ : 0.0f; // off
+}
diff --git a/Plugin/Source/Processors/Compression/CompressionProcessor.h b/Plugin/Source/Processors/Compression/CompressionProcessor.h
@@ -0,0 +1,38 @@
+#ifndef COMPRESSIONPROCESSOR_H_INCLUDED
+#define COMPRESSIONPROCESSOR_H_INCLUDED
+
+#include "../BypassProcessor.h"
+#include <xsimd/xsimd.hpp>
+
+class CompressionProcessor
+{
+public:
+ CompressionProcessor (AudioProcessorValueTreeState& vts);
+
+ static void createParameterLayout (std::vector<std::unique_ptr<RangedAudioParameter>>& params);
+
+ void prepare (double sr, int samplesPerBlock);
+ void processBlock (AudioBuffer<float>& buffer);
+
+ float getLatencySamples() const noexcept;
+
+private:
+ std::atomic<float>* onOff = nullptr;
+ std::atomic<float>* amountParam = nullptr;
+ std::atomic<float>* attackParam = nullptr;
+ std::atomic<float>* releaseParam = nullptr;
+
+ chowdsp::LevelDetector<float> slewLimiter[2];
+ BypassProcessor bypass;
+
+ dsp::Oversampling<float> oversample { 2, 1, dsp::Oversampling<float>::filterHalfBandPolyphaseIIR, true, true };
+
+ SmoothedValue<float, ValueSmoothingTypes::Linear> dbPlusSmooth[2];
+
+ std::vector<float, XSIMD_DEFAULT_ALLOCATOR (float)> xDBVec;
+ std::vector<float, XSIMD_DEFAULT_ALLOCATOR (float)> compGainVec;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CompressionProcessor)
+};
+
+#endif // COMPRESSIONPROCESSOR_H_INCLUDED
diff --git a/Plugin/modules/CMakeLists.txt b/Plugin/modules/CMakeLists.txt
@@ -29,8 +29,6 @@ target_link_libraries(juce_plugin_modules
warning_flags
)
-target_include_directories(juce_plugin_modules PUBLIC RTNeural/modules/xsimd/include)
-
target_compile_definitions(juce_plugin_modules
PUBLIC
JUCE_DISPLAY_SPLASH_SCREEN=0
@@ -50,6 +48,8 @@ target_compile_definitions(juce_plugin_modules
)
target_include_directories(juce_plugin_modules
+ PUBLIC
+ RTNeural/modules/xsimd/include
INTERFACE
$<TARGET_PROPERTY:juce_plugin_modules,INCLUDE_DIRECTORIES>
)
diff --git a/Simulations/compression.py b/Simulations/compression.py
@@ -0,0 +1,50 @@
+
+# %%
+import numpy as np
+import matplotlib.pyplot as plt
+
+# %%
+def db2lin(x):
+ return 10**(x / 20)
+
+def lin2db(x):
+ return 20 * np.log10(np.abs(x) + 1.0e-24)
+
+
+def saturate_db(x_db, db_plus):
+ window = 2 * db_plus if db_plus > 0 else -1000
+ y_db = np.zeros_like(x_db)
+ for idx, x_val in enumerate(x_db):
+ y_val = x_val + db_plus
+ if x_val >= -window:
+ y_val = np.log(x_val + window + 1) - db_plus
+
+ y_db[idx] = y_val
+
+ return y_db
+
+def saturate(x, db_plus):
+ sign = np.sign(x)
+ x_db = lin2db(x)
+ y_db = saturate_db(x_db, db_plus)
+ y = db2lin(y_db) * sign
+ return y
+
+# %%
+x = np.linspace(-18, 6)
+# x = np.linspace(-13, -11)
+
+for db in [0, 1, 2, 3, 4, 5, 6]:
+ plt.plot(x, saturate_db(x, db))
+
+plt.grid()
+
+# %%
+x = np.linspace(-2, 2)
+
+for db in [0, 1, 2, 3, 4, 5, 6]:
+ plt.plot(x, saturate(x, db))
+
+plt.grid()
+
+# %%