commit d8e11aab711904fcb9f25107edc4efd51254d257
parent aa64e48871204df59878d79d76d2b1af535c3f9c
Author: jatinchowdhury18 <jatinchowdhury18@gmail.com>
Date: Sat, 7 Nov 2020 20:34:28 -0800
High/Low Cut filters (#107)
* Set up interface for filters
* Implement high-/low-cut filters as 4th-order Linkwitz-Riley filters
Co-authored-by: jatinchowdhury18 <jatinchowdhury18@users.noreply.github.com>
Diffstat:
8 files changed, 293 insertions(+), 15 deletions(-)
diff --git a/Plugin/CHOWTapeModel.jucer b/Plugin/CHOWTapeModel.jucer
@@ -128,6 +128,11 @@
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"/>
+ <FILE id="ArGmHO" name="InputFilters.cpp" compile="1" resource="0"
+ file="Source/Processors/InputFilters.cpp"/>
+ <FILE id="ipAPk7" name="InputFilters.h" compile="0" resource="0" file="Source/Processors/InputFilters.h"/>
+ <FILE id="U8pzHn" name="LinkwitzRileyFilter.h" compile="0" resource="0"
+ file="Source/Processors/LinkwitzRileyFilter.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
@@ -44,13 +44,27 @@
plot-decay="0.0" plot-fill-color="FFFFFFFF"/>
</View>
<View padding="0" margin="" background-color="" lookAndFeel="">
- <View flex-direction="column" margin="5" padding="" background-color="FF31323A">
- <Slider caption="Input Gain [dB]" parameter="ingain" class="Slider" name="Input Gain"
- tooltip="Sets the input gain to the tape model in Decibels."/>
- <Slider caption="Dry/Wet" parameter="drywet" class="Slider" tooltip="Sets dry/wet mix of the entire plugin."
- name="Dry/Wet" slider-track="FF0BBDC2"/>
- <Slider caption="Output Gain [dB]" parameter="outgain" class="Slider"
- name="Output Gain" tooltip="Sets the output gain from the tape model in Decibels."/>
+ <View display="tabbed" padding="0" background-color="FF31323A" lookAndFeel="MyLNF">
+ <View flex-direction="column" tab-color="" background-color="FF31323A"
+ padding="0" tab-caption="Gain">
+ <Slider caption="Input Gain [dB]" parameter="ingain" class="Slider" name="Input Gain"
+ padding="0" margin="0" tooltip="Sets the input gain to the tape model in Decibels."/>
+ <Slider caption="Dry/Wet" parameter="drywet" class="Slider" tooltip="Sets dry/wet mix of the entire plugin."
+ padding="0" margin="0" name="Dry/Wet" slider-track="FF0BBDC2"/>
+ <Slider caption="Output Gain [dB]" parameter="outgain" class="Slider"
+ padding="0" margin="0" 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" tab-caption="Filters">
+ <Slider caption="Low Cut" parameter="ifilt_low" class="Slider" name="Low Cut"
+ tooltip="Applies a low cut filter before applying tape processing."/>
+ <Slider caption="High Cut" parameter="ifilt_high" class="Slider" name="High Cut"
+ tooltip="Applies a high cut filter before applying tape processing."/>
+ <TextButton parameter="ifilt_makeup" text="Makeup" background-color="00000000"
+ margin="0" padding="5" button-color="00000000" flex-grow="0.35"
+ button-on-color="FF8B3232" lookAndFeel="SpeedButtonLNF" name="Makeup"
+ tooltip="Adds the signal cut out by the cut filters back to the processed signal."/>
+ </View>
</View>
<View display="tabbed" padding="0" background-color="FF31323A" lookAndFeel="MyLNF">
<View flex-direction="column" tab-color="" background-color="FF31323A"
@@ -64,10 +78,12 @@
</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."/>
+ padding="0" margin="0" tooltip="Controls the treble response of the pre/post-emphasis filters."/>
+ <Slider caption="Bass" parameter="h_bass" class="Slider" name="Bass"
+ padding="0" margin="0" tooltip="Controls the bass response of the pre/post-emphasis filters."/>
+ <Slider caption="Frequency" parameter="h_tfreq" class="Slider" name="Transition Frequency"
+ padding="0" margin="0" tooltip="Controls the transition frequency between the bass and treble sections of the EQ."/>
</View>
</View>
<View display="tabbed" padding="0" background-color="FF31323A" flex-grow="1.5"
@@ -168,7 +184,8 @@
tooltip="Adds this plugin to a mix group. When the plugin is added to a group, the group parameters will be copied to this plugin, and their parameters will remain in sync."/>
<MixGroupViz flex-grow="0.3" margin="5" padding="0" background-color="00000000"/>
<presets margin="5" padding="0" background-color="00000000" border-color="595C6B"
- radius="" border="" lookAndFeel="PresetsLNF" flex-grow="1.9" max-height="100"/>
+ radius="" border="" lookAndFeel="PresetsLNF" flex-grow="1.9"
+ max-height="100"/>
</View>
</View>
</magic>
diff --git a/Plugin/Source/PluginProcessor.cpp b/Plugin/Source/PluginProcessor.cpp
@@ -32,6 +32,7 @@ ChowtapeModelAudioProcessor::ChowtapeModelAudioProcessor()
),
#endif
vts (*this, nullptr, Identifier ("Parameters"), createParameterLayout()),
+ inputFilters (vts),
toneControl (vts),
hysteresis (vts),
degrade (vts),
@@ -63,6 +64,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, maxNumPresets, 0));
+ InputFilters::createParameterLayout (params);
ToneControl::createParameterLayout (params);
HysteresisProcessor::createParameterLayout (params);
LossFilter::createParameterLayout (params);
@@ -155,6 +157,7 @@ void ChowtapeModelAudioProcessor::prepareToPlay (double sampleRate, int samplesP
setRateAndBufferSizeDetails (sampleRate, samplesPerBlock);
inGain.prepareToPlay (sampleRate, samplesPerBlock);
+ inputFilters.prepareToPlay (sampleRate, samplesPerBlock);
toneControl.prepare (sampleRate);
hysteresis.prepareToPlay (sampleRate, samplesPerBlock);
degrade.prepareToPlay (sampleRate, samplesPerBlock);
@@ -222,6 +225,7 @@ void ChowtapeModelAudioProcessor::processBlock (AudioBuffer<float>& buffer, Midi
dryBuffer.makeCopyOf (buffer, true);
inGain.processBlock (buffer, midiMessages);
+ inputFilters.processBlock (buffer);
scope->pushSamples (buffer, TapeScope::AudioType::Input);
@@ -230,7 +234,6 @@ void ChowtapeModelAudioProcessor::processBlock (AudioBuffer<float>& buffer, Midi
toneControl.processBlockOut (buffer);
chewer.processBlock (buffer);
degrade.processBlock (buffer, midiMessages);
-
flutter.processBlock (buffer, midiMessages);
for (int ch = 0; ch < buffer.getNumChannels(); ++ch)
@@ -238,6 +241,7 @@ void ChowtapeModelAudioProcessor::processBlock (AudioBuffer<float>& buffer, Midi
latencyCompensation();
+ inputFilters.processBlockMakeup (buffer);
outGain.processBlock (buffer, midiMessages);
dryWet.processBlock (dryBuffer, buffer);
@@ -247,15 +251,19 @@ void ChowtapeModelAudioProcessor::processBlock (AudioBuffer<float>& buffer, Midi
void ChowtapeModelAudioProcessor::latencyCompensation()
{
// delay dry buffer to avoid phase issues
- const auto latencySamp = roundToInt (calcLatencySamples());
+ const auto latencySampFloat = calcLatencySamples();
+ const auto latencySamp = roundToInt (latencySampFloat);
setLatencySamples (latencySamp);
+ // delay makeup block from input filters
+ inputFilters.setMakeupDelay (latencySampFloat);
+
// For "true bypass" use integer sample delay to avoid delay
// line interpolation freq. response issues
if (dryWet.getDryWet() < 0.15f)
dryDelay.setDelay ((float) latencySamp);
else
- dryDelay.setDelay (calcLatencySamples());
+ dryDelay.setDelay (latencySampFloat);
dsp::AudioBlock<float> block { dryBuffer };
dryDelay.process (dsp::ProcessContextReplacing<float> { block });
diff --git a/Plugin/Source/PluginProcessor.h b/Plugin/Source/PluginProcessor.h
@@ -19,6 +19,7 @@
#include "Processors/Degrade/DegradeProcessor.h"
#include "Processors/Chew/ChewProcessor.h"
#include "Processors/DryWetProcessor.h"
+#include "Processors/InputFilters.h"
#include "Presets/PresetManager.h"
#include "GUI/MyLNF.h"
#include "GUI/AutoUpdating.h"
@@ -79,6 +80,7 @@ private:
AudioProcessorValueTreeState vts;
GainProcessor inGain;
+ InputFilters inputFilters;
ToneControl toneControl;
HysteresisProcessor hysteresis;
DegradeProcessor degrade;
diff --git a/Plugin/Source/Presets/PresetComp.cpp b/Plugin/Source/Presets/PresetComp.cpp
@@ -7,7 +7,7 @@ PresetComp::PresetComp (ChowtapeModelAudioProcessor& proc, PresetManager& manage
manager.addListener (this);
presetBox.setName ("Preset Manager");
- presetBox.setTooltip ("Use this menu to select presets, and to save and manage user presets");
+ presetBox.setTooltip ("Use this menu to select presets, and to save and manage user presets.");
setColour (backgroundColourId, Colour (0xFF595C6B));
setColour (textColourId, Colours::white);
diff --git a/Plugin/Source/Processors/InputFilters.cpp b/Plugin/Source/Processors/InputFilters.cpp
@@ -0,0 +1,111 @@
+#include "InputFilters.h"
+
+namespace
+{
+ constexpr float minFreq = 20.0f;
+ constexpr float maxFreq = 22000.0f;
+}
+
+InputFilters::InputFilters (AudioProcessorValueTreeState& vts)
+{
+ lowCutParam = vts.getRawParameterValue ("ifilt_low");
+ highCutParam = vts.getRawParameterValue ("ifilt_high");
+ makeupParam = vts.getRawParameterValue ("ifilt_makeup");
+}
+
+void InputFilters::createParameterLayout (std::vector<std::unique_ptr<RangedAudioParameter>>& params)
+{
+ NormalisableRange lowFreqRange { minFreq, 2000.0f };
+ lowFreqRange.setSkewForCentre (250.0f);
+
+ NormalisableRange highFreqRange { 2000.0f, maxFreq };
+ highFreqRange.setSkewForCentre (10000.0f);
+
+ auto freqToString = [] (float freq, int) -> String {
+ String suffix = " Hz";
+ if (freq > 1000.0f) { freq /= 1000.0f; suffix = " kHz"; }
+ return String (freq, 2, false) + suffix;
+ };
+
+ auto stringToFreq = [] (const String& string) -> float {
+ float freq = string.getFloatValue();
+ if (string.getLastCharacter() == 'k')
+ freq *= 1000.0f;
+
+ return freq;
+ };
+
+ params.push_back (std::make_unique<AudioParameterFloat> ("ifilt_low", "Low Cut", lowFreqRange,
+ minFreq, String(), AudioProcessorParameter::genericParameter, freqToString, stringToFreq));
+ params.push_back (std::make_unique<AudioParameterFloat> ("ifilt_high", "High Cut", highFreqRange,
+ maxFreq, String(), AudioProcessorParameter::genericParameter, freqToString, stringToFreq));
+ params.push_back (std::make_unique<AudioParameterBool> ("ifilt_makeup", "Cut Makeup", false));
+}
+
+void InputFilters::prepareToPlay (double sampleRate, int samplesPerBlock)
+{
+ fs = (float) sampleRate;
+ dsp::ProcessSpec spec { sampleRate, (uint32) samplesPerBlock, 2 };
+ lowCutFilter.prepare (spec);
+ highCutFilter.prepare (spec);
+ makeupDelay.prepare (spec);
+
+ lowCutBuffer .setSize (2, samplesPerBlock);
+ highCutBuffer.setSize (2, samplesPerBlock);
+ makeupBuffer .setSize (2, samplesPerBlock);
+
+ internalBypass = false;
+}
+
+void InputFilters::processBlock (AudioBuffer<float>& buffer)
+{
+ if (*lowCutParam == minFreq && *highCutParam == maxFreq)
+ {
+ internalBypass = true;
+ return;
+ }
+
+ internalBypass = false;
+
+ lowCutFilter.setCutoff (lowCutParam->load());
+ highCutFilter.setCutoff (jmin (highCutParam->load(), fs * 0.48f));
+
+ for (int ch = 0; ch < buffer.getNumChannels(); ++ch)
+ {
+ auto* data = buffer.getWritePointer (ch);
+ auto* cutLowSignal = lowCutBuffer.getWritePointer (ch);
+ auto* cutHighSignal = highCutBuffer.getWritePointer (ch);
+
+ for (int n = 0; n < buffer.getNumSamples(); ++n)
+ {
+ lowCutFilter.processSample (ch, data[n], cutLowSignal[n], data[n]);
+ highCutFilter.processSample (ch, data[n], data[n], cutHighSignal[n]);
+ }
+ }
+
+ lowCutFilter.snapToZero();
+ highCutFilter.snapToZero();
+}
+
+void InputFilters::processBlockMakeup (AudioBuffer<float>& buffer)
+{
+ if (! static_cast<bool> (makeupParam->load()) || internalBypass)
+ return;
+
+ // compile makeup signal
+ dsp::AudioBlock<float> lowCutBlock (lowCutBuffer);
+ dsp::AudioBlock<float> highCutBlock (highCutBuffer);
+ dsp::AudioBlock<float> makeupBlock (makeupBuffer);
+
+ makeupBlock.fill (0.0f);
+ makeupBlock += lowCutBlock;
+ makeupBlock += highCutBlock;
+
+ // delay makeup signal to be in phase with everything else
+ dsp::ProcessContextReplacing<float> context (makeupBlock);
+ makeupDelay.process (context);
+
+ // add makeup back to main buffer
+ dsp::AudioBlock<float> outputBlock (buffer);
+ outputBlock += makeupBlock;
+}
diff --git a/Plugin/Source/Processors/InputFilters.h b/Plugin/Source/Processors/InputFilters.h
@@ -0,0 +1,35 @@
+#ifndef INPUTFILTERS_H_INCLUDED
+#define INPUTFILTERS_H_INCLUDED
+
+#include "LinkwitzRileyFilter.h"
+
+class InputFilters
+{
+public:
+ InputFilters (AudioProcessorValueTreeState& vts);
+
+ static void createParameterLayout (std::vector<std::unique_ptr<RangedAudioParameter>>& params);
+ void prepareToPlay (double sampleRate, int samplesPerBlock);
+ void setMakeupDelay (float newDelaySamples) { makeupDelay.setDelay (newDelaySamples); }
+
+ void processBlock (AudioBuffer<float>& buffer);
+ void processBlockMakeup (AudioBuffer<float>& buffer);
+
+private:
+ std::atomic<float>* lowCutParam = nullptr;
+ std::atomic<float>* highCutParam = nullptr;
+ std::atomic<float>* makeupParam = nullptr;
+ bool internalBypass = false;
+
+ float fs = 44100.0f;
+ LinkwitzRileyFilter<float, 2> lowCutFilter;
+ LinkwitzRileyFilter<float, 2> highCutFilter;
+ dsp::DelayLine<float, dsp::DelayLineInterpolationTypes::Lagrange3rd> makeupDelay { 1 << 21 };
+
+ AudioBuffer<float> lowCutBuffer, highCutBuffer, makeupBuffer;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InputFilters)
+};
+
+#endif // !INPUTFILTERS_H_INCLUDED
+
diff --git a/Plugin/Source/Processors/LinkwitzRileyFilter.h b/Plugin/Source/Processors/LinkwitzRileyFilter.h
@@ -0,0 +1,100 @@
+#ifndef LINKWITZRILEYFILTER_H_INCLUDED
+#define LINKWITZRILEYFILTER_H_INCLUDED
+
+#include <JuceHeader.h>
+
+/** 4th-order L-R Filter */
+template <typename SampleType, size_t N_chan>
+class LinkwitzRileyFilter
+{
+public:
+ LinkwitzRileyFilter()
+ {
+ update();
+ }
+
+ /** Sets the cutoff frequency of the filter in Hz. */
+ void setCutoff (SampleType newCutoffFrequencyHz)
+ {
+ jassert (isPositiveAndBelow (newCutoffFrequencyHz, static_cast<SampleType> (sampleRate * 0.5)));
+ cutoffFrequency = newCutoffFrequencyHz;
+ update();
+ }
+
+ /** Initialises the filter. */
+ void prepare (const dsp::ProcessSpec& spec)
+ {
+ jassert (spec.sampleRate > 0);
+ jassert (spec.numChannels > 0);
+ jassert (spec.numChannels == N_chan);
+
+ sampleRate = spec.sampleRate;
+ update();
+ reset();
+ }
+
+ /** Resets the internal state variables of the filter. */
+ void reset()
+ {
+ for (auto& s : state)
+ std::fill (s.begin(), s.end(), static_cast<SampleType> (0));
+ }
+
+ /** Performs the filter operation on a single sample at a time, and returns both
+ the low-pass and the high-pass outputs of the TPT structure.
+ */
+ inline void processSample (size_t ch, SampleType x, SampleType &outputLow, SampleType &outputHigh) noexcept
+ {
+ auto yH = (x - (R2 + g) * state[ch][0] - state[ch][1]) * h;
+
+ auto tB = g * yH;
+ auto yB = tB + state[ch][0];
+ state[ch][0] = tB + yB;
+
+ auto tL = g * yB;
+ auto yL = tL + state[ch][1];
+ state[ch][1] = tL + yL;
+
+ auto yH2 = (yL - (R2 + g) * state[ch][2] - state[ch][3]) * h;
+
+ auto tB2 = g * yH2;
+ auto yB2 = tB2 + state[ch][2];
+ state[ch][2] = tB2 + yB2;
+
+ auto tL2 = g * yB2;
+ auto yL2 = tL2 + state[ch][3];
+ state[ch][3] = tL2 + yL2;
+
+ outputLow = yL2;
+ outputHigh = yL - R2 * yB + yH - yL2;
+ }
+
+ /** Ensure that the state variables are rounded to zero if the state
+ variables are denormals. This is only needed if you are doing
+ sample by sample processing.
+ */
+ inline void snapToZero() noexcept
+ {
+ for (auto& s : state)
+ for (auto element : s)
+ juce::dsp::util::snapToZero (element);
+ }
+
+private:
+ void update()
+ {
+ g = (SampleType) std::tan (MathConstants<double>::pi * cutoffFrequency / sampleRate);
+ h = (SampleType) (1.0 / (1.0 + R2 * g + g * g));
+ }
+
+ SampleType g, h;
+ static constexpr SampleType R2 = static_cast<SampleType> (1.41421356237);
+ std::array<std::array<SampleType, 4>, N_chan> state;
+
+ double sampleRate = 44100.0;
+ SampleType cutoffFrequency = 2000.0;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LinkwitzRileyFilter)
+};
+
+#endif // LINKWITZRILEYFILTER_H_INCLUDED