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:
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()