AnalogTapeModel

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

commit 573e0ad2c21c319d645d535bb1a195e9440c1bf6
parent cad2f5cb6fdb593866a5e05430cc9dec8d634e60
Author: jatinchowdhury18 <jatinchowdhury18@gmail.com>
Date:   Mon,  7 Sep 2020 15:24:23 -0700

Pre/Post Emphasis Filters (#85)

* Update DC blocker to use generalised IIR form

* Add DSP for tone control

* Finish implementing emphasis filters with controls

* Update presets

* Add virtual destructor to IIRFilter

Co-authored-by: jatinchowdhury18 <jatinchowdhury18@users.noreply.github.com>
Diffstat:
MPlugin/CHOWTapeModel.jucer | 11++++++-----
MPlugin/Source/GUI/Assets/gui.xml | 24+++++++++++++++++-------
MPlugin/Source/PluginProcessor.cpp | 5+++++
MPlugin/Source/PluginProcessor.h | 3++-
MPlugin/Source/Presets/PresetConfigs/Default.xml | 5++++-
MPlugin/Source/Presets/PresetConfigs/LoFi.xml | 5++++-
MPlugin/Source/Presets/PresetConfigs/OldTape.xml | 5++++-
MPlugin/Source/Presets/PresetConfigs/TC260.xml | 5++++-
MPlugin/Source/Presets/PresetConfigs/Underbiased.xml | 5++++-
MPlugin/Source/Presets/PresetConfigs/WoozyChorus.xml | 5++++-
APlugin/Source/Processors/BilinearUtils.h | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APlugin/Source/Processors/Hysteresis/DCBlocker.h | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
DPlugin/Source/Processors/Hysteresis/DCFilters.h | 118-------------------------------------------------------------------------------
MPlugin/Source/Processors/Hysteresis/HysteresisProcessor.cpp | 23+++--------------------
MPlugin/Source/Processors/Hysteresis/HysteresisProcessor.h | 4++--
APlugin/Source/Processors/Hysteresis/ToneControl.cpp | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APlugin/Source/Processors/Hysteresis/ToneControl.h | 42++++++++++++++++++++++++++++++++++++++++++
APlugin/Source/Processors/Hysteresis/ToneFilter.h | 44++++++++++++++++++++++++++++++++++++++++++++
APlugin/Source/Processors/IIRFilter.h | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
DPlugin/Source/Processors/Timing_Effects/DelayProcessor.cpp | 44--------------------------------------------
DPlugin/Source/Processors/Timing_Effects/DelayProcessor.h | 79-------------------------------------------------------------------------------
MPlugin/Source/Processors/Timing_Effects/Flutter.cpp | 3+--
MPlugin/Source/Processors/Timing_Effects/Flutter.h | 5++---
23 files changed, 464 insertions(+), 287 deletions(-)

diff --git a/Plugin/CHOWTapeModel.jucer b/Plugin/CHOWTapeModel.jucer @@ -62,7 +62,7 @@ file="Source/Processors/Degrade/DegradeProcessor.h"/> </GROUP> <GROUP id="{6052B1B0-83EF-DBFA-991C-FC0B47A949C9}" name="Hysteresis"> - <FILE id="l6IKp3" name="DCFilters.h" compile="0" resource="0" file="Source/Processors/Hysteresis/DCFilters.h"/> + <FILE id="l6IKp3" name="DCBlocker.h" compile="0" resource="0" file="Source/Processors/Hysteresis/DCBlocker.h"/> <FILE id="Qe4tlV" name="HysteresisProcessing.cpp" compile="1" resource="0" file="Source/Processors/Hysteresis/HysteresisProcessing.cpp"/> <FILE id="OYS18C" name="HysteresisProcessing.h" compile="0" resource="0" @@ -71,6 +71,9 @@ file="Source/Processors/Hysteresis/HysteresisProcessor.cpp"/> <FILE id="TRDp2E" name="HysteresisProcessor.h" compile="0" resource="0" file="Source/Processors/Hysteresis/HysteresisProcessor.h"/> + <FILE id="yqa9LH" name="ToneControl.cpp" compile="1" resource="0" file="Source/Processors/Hysteresis/ToneControl.cpp"/> + <FILE id="vd8RTB" name="ToneControl.h" compile="0" resource="0" file="Source/Processors/Hysteresis/ToneControl.h"/> + <FILE id="Gxo9aB" name="ToneFilter.h" compile="0" resource="0" file="Source/Processors/Hysteresis/ToneFilter.h"/> </GROUP> <GROUP id="{37F4BCFA-28D3-CD4D-17AF-3C696E7EC8DA}" name="Loss_Effects"> <FILE id="gJA2Gi" name="FIRFilter.h" compile="0" resource="0" file="Source/Processors/Loss_Effects/FIRFilter.h"/> @@ -78,16 +81,14 @@ <FILE id="ZNErgZ" name="LossFilter.h" compile="0" resource="0" file="Source/Processors/Loss_Effects/LossFilter.h"/> </GROUP> <GROUP id="{0C000F30-53FD-3EFB-FAEA-6321B08AE56A}" name="Timing_Effects"> - <FILE id="kv0dir" name="DelayProcessor.cpp" compile="1" resource="0" - file="Source/Processors/Timing_Effects/DelayProcessor.cpp"/> - <FILE id="ZrrIc3" name="DelayProcessor.h" compile="0" resource="0" - file="Source/Processors/Timing_Effects/DelayProcessor.h"/> <FILE id="cVSIAR" name="Flutter.cpp" compile="1" resource="0" file="Source/Processors/Timing_Effects/Flutter.cpp"/> <FILE id="Wz3lz6" name="Flutter.h" compile="0" resource="0" file="Source/Processors/Timing_Effects/Flutter.h"/> </GROUP> + <FILE id="jlO9LL" name="BilinearUtils.h" compile="0" resource="0" file="Source/Processors/BilinearUtils.h"/> <FILE id="YNkJOh" name="DryWetProcessor.h" compile="0" resource="0" file="Source/Processors/DryWetProcessor.h"/> <FILE id="zwLvQ9" name="GainProcessor.h" compile="0" resource="0" file="Source/Processors/GainProcessor.h"/> + <FILE id="SSnw2J" name="IIRFilter.h" compile="0" resource="0" file="Source/Processors/IIRFilter.h"/> </GROUP> <FILE id="zPJjtw" name="PluginProcessor.cpp" compile="1" resource="0" file="Source/PluginProcessor.cpp"/> diff --git a/Plugin/Source/GUI/Assets/gui.xml b/Plugin/Source/GUI/Assets/gui.xml @@ -52,13 +52,23 @@ <Slider caption="Output Gain [dB]" parameter="outgain" class="Slider" name="Output Gain" tooltip="Sets the output gain from the tape model in Decibels."/> </View> - <View flex-direction="column" tab-color="" background-color="FF31323A" - padding="0"> - <Slider caption="Bias" parameter="width" class="Slider" name="Bias" tooltip="Controls the amount of bias used by the tape recorder. Turning down the bias can create &quot;deadzone&quot; distortion."/> - <Slider caption="Saturation" parameter="sat" class="Slider" name="Saturation" - tooltip="Controls the amount of tape saturation applied to the signal."/> - <Slider caption="Drive" parameter="drive" class="Slider" name="Drive" - tooltip="Controls the amount of amplification done during the tape magnetisation process. Note that unlike the &quot;Input Gain&quot;, this amplification is highly nonlinear."/> + <View display="tabbed" padding="0" background-color="FF31323A" lookAndFeel="MyLNF"> + <View flex-direction="column" tab-color="" background-color="FF31323A" + padding="0" tab-caption="Tape"> + <Slider caption="Bias" parameter="width" class="Slider" name="Bias" + padding="0" margin="0" tooltip="Controls the amount of bias used by the tape recorder. Turning down the bias can create &quot;deadzone&quot; distortion."/> + <Slider caption="Saturation" parameter="sat" class="Slider" name="Saturation" + padding="0" margin="0" tooltip="Controls the amount of tape saturation applied to the signal."/> + <Slider caption="Drive" parameter="drive" class="Slider" name="Drive" + padding="0" margin="0" tooltip="Controls the amount of amplification done during the tape magnetisation process. Note that unlike the &quot;Input Gain&quot;, this amplification is highly nonlinear."/> + </View> + <View flex-direction="column" tab-color="" background-color="FF31323A" + padding="0" tab-caption="Tone"> + <Slider caption="Bass" parameter="h_bass" class="Slider" name="Bass" + tooltip="Controls the bass response of the pre/post-emphasis filters."/> + <Slider caption="Treble" parameter="h_treble" class="Slider" name="Treble" + tooltip="Controls the treble response of the pre/post-emphasis filters."/> + </View> </View> <View display="tabbed" padding="0" background-color="FF31323A" flex-grow="1.5" lookAndFeel="MyLNF"> diff --git a/Plugin/Source/PluginProcessor.cpp b/Plugin/Source/PluginProcessor.cpp @@ -26,6 +26,7 @@ ChowtapeModelAudioProcessor::ChowtapeModelAudioProcessor() ), #endif vts (*this, nullptr, Identifier ("Parameters"), createParameterLayout()), + toneControl (vts), hysteresis (vts), degrade (vts), chewer (vts), @@ -54,6 +55,7 @@ AudioProcessorValueTreeState::ParameterLayout ChowtapeModelAudioProcessor::creat params.push_back (std::make_unique<AudioParameterFloat> ("drywet", "Dry/Wet", 0.0f, 100.0f, 100.0f)); params.push_back (std::make_unique<AudioParameterInt> ("preset", "Preset", 0, 10, 0)); + ToneControl::createParameterLayout (params); HysteresisProcessor::createParameterLayout (params); LossFilter::createParameterLayout (params); Flutter::createParameterLayout (params); @@ -141,6 +143,7 @@ void ChowtapeModelAudioProcessor::prepareToPlay (double sampleRate, int samplesP setRateAndBufferSizeDetails (sampleRate, samplesPerBlock); inGain.prepareToPlay (sampleRate, samplesPerBlock); + toneControl.prepare (sampleRate); hysteresis.prepareToPlay (sampleRate, samplesPerBlock); degrade.prepareToPlay (sampleRate, samplesPerBlock); chewer.prepare (sampleRate); @@ -208,7 +211,9 @@ void ChowtapeModelAudioProcessor::processBlock (AudioBuffer<float>& buffer, Midi dryBuffer.makeCopyOf (buffer, true); inGain.processBlock (buffer, midiMessages); + toneControl.processBlockIn (buffer); hysteresis.processBlock (buffer, midiMessages); + toneControl.processBlockOut (buffer); chewer.processBlock (buffer); degrade.processBlock (buffer, midiMessages); diff --git a/Plugin/Source/PluginProcessor.h b/Plugin/Source/PluginProcessor.h @@ -13,12 +13,12 @@ #include "../JuceLibraryCode/JuceHeader.h" #include "Processors/GainProcessor.h" #include "Processors/Hysteresis/HysteresisProcessor.h" +#include "Processors/Hysteresis/ToneControl.h" #include "Processors/Loss_Effects/LossFilter.h" #include "Processors/Timing_Effects/Flutter.h" #include "Processors/Degrade/DegradeProcessor.h" #include "Processors/Chew/ChewProcessor.h" #include "Processors/DryWetProcessor.h" -#include "Processors/Timing_Effects/DelayProcessor.h" #include "Presets/PresetManager.h" #include "GUI/MyLNF.h" #include "GUI/AutoUpdating.h" @@ -76,6 +76,7 @@ private: AudioProcessorValueTreeState vts; GainProcessor inGain; + ToneControl toneControl; HysteresisProcessor hysteresis; DegradeProcessor degrade; ChewProcessor chewer; diff --git a/Plugin/Source/Presets/PresetConfigs/Default.xml b/Plugin/Source/Presets/PresetConfigs/Default.xml @@ -11,8 +11,11 @@ <PARAM id="drive" value="0.5"/> <PARAM id="drywet" value="100.0"/> <PARAM id="gap" value="9.999999974752427e-7"/> + <PARAM id="h_bass" value="0.0"/> + <PARAM id="h_treble" value="0.0"/> <PARAM id="ingain" value="0.0"/> - <PARAM id="os" value="1.0"/> + <PARAM id="mode" value="0"/> + <PARAM id="os" value="1"/> <PARAM id="outgain" value="0.0"/> <PARAM id="rate" value="0.2999999821186066"/> <PARAM id="sat" value="0.5"/> diff --git a/Plugin/Source/Presets/PresetConfigs/LoFi.xml b/Plugin/Source/Presets/PresetConfigs/LoFi.xml @@ -11,8 +11,11 @@ <PARAM id="drive" value="1.0"/> <PARAM id="drywet" value="100.0"/> <PARAM id="gap" value="9.999999974752427e-7"/> + <PARAM id="h_bass" value="0.0"/> + <PARAM id="h_treble" value="0.0"/> <PARAM id="ingain" value="0.0"/> - <PARAM id="os" value="1.0"/> + <PARAM id="mode" value="2"/> + <PARAM id="os" value="2"/> <PARAM id="outgain" value="-3.5"/> <PARAM id="rate" value="0.2999999821186066"/> <PARAM id="sat" value="1.0"/> diff --git a/Plugin/Source/Presets/PresetConfigs/OldTape.xml b/Plugin/Source/Presets/PresetConfigs/OldTape.xml @@ -11,8 +11,11 @@ <PARAM id="drive" value="0.5"/> <PARAM id="drywet" value="100.0"/> <PARAM id="gap" value="9.999999974752427e-7"/> + <PARAM id="h_bass" value="0.0"/> + <PARAM id="h_treble" value="0.0"/> <PARAM id="ingain" value="0.0"/> - <PARAM id="os" value="1.0"/> + <PARAM id="mode" value="0"/> + <PARAM id="os" value="1"/> <PARAM id="outgain" value="0.0"/> <PARAM id="rate" value="0.2999999821186066"/> <PARAM id="sat" value="0.5"/> diff --git a/Plugin/Source/Presets/PresetConfigs/TC260.xml b/Plugin/Source/Presets/PresetConfigs/TC260.xml @@ -11,8 +11,11 @@ <PARAM id="drive" value="0.6599999666213989"/> <PARAM id="drywet" value="100.0"/> <PARAM id="gap" value="1.614829443497001e-6"/> + <PARAM id="h_bass" value="0.0"/> + <PARAM id="h_treble" value="0.0"/> <PARAM id="ingain" value="0.0"/> - <PARAM id="os" value="1.0"/> + <PARAM id="mode" value="2"/> + <PARAM id="os" value="2"/> <PARAM id="outgain" value="-2.0"/> <PARAM id="rate" value="0.199999988079071"/> <PARAM id="sat" value="0.75"/> diff --git a/Plugin/Source/Presets/PresetConfigs/Underbiased.xml b/Plugin/Source/Presets/PresetConfigs/Underbiased.xml @@ -11,8 +11,11 @@ <PARAM id="drive" value="0.25"/> <PARAM id="drywet" value="100.0"/> <PARAM id="gap" value="9.999999974752427e-7"/> + <PARAM id="h_bass" value="0.0"/> + <PARAM id="h_treble" value="0.0"/> <PARAM id="ingain" value="0.0"/> - <PARAM id="os" value="1.0"/> + <PARAM id="mode" value="0"/> + <PARAM id="os" value="1"/> <PARAM id="outgain" value="0.0"/> <PARAM id="rate" value="0.2999999821186066"/> <PARAM id="sat" value="0.699999988079071"/> diff --git a/Plugin/Source/Presets/PresetConfigs/WoozyChorus.xml b/Plugin/Source/Presets/PresetConfigs/WoozyChorus.xml @@ -11,8 +11,11 @@ <PARAM id="drive" value="0.5"/> <PARAM id="drywet" value="65.0"/> <PARAM id="gap" value="9.999999974752427e-7"/> + <PARAM id="h_bass" value="0.0"/> + <PARAM id="h_treble" value="0.0"/> <PARAM id="ingain" value="0.0"/> - <PARAM id="os" value="1.0"/> + <PARAM id="mode" value="0"/> + <PARAM id="os" value="1"/> <PARAM id="outgain" value="0.0"/> <PARAM id="rate" value="0.4399999976158142"/> <PARAM id="sat" value="0.5"/> diff --git a/Plugin/Source/Processors/BilinearUtils.h b/Plugin/Source/Processors/BilinearUtils.h @@ -0,0 +1,56 @@ +#pragma once + +#include <cmath> + +namespace Bilinear +{ + +/** Dummy generic bilinear transform (@TODO: actually implement this) */ +template<typename T, size_t N> +struct BilinearTransform +{ + static inline void call (T (&b)[N], T (&a)[N], T (&bs)[N], T (&as)[N], T K); +}; + +/** Bilinear transform for a first-order filter */ +template<typename T> +struct BilinearTransform<T, 2> +{ + static inline void call (T (&b)[2], T (&a)[2], T (&bs)[2], T (&as)[2], T K) + { + const auto a0 = as[0] * K + as[1]; + b[0] = ( bs[0] * K + bs[1]) / a0; + b[1] = (-bs[0] * K + bs[1]) / a0; + a[0] = 1.0f; + a[1] = (-as[0] * K + as[1]) / a0; + } +}; + +/** Bilinear transform for a second-order filter */ +template<typename T> +struct BilinearTransform<T, 3> +{ + static inline void call (T (&b)[3], T (&a)[3], T (&bs)[3], T (&as)[3], T K) + { + const auto KSq = K * K; + const float a0 = as[0] * KSq + as[1] * K + as[2]; + + a[0] = 1.0f; + a[1] = 2.0f * (as[2] - as[0] * KSq) / a0; + a[2] = (as[0] * KSq - as[1] * K + as[2]) / a0; + b[0] = (bs[0] * KSq + bs[1] * K + bs[2]) / a0; + b[1] = 2.0f * (bs[2] - bs[0] * KSq) / a0; + b[2] = (bs[0] * KSq - bs[1] * K + bs[2]) / a0; + } +}; + +inline float calcPoleFreq (float a, float b, float c) +{ + auto radicand = b*b - 4.0f*a*c; + if (radicand >= 0.0f) + return 0.0f; + + return std::sqrt (-radicand) / (2.0f * a); +} + +} // Bilinear diff --git a/Plugin/Source/Processors/Hysteresis/DCBlocker.h b/Plugin/Source/Processors/Hysteresis/DCBlocker.h @@ -0,0 +1,67 @@ +#ifndef DCBLOCKER_H_INCLUDED +#define DCBLOCKER_H_INCLUDED + +#include <JuceHeader.h> +#include "../IIRFilter.h" + +/** DC blocking filter */ +class DCBlocker +{ +public: + DCBlocker() = default; + + /** Prepare the DC blocker to process samples + * + * @param dcFreq: the cutoff frequency of the highpass filter. + */ + void prepare (double sampleRate, float dcFreq) + { + hpf[0].reset(); + hpf[1].reset(); + + fs = (float) sampleRate; + + calcCoefs (dcFreq); + } + + void calcCoefs (float fc) + { + // Q values for 4th-order Butterworth filter + // (https://en.wikipedia.org/wiki/Butterworth_filter#Normalized_Butterworth_polynomials) + constexpr float Qs[] = { 1.0f / 0.7654f, 1.0f / 1.8478f }; + + float wc = MathConstants<float>::twoPi * fc / fs; + float c = 1.0f / dsp::FastMathApproximations::tan (wc / 2.0f); + float phi = c * c; + + float b[3], a[3]; + for (int i = 0; i < 2; ++i) + { + float K = c / Qs[i]; + float a0 = phi + K + 1.0f; + + b[0] = phi / a0; + b[1] = -2.0f * b[0]; + b[2] = b[0]; + a[1] = 2.0f * (1.0f - phi) / a0; + a[2] = (phi - K + 1.0f) / a0; + + hpf[i].setCoefs (b, a); + } + } + + void processBlock (float* buffer, const int numSamples) + { + hpf[0].processBlock (buffer, numSamples); + hpf[1].processBlock (buffer, numSamples); + } + +private: + chowdsp::IIRFilter<2> hpf[2]; + + float fs = 44100.0f; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DCBlocker) +}; + +#endif // DCBLOCKER_H_INCLUDED diff --git a/Plugin/Source/Processors/Hysteresis/DCFilters.h b/Plugin/Source/Processors/Hysteresis/DCFilters.h @@ -1,118 +0,0 @@ -#ifndef DCFILTERS_H_INCLUDED -#define DCFILTERS_H_INCLUDED - -#include "JuceHeader.h" - -/* High-pass filter to compensate for low frequency noise of transformer */ -class TransformerHPF -{ -public: - TransformerHPF() {} - - void reset (double sampleRate) - { - for (int n = 0; n < 3; ++n) - z[n] = 0.0f; - - fs = (float) sampleRate; - } - - void calcCoefs (float fc, float Q) - { - float wc = MathConstants<float>::twoPi * fc / fs; - float c = 1.0f / dsp::FastMathApproximations::tan (wc / 2.0f); - float phi = c * c; - float K = c / Q; - float a0 = phi + K + 1.0f; - - b[0] = phi / a0; - b[1] = -2.0f * b[0]; - b[2] = b[0]; - a[1] = 2.0f * (1.0f - phi) / a0; - a[2] = (phi - K + 1.0f) / a0; - } - - void processBlock (float* buffer, const int numSamples) - { - for (int n = 0; n < numSamples; ++n) - buffer[n] = processSample (buffer[n]); - } - - inline float processSample (float x) noexcept - { - // direct form II transposed - float y = z[1] + x * b[0]; - - z[1] = z[2] + x*b[1] - y*a[1]; - z[2] = x*b[2] - y*a[2]; - - return y; - } - -private: - float a[3] = { 0.0f, 0.0f, 0.0f }; - float b[3] = { 1.0f, 0.0f, 0.0f }; - - float z[3] = { 0.0f, 0.0f, 0.0f }; - - float fs = 44100.0f; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TransformerHPF) -}; - -/* Low-shelf filter to compensate for low frequency noise of transformer -class TransformerShelf -{ -public: -TransformerShelf() {} - -void reset (double sampleRate) -{ -for (int n = 0; n < 3; ++n) -z[n] = 0.0f; - -fs = (float) sampleRate; -} - -void calcCoefs (float fc, float Q, float gain) -{ -float A = sqrtf (gain); -float wc = MathConstants<float>::twoPi * fc / fs; -float wS = dsp::FastMathApproximations::sin (wc); -float wC = dsp::FastMathApproximations::cos (wc); -float beta = sqrtf (A) / Q; - -float a0 = ((A+1.0f) + ((A-1.0f) * wC) + (beta*wS)); - -b[0] = A*((A+1.0f) - ((A-1.0f)*wC) + (beta*wS)) / a0; -b[1] = 2.0f*A * ((A-1.0f) - ((A+1.0f)*wC)) / a0; -b[2] = A*((A+1.0f) - ((A-1.0f)*wC) - (beta*wS)) / a0; - -a[1] = -2.0f * ((A-1.0f) + ((A+1.0f)*wC)) / a0; -a[2] = ((A+1.0f) + ((A-1.0f)*wC)-(beta*wS)) / a0; -} - -inline float processSample (float x) -{ -// direct form II transposed -float y = z[1] + x * b[0]; - -z[1] = z[2] + x*b[1] - y*a[1]; -z[2] = x*b[2] - y*a[2]; - -return y; -} - -private: -float a[3] = { 0.0f, 0.0f, 0.0f }; -float b[3] = { 1.0f, 0.0f, 0.0f }; - -float z[3] = { 0.0f, 0.0f, 0.0f }; - -float fs = 44100.0f; - -JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TransformerShelf) -}; -*/ - -#endif // DCFILTERS_H_INCLUDED diff --git a/Plugin/Source/Processors/Hysteresis/HysteresisProcessor.cpp b/Plugin/Source/Processors/Hysteresis/HysteresisProcessor.cpp @@ -133,17 +133,7 @@ void HysteresisProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) prevOS = curOS; for (int ch = 0; ch < 2; ++ch) - { - // Q values for 4th-order Butterworth filter - // (https://en.wikipedia.org/wiki/Butterworth_filter#Normalized_Butterworth_polynomials) - constexpr float orders[] = { 1.0f / 0.7654f, 1.0f / 1.8478f }; - - for (int order = 0; order < 2; ++order) - { - dcBlocker[ch][order].reset (sampleRate); - dcBlocker[ch][order].calcCoefs (dcFreq, orders[order]); - } - } + dcBlocker[ch].prepare (sampleRate, dcFreq); } void HysteresisProcessor::releaseResources() @@ -274,13 +264,6 @@ void HysteresisProcessor::processSmoothV1 (dsp::AudioBlock<float>& block) void HysteresisProcessor::applyDCBlockers (AudioBuffer<float>& buffer) { - for (int channel = 0; channel < buffer.getNumChannels(); ++channel) - { - for (int order = 0; order < 2; ++order) - { - auto* x = buffer.getWritePointer (channel); - for (int samp = 0; samp < buffer.getNumSamples(); samp++) - x[samp] = dcBlocker[channel][order].processSample (x[samp]); - } - } + for (int ch = 0; ch < buffer.getNumChannels(); ++ch) + dcBlocker[ch].processBlock (buffer.getWritePointer (ch), buffer.getNumSamples()); } diff --git a/Plugin/Source/Processors/Hysteresis/HysteresisProcessor.h b/Plugin/Source/Processors/Hysteresis/HysteresisProcessor.h @@ -2,7 +2,7 @@ #define HYSTERESISPROCESSOR_H_INCLUDED #include "HysteresisProcessing.h" -#include "DCFilters.h" +#include "DCBlocker.h" /* Hysteresis Processor for tape. */ class HysteresisProcessor @@ -53,7 +53,7 @@ private: int curOS = 0, prevOS = 0; HysteresisProcessing hProcs[2]; std::unique_ptr<dsp::Oversampling<float>> overSample[5]; // needs oversampling to avoid aliasing - TransformerHPF dcBlocker[2][2]; + DCBlocker dcBlocker[2]; int overSamplingFactor = 2; const float dcFreq = 35.0f; diff --git a/Plugin/Source/Processors/Hysteresis/ToneControl.cpp b/Plugin/Source/Processors/Hysteresis/ToneControl.cpp @@ -0,0 +1,109 @@ +#include "ToneControl.h" + +namespace +{ + constexpr double slewTime = 0.05; + constexpr float transFreq = 500.0f; + constexpr float dbScale = 12.0f; +} + +ToneStage::ToneStage() +{ + for (int ch = 0; ch < 2; ++ch) + { + lowGain[ch] = 1.0f; + highGain[ch] = 1.0f; + } +} + +void ToneStage::prepare (double sampleRate) +{ + fs = (float) sampleRate; + + for (int ch = 0; ch < 2; ++ch) + { + lowGain[ch].reset (sampleRate, slewTime); + lowGain[ch].setCurrentAndTargetValue (lowGain[ch].getTargetValue()); + highGain[ch].reset (sampleRate, slewTime); + highGain[ch].setCurrentAndTargetValue (highGain[ch].getTargetValue()); + + tone[ch].reset(); + tone[ch].calcCoefs (lowGain[ch].getTargetValue(), highGain[ch].getTargetValue(), transFreq, fs); + } +} + +void ToneStage::setLowGain (float lowGainDB) +{ + auto newLowGain = Decibels::decibelsToGain (lowGainDB); + if (newLowGain == lowGain[0].getTargetValue()) + return; + + lowGain[0].setTargetValue (newLowGain); + lowGain[1].setTargetValue (newLowGain); +} + +void ToneStage::setHighGain (float highGainDB) +{ + auto newHighGain = Decibels::decibelsToGain (highGainDB); + if (newHighGain == highGain[0].getTargetValue()) + return; + + highGain[0].setTargetValue (newHighGain); + highGain[1].setTargetValue (newHighGain); +} + +void ToneStage::processBlock (AudioBuffer<float>& buffer) +{ + for (int ch = 0; ch < 2; ++ch) + { + if (lowGain[ch].isSmoothing() || highGain[ch].isSmoothing()) + { + auto* x = buffer.getWritePointer (ch); + for (int n = 0; n < buffer.getNumSamples(); ++n) + { + tone[ch].calcCoefs (lowGain[ch].getNextValue(), highGain[ch].getNextValue(), transFreq, fs); + x[n] = tone[ch].processSample (x[n]); + } + } + else + { + tone[ch].processBlock (buffer.getWritePointer (ch), buffer.getNumSamples()); + } + } +} + +//=================================================== +ToneControl::ToneControl (AudioProcessorValueTreeState& vts) +{ + bassParam = vts.getRawParameterValue ("h_bass"); + trebleParam = vts.getRawParameterValue ("h_treble"); +} + +void ToneControl::createParameterLayout (std::vector<std::unique_ptr<RangedAudioParameter>>& params) +{ + params.push_back (std::make_unique<AudioParameterFloat> ("h_bass", "Bass", -1.0f, 1.0f, 0.0f)); + params.push_back (std::make_unique<AudioParameterFloat> ("h_treble", "Treble", -1.0f, 1.0f, 0.0f)); +} + +void ToneControl::prepare (double sampleRate) +{ + toneIn.prepare (sampleRate); + toneOut.prepare (sampleRate); +} + +void ToneControl::processBlockIn (AudioBuffer<float>& buffer) +{ + toneIn.setLowGain (dbScale * bassParam->load()); + toneIn.setHighGain (dbScale * trebleParam->load()); + + toneIn.processBlock (buffer); +} + +void ToneControl::processBlockOut (AudioBuffer<float>& buffer) +{ + toneOut.setLowGain (-1.0f * dbScale * bassParam->load()); + toneOut.setHighGain (-1.0f * dbScale * trebleParam->load()); + + toneOut.processBlock (buffer); +} + diff --git a/Plugin/Source/Processors/Hysteresis/ToneControl.h b/Plugin/Source/Processors/Hysteresis/ToneControl.h @@ -0,0 +1,42 @@ +#ifndef TONECONTROL_H_INCLUDED +#define TONECONTROL_H_INCLUDED + +#include "ToneFilter.h" + +using SmoothGain = SmoothedValue<float, ValueSmoothingTypes::Multiplicative>; + +struct ToneStage +{ + ToneFilter tone[2]; + SmoothGain lowGain[2], highGain[2]; + float fs = 44100.0f; + + ToneStage(); + + void prepare (double sampleRate); + void processBlock (AudioBuffer<float>& buffer); + void setLowGain (float lowGainDB); + void setHighGain (float highGainDB); +}; + +class ToneControl +{ +public: + ToneControl (AudioProcessorValueTreeState& vts); + + static void createParameterLayout (std::vector<std::unique_ptr<RangedAudioParameter>>& params); + void prepare (double sampleRate); + + void processBlockIn (AudioBuffer<float>& buffer); + void processBlockOut (AudioBuffer<float>& buffer); + +private: + ToneStage toneIn, toneOut; + + std::atomic<float>* bassParam = nullptr; + std::atomic<float>* trebleParam = nullptr; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ToneControl) +}; + +#endif // TONECONTROL_H_INCLUDED diff --git a/Plugin/Source/Processors/Hysteresis/ToneFilter.h b/Plugin/Source/Processors/Hysteresis/ToneFilter.h @@ -0,0 +1,44 @@ +#ifndef TONEFILTER_H_INCLUDED +#define TONEFILTER_H_INCLUDED + +#include "../IIRFilter.h" +#include "../BilinearUtils.h" + +/** A first order shelving filter, with a set gain at DC, +* a set gain at high frequencies, and a transition frequency. +*/ +class ToneFilter : public chowdsp::IIRFilter<1> +{ +public: + ToneFilter() = default; + + /** Calculates the coefficients for the filter. + * @param lowGain: the gain of the filter at low frequencies + * @param highGain: the gain of the filter at high frequencies + * @param fc: the transition frequency of the filter + * @param fs: the sample rate for the filter + */ + void calcCoefs (float lowGain, float highGain, float fc, float fs) + { + // reduce to simple gain element + if (lowGain == highGain) + { + this->b[0] = lowGain; this->b[1] = 0.0f; + this->a[0] = 1.0f; this->a[1] = 0.0f; + return; + } + + auto wc = MathConstants<float>::twoPi * fc; + auto p = std::sqrt (wc*wc * (highGain*highGain - lowGain*highGain) / (lowGain*highGain - lowGain*lowGain)); + auto K = p / std::tan (p / (2.0f * fs)); + + float bs[2] { highGain / p, lowGain }; + float as[2] { 1.0f / p, 1.0f }; + Bilinear::BilinearTransform<float, 2>::call (this->b, this->a, bs, as, K); + } + +private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ToneFilter) +}; + +#endif // TONEFILTER_H_INCLUDED diff --git a/Plugin/Source/Processors/IIRFilter.h b/Plugin/Source/Processors/IIRFilter.h @@ -0,0 +1,84 @@ +#pragma once + +#include <JuceHeader.h> + +namespace chowdsp +{ + +/** IIR filter of arbirtary order. + * Uses Transposed Direct Form II: + * https://ccrma.stanford.edu/~jos/fp/Transposed_Direct_Forms.html + */ +template<int order, typename FloatType=float> +class IIRFilter +{ +public: + IIRFilter() = default; + virtual ~IIRFilter() {} + + /** Reset filter state */ + virtual void reset() + { + std::fill (z, &z[order+1], 0.0f); + } + + /** Set coefficients to new values */ + virtual void setCoefs (const FloatType (&newB)[order+1], const FloatType (&newA)[order+1]) + { + std::copy (newB, &newB[order+1], b); + std::copy (newA, &newA[order+1], a); + } + + /** Optimized processing call for first-order filter */ + template <int N = order> + inline typename std::enable_if <N == 1, FloatType>::type + processSample (FloatType x) noexcept + { + FloatType y = z[1] + x * b[0]; + z[order] = x * b[order] - y * a[order]; + return y; + } + + /** Optimized processing call for second-order filter */ + template <int N = order> + inline typename std::enable_if <N == 2, FloatType>::type + processSample (FloatType x) noexcept + { + FloatType y = z[1] + x * b[0]; + z[1] = z[2] + x * b[1] - y * a[1]; + z[order] = x * b[order] - y * a[order]; + return y; + } + + /** Optimized processing call for Nth-order filter */ + template <int N = order> + inline typename std::enable_if <(N > 2), FloatType>::type + processSample (FloatType x) noexcept + { + FloatType y = z[1] + x * b[0]; + + for (int i = 1; i < order; ++i) + z[i] = z[i+1] + x * b[i] - y * a[i]; + + z[order] = x * b[order] - y * a[order]; + + return y; + } + + /** Process block of samples */ + virtual void processBlock (FloatType* block, const int numSamples) noexcept + { + for (int n = 0; n < numSamples; ++n) + block[n] = processSample (block[n]); + } + +protected: + FloatType a[order+1]; + FloatType b[order+1]; + FloatType z[order+1]; + +private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (IIRFilter) +}; + +} // chowdsp diff --git a/Plugin/Source/Processors/Timing_Effects/DelayProcessor.cpp b/Plugin/Source/Processors/Timing_Effects/DelayProcessor.cpp @@ -1,44 +0,0 @@ -#include "DelayProcessor.h" - -void DelayProcessor::setLengthMs (double lengthMs, bool force) -{ - auto newLength = lengthMs * sampleRate / 1000.0; - newLength = jmin (newLength, (double) bufferSize); - - if (! force) - length.setTargetValue ((float) newLength); - else - length.setCurrentAndTargetValue ((float) newLength); -} - -float DelayProcessor::getLengthMS () const -{ - return (length.getTargetValue() / sampleRate) * 1000.0f; -} - -void DelayProcessor::setFeedback (float newFeedback) -{ - feedback = jlimit<float> (0.0f, 0.9f, newFeedback); -} - -float DelayProcessor::getFeedback() const -{ - return feedback; -} - -void DelayProcessor::prepareToPlay (double sr, int /*maximumExpectedSamplesPerBlock*/) -{ - sampleRate = (float) sr; - bufferSize = (int) sampleRate * maxDelaySeconds; - delayBuffer.setSize (1, bufferSize); - delayBuffer.clear(); - - length.reset (sampleRate, 5e-3); - resetPtrs(); -} - -void DelayProcessor::process (float* buffer, int numSamples) -{ - for (int n = 0; n < numSamples; n++) - buffer[n] += delay (buffer[n]); -} diff --git a/Plugin/Source/Processors/Timing_Effects/DelayProcessor.h b/Plugin/Source/Processors/Timing_Effects/DelayProcessor.h @@ -1,79 +0,0 @@ -#ifndef DELAYPROCESSOR_H_INCLUDED -#define DELAYPROCESSOR_H_INCLUDED - -#include "JuceHeader.h" - -/** Mono Delay */ -class DelayProcessor -{ -public: - DelayProcessor() {} - - void prepareToPlay (double sampleRate, int maxExpectedBlockSize); - void process (float* buffer, int numSamples); - - inline float delay (float x) - { - // Erase head - const int erasePtr = negativeAwareModulo (readPtr - 1, bufferSize); - delayBuffer.setSample (0, erasePtr, 0.0f); - - // Write head - const float len = length.getNextValue(); - - float y = x; - if (len > 0) - { - // Read head - y = delayBuffer.getSample (0, readPtr); - - // write ptr - const float fractionSample = len - (int) len; - const int writePtr = (readPtr + (int) floorf (len)) % bufferSize; - - // feedback - float writeSample = x + y * feedback; - - delayBuffer.addSample (0, writePtr, writeSample * (1.0f - fractionSample)); - delayBuffer.addSample (0, (writePtr + 1) % bufferSize, writeSample * fractionSample); - } - - //update pointers - setReadPtr (bufferSize); - - return y; - } - - void setLengthMs (double lengthMs, bool force = false); - float getLengthMS() const; - - void setFeedback (float newFeedback); - float getFeedback() const; - -private: - enum - { - maxDelaySeconds = 5, - }; - - SmoothedValue<float, ValueSmoothingTypes::Linear> length = 0.0f; - float feedback = 0.0f; - int readPtr = 0; - - inline void resetPtrs() { readPtr = 0; } - inline void setReadPtr (int maxLength) - { - readPtr++; - if (readPtr >= maxLength) //wrap - readPtr = 0; - } - - float sampleRate = 48000.0f; - - AudioBuffer<float> delayBuffer; - int bufferSize = 0; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DelayProcessor) -}; - -#endif //DELAYPROCESSOR_H_INCLUDED diff --git a/Plugin/Source/Processors/Timing_Effects/Flutter.cpp b/Plugin/Source/Processors/Timing_Effects/Flutter.cpp @@ -49,8 +49,7 @@ void Flutter::prepareToPlay (double sampleRate, int samplesPerBlock) phase2[ch] = 0.0f; phase3[ch] = 0.0f; - dcBlocker[ch].reset (sampleRate); - dcBlocker[ch].calcCoefs (30.0f, 0.707f); + dcBlocker[ch].prepare (sampleRate, 15.0f); } wowAmp = 1000.0f * 1000.0f / (float) sampleRate; diff --git a/Plugin/Source/Processors/Timing_Effects/Flutter.h b/Plugin/Source/Processors/Timing_Effects/Flutter.h @@ -2,8 +2,7 @@ #define FLUTTER_H_INCLUDED #include "../JuceLibraryCode/JuceHeader.h" -#include "DelayProcessor.h" -#include "../Hysteresis/DCFilters.h" +#include "../Hysteresis/DCBlocker.h" class Flutter { @@ -57,7 +56,7 @@ private: }; dsp::DelayLine<float, dsp::DelayLineInterpolationTypes::Lagrange3rd> delay { HISTORY_SIZE }; - TransformerHPF dcBlocker[2]; + DCBlocker dcBlocker[2]; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Flutter)