AnalogTapeModel

Physical modelling signal processing for analog tape recording
Log | Files | Refs | Submodules | README | LICENSE

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:
MCHANGELOG.md | 1+
MPlugin/Source/GUI/Assets/gui.xml | 20+++++++++++++++++---
MPlugin/Source/GUI/OnOff/OnOffManager.cpp | 1+
MPlugin/Source/PluginProcessor.cpp | 6+++++-
MPlugin/Source/PluginProcessor.h | 2++
MPlugin/Source/Processors/CMakeLists.txt | 1+
APlugin/Source/Processors/Compression/CompressionProcessor.cpp | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APlugin/Source/Processors/Compression/CompressionProcessor.h | 38++++++++++++++++++++++++++++++++++++++
MPlugin/modules/CMakeLists.txt | 4++--
ASimulations/compression.py | 50++++++++++++++++++++++++++++++++++++++++++++++++++
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() + +# %%