AnalogTapeModel

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

commit 19f1a8b093c4a11bcd326b1a46bab13cb24d2f21
parent b719dfcd48167e113a226f738d6ae67ecf695ea2
Author: jatinchowdhury18 <jatinchowdhury18@gmail.com>
Date:   Sun, 28 Feb 2021 17:53:55 -0800

Wow/Flutter: refactoring and improvements (#145)

* Python code for simulating improved wow controls

* Rename Flutter class to WowFlutterProcessor

* Refactor WowProcess to a separate class

* Refactor FlutterProcess to a separate class

* {Apply clang-format}

* Refactor wow/flutter to use bypass processor

* {Apply clang-format}

* Fix typo

* First pass at adding random behavior in for wow control

* Add drift parameter

* {Apply clang-format}

* Update build and install scripts

* Fix algorithm bugs and UI issues

* {Apply clang-format}

Co-authored-by: jatinchowdhury18 <jatinchowdhury18@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Diffstat:
M.github/labeler.yml | 2+-
MPlugin/CMakeLists.txt | 1+
MPlugin/Source/GUI/Assets/gui.xml | 16+++++++++++-----
MPlugin/Source/GUI/OnOffManager.cpp | 2+-
MPlugin/Source/PluginProcessor.cpp | 2+-
MPlugin/Source/PluginProcessor.h | 4++--
MPlugin/Source/Processors/CMakeLists.txt | 4+++-
DPlugin/Source/Processors/Timing_Effects/Flutter.cpp | 217-------------------------------------------------------------------------------
DPlugin/Source/Processors/Timing_Effects/Flutter.h | 70----------------------------------------------------------------------
APlugin/Source/Processors/Timing_Effects/FlutterProcess.cpp | 46++++++++++++++++++++++++++++++++++++++++++++++
APlugin/Source/Processors/Timing_Effects/FlutterProcess.h | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APlugin/Source/Processors/Timing_Effects/OHProcess.h | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APlugin/Source/Processors/Timing_Effects/WowFlutterProcessor.cpp | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APlugin/Source/Processors/Timing_Effects/WowFlutterProcessor.h | 50++++++++++++++++++++++++++++++++++++++++++++++++++
APlugin/Source/Processors/Timing_Effects/WowProcess.cpp | 42++++++++++++++++++++++++++++++++++++++++++
APlugin/Source/Processors/Timing_Effects/WowProcess.h | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
ASimulations/TimingEffects/OHProcess.py | 41+++++++++++++++++++++++++++++++++++++++++
17 files changed, 535 insertions(+), 298 deletions(-)

diff --git a/.github/labeler.yml b/.github/labeler.yml @@ -9,7 +9,7 @@ gui: - Plugin/Source/GUI/**/* presets: - - - Plugin/Source/Presets/**/* + - Plugin/Source/Presets/**/* documentation: - Manual/**/* diff --git a/Plugin/CMakeLists.txt b/Plugin/CMakeLists.txt @@ -18,6 +18,7 @@ juce_add_plugin(CHOWTapeModel ProductName "CHOWTapeModel" LV2_URI https://github.com/jatinchowdhury18/AnalogTapeModel ICON_BIG Source/GUI/Assets/logo.png + MICROPHONE_PERMISSION_ENABLED TRUE ) # create JUCE header diff --git a/Plugin/Source/GUI/Assets/gui.xml b/Plugin/Source/GUI/Assets/gui.xml @@ -168,7 +168,7 @@ parameter="chew_onoff" name="Chew On/Off" tooltip="Turns the chew processing on or off."/> </View> </View> - <View display="tabbed" padding="0" background-color="FF31323A" lookAndFeel="MyLNF"> + <View display="tabbed" padding="0" margin="2" background-color="FF31323A" lookAndFeel="MyLNF"> <View tab-caption="Flutter" flex-direction="column" background-color="FF31323A"> <Slider caption="Depth" parameter="depth" max-height="150" class="Slider" name="Flutter Depth" tooltip="Sets depth of the tape flutter." @@ -182,14 +182,20 @@ min-height="20" button-color="ff595c6b" button-on-color="FFEAA92C" parameter="flutter_onoff" name="Wow/Flutter On/Off" tooltip="Turns the wow and flutter processing on or off."/> </View> - <View tab-caption="Wow" flex-direction="column" background-color="FF31323A"> + <View tab-caption="Wow" flex-direction="column" background-color="FF31323A" padding="0" margin="3"> <Slider caption="Depth" parameter="wow_depth" max-height="150" class="Slider" name="Wow Depth" tooltip="Sets the depth of the tape wow." margin="0" - padding="0"/> + padding="0" slider-type="linear-horizontal"/> <Slider caption="Rate" parameter="wow_rate" class="Slider" max-height="150" name="Wow Rate" tooltip="Sets the rate of the tape wow." margin="0" - padding="0"/> - <Plot source="wow" plot-decay="0.8" flex-grow="0.8" background-color="FF1E1F22" + padding="0" slider-type="linear-horizontal"/> + <Slider caption="Variance" parameter="wow_var" class="Slider" max-height="150" + name="Wow Variance" tooltip="Sets the amount of variance in the tape wow." margin="0" + padding="0" slider-type="linear-horizontal"/> + <Slider caption="Drift" parameter="wow_drift" class="Slider" max-height="150" + name="Wow Drift" tooltip="Sets the amount of drift in the tape wow." margin="0" + padding="0" slider-type="linear-horizontal"/> + <Plot source="wow" plot-decay="0.8" flex-grow="1.45" background-color="FF1E1F22" plot-color="FFEAA92C" plot-fill-color="CC8B3232"/> <PowerButton margin="0" padding="0" background-color="00000000" max-height="25" min-height="20" button-color="ff595c6b" button-on-color="FFEAA92C" diff --git a/Plugin/Source/GUI/OnOffManager.cpp b/Plugin/Source/GUI/OnOffManager.cpp @@ -10,7 +10,7 @@ static const std::unordered_map<String, StringArray> triggerMap { { String ("loss_onoff"), StringArray ({ "Gap", "Thickness", "Spacing", "Speed", "3.75 ips", "7.5 ips", "15 ips", "30 ips" }) }, { String ("chew_onoff"), StringArray ({ "Chew Depth", "Chew Frequency", "Chew Variance" }) }, { String ("deg_onoff"), StringArray ({ "Depth", "Amount", "Variance" }) }, - { String ("flutter_onoff"), StringArray ({ "Flutter Depth", "Flutter Rate", "Wow Depth", "Wow Rate" }) }, + { String ("flutter_onoff"), StringArray ({ "Flutter Depth", "Flutter Rate", "Wow Depth", "Wow Rate", "Wow Variance", "Wow Drift" }) }, }; void toggleEnableDisable (Component* root, StringArray& compNames, bool shouldBeEnabled) diff --git a/Plugin/Source/PluginProcessor.cpp b/Plugin/Source/PluginProcessor.cpp @@ -71,7 +71,7 @@ AudioProcessorValueTreeState::ParameterLayout ChowtapeModelAudioProcessor::creat ToneControl::createParameterLayout (params); HysteresisProcessor::createParameterLayout (params); LossFilter::createParameterLayout (params); - Flutter::createParameterLayout (params); + WowFlutterProcessor::createParameterLayout (params); DegradeProcessor::createParameterLayout (params); ChewProcessor::createParameterLayout (params); MixGroupsController::createParameterLayout (params); diff --git a/Plugin/Source/PluginProcessor.h b/Plugin/Source/PluginProcessor.h @@ -24,7 +24,7 @@ #include "Processors/Hysteresis/ToneControl.h" #include "Processors/Input_Filters/InputFilters.h" #include "Processors/Loss_Effects/LossFilter.h" -#include "Processors/Timing_Effects/Flutter.h" +#include "Processors/Timing_Effects/WowFlutterProcessor.h" #include <JuceHeader.h> //============================================================================== @@ -88,7 +88,7 @@ private: DegradeProcessor degrade; ChewProcessor chewer; LossFilter lossFilter; - Flutter flutter; + WowFlutterProcessor flutter; DryWetProcessor dryWet; dsp::DelayLine<float, dsp::DelayLineInterpolationTypes::Lagrange3rd> dryDelay { 1 << 21 }; GainProcessor outGain; diff --git a/Plugin/Source/Processors/CMakeLists.txt b/Plugin/Source/Processors/CMakeLists.txt @@ -9,5 +9,7 @@ target_sources(CHOWTapeModel PRIVATE Input_Filters/InputFilters.cpp Loss_Effects/LossFilter.cpp - Timing_Effects/Flutter.cpp + Timing_Effects/WowFlutterProcessor.cpp + Timing_Effects/FlutterProcess.cpp + Timing_Effects/WowProcess.cpp ) diff --git a/Plugin/Source/Processors/Timing_Effects/Flutter.cpp b/Plugin/Source/Processors/Timing_Effects/Flutter.cpp @@ -1,217 +0,0 @@ -#include "Flutter.h" -#include "../../GUI/LightMeter.h" - -namespace -{ -constexpr float depthSlewMin = 0.001f; -} - -Flutter::Flutter (AudioProcessorValueTreeState& vts) -{ - flutterRate = vts.getRawParameterValue ("rate"); - flutterDepth = vts.getRawParameterValue ("depth"); - - wowRate = vts.getRawParameterValue ("wow_rate"); - wowDepth = vts.getRawParameterValue ("wow_depth"); - - flutterOnOff = vts.getRawParameterValue ("flutter_onoff"); - - depthSlewWow[0].setCurrentAndTargetValue (*wowDepth); - depthSlewWow[1].setCurrentAndTargetValue (*wowDepth); - - depthSlewFlutter[0].setCurrentAndTargetValue (*flutterDepth); - depthSlewFlutter[1].setCurrentAndTargetValue (*flutterDepth); -} - -void Flutter::initialisePlots (foleys::MagicGUIState& magicState) -{ - wowPlot = magicState.createAndAddObject<LightMeter> ("wow"); - magicState.addBackgroundProcessing (wowPlot); - - flutterPlot = magicState.createAndAddObject<LightMeter> ("flutter"); - magicState.addBackgroundProcessing (flutterPlot); -} - -void Flutter::createParameterLayout (std::vector<std::unique_ptr<RangedAudioParameter>>& params) -{ - params.push_back (std::make_unique<AudioParameterBool> ("flutter_onoff", "Wow/Flutter On/Off", true)); - - params.push_back (std::make_unique<AudioParameterFloat> ("rate", "Flutter Rate", 0.0f, 1.0f, 0.3f)); - params.push_back (std::make_unique<AudioParameterFloat> ("depth", "Flutter Depth", 0.0f, 1.0f, 0.0f)); - - params.push_back (std::make_unique<AudioParameterFloat> ("wow_rate", "Wow Rate", 0.0f, 1.0f, 0.25f)); - params.push_back (std::make_unique<AudioParameterFloat> ("wow_depth", "Wow Depth", 0.0f, 1.0f, 0.0f)); -} - -void Flutter::prepareToPlay (double sampleRate, int samplesPerBlock) -{ - fs = (float) sampleRate; - - for (int ch = 0; ch < 2; ++ch) - { - delay.prepare ({ sampleRate, (uint32) samplesPerBlock, 2 }); - delay.setDelay (0.0f); - - depthSlewWow[ch].reset (sampleRate, 0.05); - depthSlewWow[ch].setCurrentAndTargetValue (depthSlewMin); - - depthSlewFlutter[ch].reset (sampleRate, 0.05); - depthSlewFlutter[ch].setCurrentAndTargetValue (depthSlewMin); - - wowPhase[ch] = 0.0f; - phase1[ch] = 0.0f; - phase2[ch] = 0.0f; - phase3[ch] = 0.0f; - - dcBlocker[ch].prepare (sampleRate, 15.0f); - } - - wowAmp = 1000.0f * 1000.0f / (float) sampleRate; - amp1 = -230.0f * 1000.0f / (float) sampleRate; - amp2 = -80.0f * 1000.0f / (float) sampleRate; - amp3 = -99.0f * 1000.0f / (float) sampleRate; - dcOffset = 350.0f * 1000.0f / (float) sampleRate; - - isOff = true; - dryBuffer.setSize (2, samplesPerBlock); - wowBuffer.setSize (2, samplesPerBlock); - flutterBuffer.setSize (2, samplesPerBlock); - - wowPlot->prepareToPlay (sampleRate, samplesPerBlock); - flutterPlot->prepareToPlay (sampleRate, samplesPerBlock); -} - -void Flutter::processBlock (AudioBuffer<float>& buffer, MidiBuffer& /*midiMessages*/) -{ - ScopedNoDenormals noDenormals; - - auto curDepthWow = powf (*wowDepth, 3.0f); - depthSlewWow[0].setTargetValue (jmax (depthSlewMin, curDepthWow)); - depthSlewWow[1].setTargetValue (jmax (depthSlewMin, curDepthWow)); - - auto curDepthFlutter = powf (powf (*flutterDepth, 3.0f) * 81.0f / 625.0f, 0.5f); - depthSlewFlutter[0].setTargetValue (jmax (depthSlewMin, curDepthFlutter)); - depthSlewFlutter[1].setTargetValue (jmax (depthSlewMin, curDepthFlutter)); - - auto wowFreq = powf (4.5, *wowRate) - 1.0f; - angleDeltaWow = MathConstants<float>::twoPi * wowFreq / fs; - - auto flutterFreq = 0.1f * powf (1000.0f, *flutterRate); - angleDelta1 = MathConstants<float>::twoPi * 1.0f * flutterFreq / fs; - angleDelta2 = MathConstants<float>::twoPi * 2.0f * flutterFreq / fs; - angleDelta3 = MathConstants<float>::twoPi * 3.0f * flutterFreq / fs; - - wowBuffer.setSize (2, buffer.getNumSamples(), false, false, true); - wowBuffer.clear(); - flutterBuffer.setSize (2, buffer.getNumSamples(), false, false, true); - flutterBuffer.clear(); - - bool shouldTurnOff = ! static_cast<bool> (flutterOnOff->load()) || (depthSlewWow[0].getTargetValue() == depthSlewMin && depthSlewFlutter[0].getTargetValue() == depthSlewMin); - if (! isOff && ! shouldTurnOff) // process normally - { - processWetBuffer (buffer); - } - else if (! isOff && shouldTurnOff) // turn off - { - dryBuffer.makeCopyOf (buffer, true); - processWetBuffer (buffer); - - buffer.applyGainRamp (0, buffer.getNumSamples(), 1.0f, 0.0f); - for (int ch = 0; ch < buffer.getNumChannels(); ++ch) - buffer.addFromWithRamp (ch, 0, dryBuffer.getWritePointer (ch), buffer.getNumSamples(), 0.0f, 1.0f); - } - else if (isOff && ! shouldTurnOff) // turn on - { - dryBuffer.makeCopyOf (buffer, true); - processWetBuffer (buffer); - - buffer.applyGainRamp (0, buffer.getNumSamples(), 0.0f, 1.0f); - for (int ch = 0; ch < buffer.getNumChannels(); ++ch) - buffer.addFromWithRamp (ch, 0, dryBuffer.getWritePointer (ch), buffer.getNumSamples(), 1.0f, 0.0f); - } - else // off - { - processBypassed (buffer); - } - - isOff = shouldTurnOff; - - // dc block - for (int ch = 0; ch < buffer.getNumChannels(); ++ch) - dcBlocker[ch].processBlock (buffer.getWritePointer (ch), buffer.getNumSamples()); - - wowBuffer.applyGain (0.83333f / wowAmp); - wowPlot->pushSamples (wowBuffer); - - flutterBuffer.applyGain (1.3333f / amp1); - flutterPlot->pushSamples (flutterBuffer); -} - -void Flutter::processWetBuffer (AudioBuffer<float>& buffer) -{ - for (int ch = 0; ch < buffer.getNumChannels(); ++ch) - { - auto* x = buffer.getWritePointer (ch); - auto* wowPtr = wowBuffer.getWritePointer (ch); - auto* flutterPtr = flutterBuffer.getWritePointer (ch); - for (int n = 0; n < buffer.getNumSamples(); ++n) - { - wowPhase[ch] += angleDeltaWow; - phase1[ch] += angleDelta1; - phase2[ch] += angleDelta2; - phase3[ch] += angleDelta3; - - auto wowLFO = depthSlewWow[ch].getNextValue() * wowAmp * cosf (wowPhase[ch]); - auto flutterLFO = depthSlewFlutter[ch].getNextValue() - * (amp1 * cosf (phase1[ch] + phaseOff1) - + amp2 * cosf (phase2[ch] + phaseOff2) - + amp3 * cosf (phase3[ch] + phaseOff3)); - - auto newLength = (wowLFO + flutterLFO + dcOffset + depthSlewWow[ch].getCurrentValue() * wowAmp) * (float) fs / 1000.0f; - newLength = jlimit (0.0f, (float) HISTORY_SIZE, newLength); - - delay.setDelay (newLength); - delay.pushSample (ch, x[n]); - x[n] = delay.popSample (ch); - - wowPtr[n] = wowLFO; - flutterPtr[n] = flutterLFO; - } - - while (wowPhase[ch] >= MathConstants<float>::twoPi) - wowPhase[ch] -= MathConstants<float>::twoPi; - while (phase1[ch] >= MathConstants<float>::twoPi) - phase1[ch] -= MathConstants<float>::twoPi; - while (phase2[ch] >= MathConstants<float>::twoPi) - phase2[ch] -= MathConstants<float>::twoPi; - while (phase2[ch] >= MathConstants<float>::twoPi) - phase2[ch] -= MathConstants<float>::twoPi; - } -} - -void Flutter::processBypassed (AudioBuffer<float>& buffer) -{ - for (int ch = 0; ch < buffer.getNumChannels(); ++ch) - { - delay.setDelay (0.0f); - for (int n = 0; n < buffer.getNumSamples(); ++n) - { - wowPhase[ch] += angleDeltaWow; - phase1[ch] += angleDelta1; - phase2[ch] += angleDelta2; - phase3[ch] += angleDelta3; - - delay.pushSample (ch, 0.0f); - delay.popSample (ch); - } - - while (wowPhase[ch] >= MathConstants<float>::twoPi) - wowPhase[ch] -= MathConstants<float>::twoPi; - while (phase1[ch] >= MathConstants<float>::twoPi) - phase1[ch] -= MathConstants<float>::twoPi; - while (phase2[ch] >= MathConstants<float>::twoPi) - phase2[ch] -= MathConstants<float>::twoPi; - while (phase2[ch] >= MathConstants<float>::twoPi) - phase2[ch] -= MathConstants<float>::twoPi; - } -} diff --git a/Plugin/Source/Processors/Timing_Effects/Flutter.h b/Plugin/Source/Processors/Timing_Effects/Flutter.h @@ -1,70 +0,0 @@ -#ifndef FLUTTER_H_INCLUDED -#define FLUTTER_H_INCLUDED - -#include "../Hysteresis/DCBlocker.h" -#include <JuceHeader.h> - -class Flutter -{ -public: - Flutter (AudioProcessorValueTreeState& vts); - - void initialisePlots (foleys::MagicGUIState& magicState); - static void createParameterLayout (std::vector<std::unique_ptr<RangedAudioParameter>>& params); - - void prepareToPlay (double sampleRate, int samplesPerBlock); - void processBlock (AudioBuffer<float>&, MidiBuffer&); - - void processWetBuffer (AudioBuffer<float>& buffer); - void processBypassed (AudioBuffer<float>& buffer); - -private: - std::atomic<float>* flutterOnOff = nullptr; - std::atomic<float>* flutterRate = nullptr; - std::atomic<float>* flutterDepth = nullptr; - std::atomic<float>* wowRate = nullptr; - std::atomic<float>* wowDepth = nullptr; - - bool isOff = false; - AudioBuffer<float> dryBuffer; - - float wowPhase[2] = { 0.0f, 0.0f }; - float phase1[2] = { 0.0f, 0.0f }; - float phase2[2] = { 0.0f, 0.0f }; - float phase3[2] = { 0.0f, 0.0f }; - - float wowAmp = 0.0f; - float amp1 = 0.0f; - float amp2 = 0.0f; - float amp3 = 0.0f; - float fs = 48000.0f; - - float dcOffset = 0.0f; - const float phaseOff1 = 0.0f; - const float phaseOff2 = 13.0f * MathConstants<float>::pi / 4.0f; - const float phaseOff3 = -MathConstants<float>::pi / 10.0f; - - float angleDeltaWow = 0.0f; - float angleDelta1 = 0.0f; - float angleDelta2 = 0.0f; - float angleDelta3 = 0.0f; - - SmoothedValue<float, ValueSmoothingTypes::Multiplicative> depthSlewWow[2]; - SmoothedValue<float, ValueSmoothingTypes::Multiplicative> depthSlewFlutter[2]; - - AudioBuffer<float> wowBuffer, flutterBuffer; - foleys::MagicPlotSource *wowPlot = nullptr, *flutterPlot = nullptr; - - enum - { - HISTORY_SIZE = 1 << 21, - }; - - dsp::DelayLine<float, dsp::DelayLineInterpolationTypes::Lagrange3rd> delay { HISTORY_SIZE }; - DCBlocker dcBlocker[2]; - - //============================================================================== - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Flutter) -}; - -#endif //FLUTTER_H_INCLUDED diff --git a/Plugin/Source/Processors/Timing_Effects/FlutterProcess.cpp b/Plugin/Source/Processors/Timing_Effects/FlutterProcess.cpp @@ -0,0 +1,46 @@ +#include "FlutterProcess.h" + +void FlutterProcess::prepare (double sampleRate, int samplesPerBlock) +{ + fs = fs; + + for (int ch = 0; ch < 2; ++ch) + { + depthSlew[ch].reset (sampleRate, 0.05); + depthSlew[ch].setCurrentAndTargetValue (depthSlewMin); + + phase1[ch] = 0.0f; + phase2[ch] = 0.0f; + phase3[ch] = 0.0f; + } + + amp1 = -230.0f * 1000.0f / fs; + amp2 = -80.0f * 1000.0f / fs; + amp3 = -99.0f * 1000.0f / fs; + dcOffset = 350.0f * 1000.0f / fs; + + flutterBuffer.setSize (2, samplesPerBlock); +} + +void FlutterProcess::prepareBlock (float curDepth, float flutterFreq, int numSamples) +{ + depthSlew[0].setTargetValue (jmax (depthSlewMin, curDepth)); + depthSlew[1].setTargetValue (jmax (depthSlewMin, curDepth)); + + angleDelta1 = MathConstants<float>::twoPi * 1.0f * flutterFreq / fs; + angleDelta2 = MathConstants<float>::twoPi * 2.0f * flutterFreq / fs; + angleDelta3 = MathConstants<float>::twoPi * 3.0f * flutterFreq / fs; + + flutterBuffer.setSize (2, numSamples, false, false, true); + flutterBuffer.clear(); + flutterPtrs = flutterBuffer.getArrayOfWritePointers(); +} + +void FlutterProcess::plotBuffer (foleys::MagicPlotSource* plot) +{ + if (shouldTurnOff()) + flutterBuffer.clear(); + + flutterBuffer.applyGain (1.3333f / amp1); + plot->pushSamples (flutterBuffer); +} diff --git a/Plugin/Source/Processors/Timing_Effects/FlutterProcess.h b/Plugin/Source/Processors/Timing_Effects/FlutterProcess.h @@ -0,0 +1,71 @@ +#ifndef FLUTTERPROCESS_H_INCLUDED +#define FLUTTERPROCESS_H_INCLUDED + +#include <JuceHeader.h> + +class FlutterProcess +{ +public: + FlutterProcess() = default; + + void prepare (double sampleRate, int samplesPerBlock); + void prepareBlock (float curDepth, float flutterFreq, int numSamples); + void plotBuffer (foleys::MagicPlotSource* plot); + + inline bool shouldTurnOff() const noexcept { return depthSlew[0].getTargetValue() == depthSlewMin; } + inline void updatePhase (int ch) noexcept + { + phase1[ch] += angleDelta1; + phase2[ch] += angleDelta2; + phase3[ch] += angleDelta3; + } + + inline std::pair<float, float> getLFO (int n, int ch) noexcept + { + updatePhase (ch); + flutterPtrs[ch][n] = depthSlew[ch].getNextValue() + * (amp1 * std::cos (phase1[ch] + phaseOff1) + + amp2 * std::cos (phase2[ch] + phaseOff2) + + amp3 * std::cos (phase3[ch] + phaseOff3)); + return std::make_pair (flutterPtrs[ch][n], dcOffset); + } + + inline void boundPhase (int ch) noexcept + { + while (phase1[ch] >= MathConstants<float>::twoPi) + phase1[ch] -= MathConstants<float>::twoPi; + while (phase2[ch] >= MathConstants<float>::twoPi) + phase2[ch] -= MathConstants<float>::twoPi; + while (phase2[ch] >= MathConstants<float>::twoPi) + phase2[ch] -= MathConstants<float>::twoPi; + } + +private: + float phase1[2] = { 0.0f, 0.0f }; + float phase2[2] = { 0.0f, 0.0f }; + float phase3[2] = { 0.0f, 0.0f }; + + float amp1 = 0.0f; + float amp2 = 0.0f; + float amp3 = 0.0f; + SmoothedValue<float, ValueSmoothingTypes::Multiplicative> depthSlew[2]; + + float angleDelta1 = 0.0f; + float angleDelta2 = 0.0f; + float angleDelta3 = 0.0f; + + float dcOffset = 0.0f; + static constexpr float phaseOff1 = 0.0f; + static constexpr float phaseOff2 = 13.0f * MathConstants<float>::pi / 4.0f; + static constexpr float phaseOff3 = -MathConstants<float>::pi / 10.0f; + + AudioBuffer<float> flutterBuffer; + float** flutterPtrs; + float fs = 48000.0f; + + static constexpr float depthSlewMin = 0.001f; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FlutterProcess) +}; + +#endif // FLUTTERPROCESS_H_INCLUDED diff --git a/Plugin/Source/Processors/Timing_Effects/OHProcess.h b/Plugin/Source/Processors/Timing_Effects/OHProcess.h @@ -0,0 +1,79 @@ +#ifndef OHPROCESS_H_INCLUDED +#define OHPROCESS_H_INCLUDED + +#include <JuceHeader.h> + +/** + * Class to simulate the Ornstein-Uhlenbeck process. + * Mostly lifted from https://github.com/mhampton/ZetaCarinaeModules + * under the GPLv3 license. + */ +class OHProcess +{ +public: + OHProcess() = default; + + void prepare (double sampleRate, int samplesPerBlock) + { + dsp::ProcessSpec spec { sampleRate, (uint32) samplesPerBlock, 1 }; + + noiseGen.setNoiseType (chowdsp::Noise<float>::Normal); + noiseGen.setGainLinear (1.0f / 2.33f); + noiseGen.prepare (spec); + + for (int ch = 0; ch < 2; ++ch) + { + lpf[ch].prepare (spec); + lpf[ch].coefficients = dsp::IIR::Coefficients<float>::makeLowPass (sampleRate, 10.0f); + } + + noiseBuffer.setSize (1, samplesPerBlock); + rPtr = noiseBuffer.getReadPointer (0); + + sqrtdelta = 1.0f / std::sqrt ((float) sampleRate); + T = 1.0f / (float) sampleRate; + + y[0] = 1.0f; + y[1] = 0.0f; + } + + void prepareBlock (float amtParam, int numSamples) + { + noiseBuffer.setSize (1, numSamples, false, false, true); + noiseBuffer.clear(); + + dsp::AudioBlock<float> noiseBlock (noiseBuffer); + noiseGen.process (dsp::ProcessContextReplacing<float> (noiseBlock)); + + amtParam = std::pow (amtParam, 1.25f); + amt = amtParam; + damping = amtParam * 20.0f + 1.0f; + mean = amtParam; + } + + inline float process (int n, int ch) noexcept + { + y[ch] += sqrtdelta * rPtr[n] * amt; + y[ch] += damping * (mean - y[ch]) * T; + return lpf[ch].processSample (y[ch]); + } + +private: + float sqrtdelta = 1.0f / std::sqrt (48000.0f); + float T = 1.0f / 48000.0f; + float y[2] = { 0.0f, 0.0f }; + + float amt = 0.0f; + float mean = 0.0f; + float damping = 0.0f; + + chowdsp::Noise<float> noiseGen; + AudioBuffer<float> noiseBuffer; + const float* rPtr = nullptr; + + dsp::IIR::Filter<float> lpf[2]; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OHProcess) +}; + +#endif // OHPROCESS_H_INCLUDED diff --git a/Plugin/Source/Processors/Timing_Effects/WowFlutterProcessor.cpp b/Plugin/Source/Processors/Timing_Effects/WowFlutterProcessor.cpp @@ -0,0 +1,135 @@ +#include "WowFlutterProcessor.h" +#include "../../GUI/LightMeter.h" + +namespace +{ +constexpr float depthSlewMin = 0.001f; +} + +WowFlutterProcessor::WowFlutterProcessor (AudioProcessorValueTreeState& vts) +{ + flutterRate = vts.getRawParameterValue ("rate"); + flutterDepth = vts.getRawParameterValue ("depth"); + + wowRate = vts.getRawParameterValue ("wow_rate"); + wowDepth = vts.getRawParameterValue ("wow_depth"); + wowVariance = vts.getRawParameterValue ("wow_var"); + wowDrift = vts.getRawParameterValue ("wow_drift"); + + flutterOnOff = vts.getRawParameterValue ("flutter_onoff"); +} + +void WowFlutterProcessor::initialisePlots (foleys::MagicGUIState& magicState) +{ + wowPlot = magicState.createAndAddObject<LightMeter> ("wow"); + magicState.addBackgroundProcessing (wowPlot); + + flutterPlot = magicState.createAndAddObject<LightMeter> ("flutter"); + magicState.addBackgroundProcessing (flutterPlot); +} + +void WowFlutterProcessor::createParameterLayout (std::vector<std::unique_ptr<RangedAudioParameter>>& params) +{ + params.push_back (std::make_unique<AudioParameterBool> ("flutter_onoff", "Wow/Flutter On/Off", true)); + + params.push_back (std::make_unique<AudioParameterFloat> ("rate", "Flutter Rate", 0.0f, 1.0f, 0.3f)); + params.push_back (std::make_unique<AudioParameterFloat> ("depth", "Flutter Depth", 0.0f, 1.0f, 0.0f)); + + params.push_back (std::make_unique<AudioParameterFloat> ("wow_rate", "Wow Rate", 0.0f, 1.0f, 0.25f)); + params.push_back (std::make_unique<AudioParameterFloat> ("wow_depth", "Wow Depth", 0.0f, 1.0f, 0.0f)); + params.push_back (std::make_unique<AudioParameterFloat> ("wow_var", "Wow Variance", 0.0f, 1.0f, 0.0f)); + params.push_back (std::make_unique<AudioParameterFloat> ("wow_drift", "Wow Drift", 0.0f, 1.0f, 0.0f)); +} + +void WowFlutterProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) +{ + fs = (float) sampleRate; + + bypass.prepare (samplesPerBlock, bypass.toBool (flutterOnOff)); + wowProcessor.prepare (sampleRate, samplesPerBlock); + flutterProcessor.prepare (sampleRate, samplesPerBlock); + + for (int ch = 0; ch < 2; ++ch) + { + delay.prepare ({ sampleRate, (uint32) samplesPerBlock, 2 }); + delay.setDelay (0.0f); + + dcBlocker[ch].prepare (sampleRate, 15.0f); + } + + wowPlot->prepareToPlay (sampleRate, samplesPerBlock); + flutterPlot->prepareToPlay (sampleRate, samplesPerBlock); +} + +void WowFlutterProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& /*midiMessages*/) +{ + ScopedNoDenormals noDenormals; + + auto curDepthWow = powf (*wowDepth, 3.0f); + auto wowFreq = powf (4.5, *wowRate) - 1.0f; + wowProcessor.prepareBlock (curDepthWow, wowFreq, wowVariance->load(), wowDrift->load(), buffer.getNumSamples()); + + auto curDepthFlutter = powf (powf (*flutterDepth, 3.0f) * 81.0f / 625.0f, 0.5f); + auto flutterFreq = 0.1f * powf (1000.0f, *flutterRate); + flutterProcessor.prepareBlock (curDepthFlutter, flutterFreq, buffer.getNumSamples()); + + bool shouldTurnOff = ! bypass.toBool (flutterOnOff) || (wowProcessor.shouldTurnOff() && flutterProcessor.shouldTurnOff()); + if (bypass.processBlockIn (buffer, ! shouldTurnOff)) + { + processWetBuffer (buffer); + bypass.processBlockOut (buffer, ! shouldTurnOff); + } + else + { + processBypassed (buffer); + } + + // dc block + for (int ch = 0; ch < buffer.getNumChannels(); ++ch) + dcBlocker[ch].processBlock (buffer.getWritePointer (ch), buffer.getNumSamples()); + + wowProcessor.plotBuffer (wowPlot); + flutterProcessor.plotBuffer (flutterPlot); +} + +void WowFlutterProcessor::processWetBuffer (AudioBuffer<float>& buffer) +{ + for (int ch = 0; ch < buffer.getNumChannels(); ++ch) + { + auto* x = buffer.getWritePointer (ch); + for (int n = 0; n < buffer.getNumSamples(); ++n) + { + auto [wowLFO, wowOffset] = wowProcessor.getLFO (n, ch); + auto [flutterLFO, flutterOffset] = flutterProcessor.getLFO (n, ch); + + auto newLength = (wowLFO + flutterLFO + flutterOffset + wowOffset) * fs / 1000.0f; + newLength = jlimit (0.0f, (float) HISTORY_SIZE, newLength); + + delay.setDelay (newLength); + delay.pushSample (ch, x[n]); + x[n] = delay.popSample (ch); + } + + wowProcessor.boundPhase (ch); + flutterProcessor.boundPhase (ch); + } +} + +void WowFlutterProcessor::processBypassed (AudioBuffer<float>& buffer) +{ + for (int ch = 0; ch < buffer.getNumChannels(); ++ch) + { + delay.setDelay (0.0f); + for (int n = 0; n < buffer.getNumSamples(); ++n) + { + wowProcessor.updatePhase (ch); + flutterProcessor.updatePhase (ch); + + delay.pushSample (ch, 0.0f); + delay.popSample (ch); + } + + wowProcessor.boundPhase (ch); + flutterProcessor.boundPhase (ch); + } +} diff --git a/Plugin/Source/Processors/Timing_Effects/WowFlutterProcessor.h b/Plugin/Source/Processors/Timing_Effects/WowFlutterProcessor.h @@ -0,0 +1,50 @@ +#ifndef WOWFLUTTERPROCESSOR_H_INCLUDED +#define WOWFLUTTERPROCESSOR_H_INCLUDED + +#include "../BypassProcessor.h" +#include "../Hysteresis/DCBlocker.h" +#include "FlutterProcess.h" +#include "WowProcess.h" + +class WowFlutterProcessor +{ +public: + WowFlutterProcessor (AudioProcessorValueTreeState& vts); + + void initialisePlots (foleys::MagicGUIState& magicState); + static void createParameterLayout (std::vector<std::unique_ptr<RangedAudioParameter>>& params); + + void prepareToPlay (double sampleRate, int samplesPerBlock); + void processBlock (AudioBuffer<float>&, MidiBuffer&); + + void processWetBuffer (AudioBuffer<float>& buffer); + void processBypassed (AudioBuffer<float>& buffer); + +private: + std::atomic<float>* flutterOnOff = nullptr; + std::atomic<float>* flutterRate = nullptr; + std::atomic<float>* flutterDepth = nullptr; + std::atomic<float>* wowRate = nullptr; + std::atomic<float>* wowDepth = nullptr; + std::atomic<float>* wowVariance = nullptr; + std::atomic<float>* wowDrift = nullptr; + + BypassProcessor bypass; + float fs = 48000.0f; + + WowProcess wowProcessor; + FlutterProcess flutterProcessor; + foleys::MagicPlotSource *wowPlot = nullptr, *flutterPlot = nullptr; + + enum + { + HISTORY_SIZE = 1 << 21, + }; + + dsp::DelayLine<float, dsp::DelayLineInterpolationTypes::Lagrange3rd> delay { HISTORY_SIZE }; + DCBlocker dcBlocker[2]; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WowFlutterProcessor) +}; + +#endif //WOWFLUTTRPROCESSOR_H_INCLUDED diff --git a/Plugin/Source/Processors/Timing_Effects/WowProcess.cpp b/Plugin/Source/Processors/Timing_Effects/WowProcess.cpp @@ -0,0 +1,42 @@ +#include "WowProcess.h" + +void WowProcess::prepare (double sampleRate, int samplesPerBlock) +{ + fs = (float) sampleRate; + + for (int ch = 0; ch < 2; ++ch) + { + depthSlew[ch].reset (sampleRate, 0.05); + depthSlew[ch].setCurrentAndTargetValue (depthSlewMin); + phase[ch] = 0.0f; + } + + amp = 1000.0f * 1000.0f / (float) sampleRate; + wowBuffer.setSize (2, samplesPerBlock); + + ohProc.prepare (sampleRate, samplesPerBlock); +} + +void WowProcess::prepareBlock (float curDepth, float wowFreq, float wowVar, float wowDrift, int numSamples) +{ + depthSlew[0].setTargetValue (jmax (depthSlewMin, curDepth)); + depthSlew[1].setTargetValue (jmax (depthSlewMin, curDepth)); + + auto freqAdjust = wowFreq * (1.0f + std::pow (driftRand.nextFloat(), 1.25f) * wowDrift); + angleDelta = MathConstants<float>::twoPi * freqAdjust / fs; + + wowBuffer.setSize (2, numSamples, false, false, true); + wowBuffer.clear(); + wowPtrs = wowBuffer.getArrayOfWritePointers(); + + ohProc.prepareBlock (wowVar, numSamples); +} + +void WowProcess::plotBuffer (foleys::MagicPlotSource* plot) +{ + if (shouldTurnOff()) + wowBuffer.clear(); + + wowBuffer.applyGain (0.83333f / amp); + plot->pushSamples (wowBuffer); +} diff --git a/Plugin/Source/Processors/Timing_Effects/WowProcess.h b/Plugin/Source/Processors/Timing_Effects/WowProcess.h @@ -0,0 +1,51 @@ +#ifndef WOWPROCESS_H_INCLUDED +#define WOWPROCESS_H_INCLUDED + +#include "OHProcess.h" +#include <JuceHeader.h> + +class WowProcess +{ +public: + WowProcess() = default; + + void prepare (double sampleRate, int samplesPerBlock); + void prepareBlock (float curDepth, float wowFreq, float wowVar, float wowDrift, int numSamples); + void plotBuffer (foleys::MagicPlotSource* plot); + + inline bool shouldTurnOff() const noexcept { return depthSlew[0].getTargetValue() == depthSlewMin; } + inline void updatePhase (int ch) noexcept { phase[ch] += angleDelta; } + + inline std::pair<float, float> getLFO (int n, int ch) noexcept + { + updatePhase (ch); + auto curDepth = depthSlew[ch].getNextValue() * amp; + wowPtrs[ch][n] = curDepth * (std::cos (phase[ch]) + ohProc.process (n, ch)); + return std::make_pair (wowPtrs[ch][n], curDepth); + } + + inline void boundPhase (int ch) noexcept + { + while (phase[ch] >= MathConstants<float>::twoPi) + phase[ch] -= MathConstants<float>::twoPi; + } + +private: + float angleDelta = 0.0f; + float amp = 0.0f; + float phase[2] = { 0.0f, 0.0f }; + SmoothedValue<float, ValueSmoothingTypes::Multiplicative> depthSlew[2]; + + AudioBuffer<float> wowBuffer; + float** wowPtrs = nullptr; + float fs = 44100.0f; + + OHProcess ohProc; + Random driftRand; + + static constexpr float depthSlewMin = 0.001f; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WowProcess) +}; + +#endif // WOWPROCESS_H_INCLUDED diff --git a/Simulations/TimingEffects/OHProcess.py b/Simulations/TimingEffects/OHProcess.py @@ -0,0 +1,41 @@ +import numpy as np +from scipy import signal +import matplotlib.pyplot as plt + +# Code for simulating drift and variance for the wow control +# using an Ornstein-Uhlenbeck process + +freq = 0.5 +FS = 48000 +mag = 1.0 +N = int(FS * 20) + +rr = np.random.normal(0, 1.0, N) +def oh_process(inp, N, amt, damping, mean): + y = 0 + out = np.zeros(N) + sqrtdelta = 1.0 / np.sqrt(FS); + T = 1.0 / FS + + for i in range(N): + # y = (y + T * amt * rr[i] + damping * T * mean) / (1.0 + T * damping) + y += sqrtdelta * rr[i] * amt + y += 0.001 * damping * (mean - y) * T + out[i] = y + return out + + +x = mag * np.sin(2 * np.pi * freq / FS * np.arange(N)) +plt.plot(x, label='sin') + +for amt in [1.0]: # [0.0, 0.33, 0.67, 1.0]: + amt = np.power(amt, 1.15) + y = x + oh_process(x, N, amt * 0.5, amt * 0.5 + 0.25, 1.0 * amt) + b, a = signal.butter(2, Wn=10, fs=FS) + y = signal.lfilter(b, a, y) + plt.plot(y, label=f'{amt}') + +# y = np.power(0.5 * (x + 1), 10.0) +# plt.plot(y) + +plt.show()