AnalogTapeModel

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

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:
MPlugin/CHOWTapeModel.jucer | 5+++++
MPlugin/Source/GUI/Assets/gui.xml | 39++++++++++++++++++++++++++++-----------
MPlugin/Source/PluginProcessor.cpp | 14+++++++++++---
MPlugin/Source/PluginProcessor.h | 2++
MPlugin/Source/Presets/PresetComp.cpp | 2+-
APlugin/Source/Processors/InputFilters.cpp | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APlugin/Source/Processors/InputFilters.h | 35+++++++++++++++++++++++++++++++++++
APlugin/Source/Processors/LinkwitzRileyFilter.h | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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