AnalogTapeModel

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

commit b55eb970bd9c9cffe9f6297e32cb7ce39fb8f751
parent 4258a72bdb01d5799db24be68c8e6aa18e657ff4
Author: jatinchowdhury18 <jatinchowdhury18@gmail.com>
Date:   Wed, 28 Oct 2020 15:39:23 -0700

Add wow/flutter visualizations (#104)

Co-authored-by: jatinchowdhury18 <jatinchowdhury18@users.noreply.github.com>
Diffstat:
MCHANGELOG.md | 7++++---
MPlugin/CHOWTapeModel.jucer | 2++
MPlugin/Source/GUI/Assets/gui.xml | 26++++++++++++++++----------
APlugin/Source/GUI/LightMeter.cpp | 25+++++++++++++++++++++++++
APlugin/Source/GUI/LightMeter.h | 40++++++++++++++++++++++++++++++++++++++++
MPlugin/Source/PluginProcessor.cpp | 1+
MPlugin/Source/Processors/Timing_Effects/Flutter.cpp | 31+++++++++++++++++++++++++++++++
MPlugin/Source/Processors/Timing_Effects/Flutter.h | 4++++
8 files changed, 123 insertions(+), 13 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -3,9 +3,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] -- Tone section: added transition frequency control, and made bass/treble controls more extreme -- Added coloured circle on bottom bar to visualize mix group -- Added buttons to snap tape speed to conventional values +- Added visualizations for Wow and Flutter. +- Tone section: added transition frequency control, and made bass/treble controls more extreme. +- Added coloured circle on bottom bar to visualize mix group. +- Added buttons to snap tape speed to conventional values. ## [2.6.0] - 2020-09-29 - Added Pre/post emphasis filters for the hysteresis stage. diff --git a/Plugin/CHOWTapeModel.jucer b/Plugin/CHOWTapeModel.jucer @@ -24,6 +24,8 @@ <FILE id="u8IfBD" name="AutoUpdating.h" compile="0" resource="0" file="Source/GUI/AutoUpdating.h"/> <FILE id="rOj90C" name="InfoComp.cpp" compile="1" resource="0" file="Source/GUI/InfoComp.cpp"/> <FILE id="FxvDV3" name="InfoComp.h" compile="0" resource="0" file="Source/GUI/InfoComp.h"/> + <FILE id="AG1VNL" name="LightMeter.cpp" compile="1" resource="0" file="Source/GUI/LightMeter.cpp"/> + <FILE id="F1yjo4" name="LightMeter.h" compile="0" resource="0" file="Source/GUI/LightMeter.h"/> <FILE id="SYzWcj" name="MixGroupViz.cpp" compile="1" resource="0" file="Source/GUI/MixGroupViz.cpp"/> <FILE id="YzAgWK" name="MixGroupViz.h" compile="0" resource="0" file="Source/GUI/MixGroupViz.h"/> <FILE id="IgOtsG" name="MyLNF.cpp" compile="1" resource="0" file="Source/GUI/MyLNF.cpp"/> diff --git a/Plugin/Source/GUI/Assets/gui.xml b/Plugin/Source/GUI/Assets/gui.xml @@ -64,12 +64,10 @@ </View> <View flex-direction="column" tab-color="" background-color="FF31323A" padding="0" tab-caption="Tone"> - <Slider caption="Treble" parameter="h_treble" class="Slider" name="Treble" 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."/> + <Slider caption="Bass" parameter="h_bass" class="Slider" name="Bass" + tooltip="Controls the bass response of the pre/post-emphasis filters."/> + <Slider caption="Treble" parameter="h_treble" class="Slider" name="Treble" + tooltip="Controls the treble response of the pre/post-emphasis filters."/> </View> </View> <View display="tabbed" padding="0" background-color="FF31323A" flex-grow="1.5" @@ -129,15 +127,23 @@ <View display="tabbed" padding="0" 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."/> + name="Flutter Depth" tooltip="Sets depth of the tape flutter." + margin="0" padding="0"/> <Slider caption="Rate" parameter="rate" class="Slider" max-height="150" - name="Flutter Rate" tooltip="Sets the rate of the tape flutter."/> + name="Flutter Rate" tooltip="Sets the rate of the tape flutter." + margin="0" padding="0"/> + <Plot source="flutter" plot-decay="0.8" background-color="FF1E1F22" + flex-grow="0.8" plot-color="FFEAA92C" plot-fill-color="CC8B3232"/> </View> <View tab-caption="Wow" flex-direction="column" background-color="FF31323A"> <Slider caption="Depth" parameter="wow_depth" max-height="150" class="Slider" - name="Wow Depth" tooltip="Sets the depth of the tape wow."/> + name="Wow Depth" tooltip="Sets the depth of the tape wow." margin="0" + padding="0"/> <Slider caption="Rate" parameter="wow_rate" class="Slider" max-height="150" - name="Wow Rate" tooltip="Sets the rate of the tape wow."/> + 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" + plot-color="FFEAA92C" plot-fill-color="CC8B3232"/> </View> </View> </View> diff --git a/Plugin/Source/GUI/LightMeter.cpp b/Plugin/Source/GUI/LightMeter.cpp @@ -0,0 +1,25 @@ +#include "LightMeter.h" + +void LightMeter::pushSamples (const juce::AudioBuffer<float>& buffer) +{ + rms = buffer.getRMSLevel (0, 0, buffer.getNumSamples()); + + if (std::isnan (rms.load())) + rms = 0.0f; + + resetLastDataFlag(); +} + +void LightMeter::createPlotPaths (juce::Path& path, juce::Path& filledPath, juce::Rectangle<float> bounds, foleys::MagicPlotComponent&) +{ + const auto centre = bounds.getCentre(); + const auto maxDiameter = jmin (bounds.getHeight(), bounds.getWidth()); + + const auto diameter = maxDiameter * jlimit (0.0f, 1.0f, rms.load()); + const auto ellipseRect = Rectangle<float> (diameter, diameter).withCentre (centre); + + path.clear(); + path.addEllipse (ellipseRect); + + filledPath = path; +} diff --git a/Plugin/Source/GUI/LightMeter.h b/Plugin/Source/GUI/LightMeter.h @@ -0,0 +1,40 @@ +#ifndef LIGHTMETER_H_INCLUDED +#define LIGHTMETER_H_INCLUDED + +#include "JuceHeader.h" + +class LightMeter : public foleys::MagicPlotSource, + public SettableTooltipClient +{ +public: + LightMeter() { rms = 0.0f; } + + /** + This method is called by the MagicProcessorState to allow the plot computation to be set up + */ + void prepareToPlay (double, int) override {} + + /** + This is the callback whenever new sample data arrives. It is the subclasses + responsibility to put that into a FIFO and return as quickly as possible. + */ + void pushSamples (const juce::AudioBuffer<float>& buffer) override; + + /** + This is the callback that creates the plot for drawing. + + @param path is the path instance that is constructed by the MagicPlotSource + @param filledPath is the path instance that is constructed by the MagicPlotSource to be filled + @param bounds the bounds of the plot + @param component grants access to the plot component, e.g. to find the colours from it + */ + void createPlotPaths (juce::Path& path, juce::Path& filledPath, juce::Rectangle<float> bounds, foleys::MagicPlotComponent& component) override; + +private: + std::atomic<float> rms; + + JUCE_DECLARE_WEAK_REFERENCEABLE (LightMeter) + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LightMeter) +}; + +#endif // LIGHTMETER_H_INCLUDED diff --git a/Plugin/Source/PluginProcessor.cpp b/Plugin/Source/PluginProcessor.cpp @@ -38,6 +38,7 @@ ChowtapeModelAudioProcessor::ChowtapeModelAudioProcessor() lossFilter[ch].reset (new LossFilter (vts)); scope = magicState.createAndAddObject<foleys::MagicOscilloscope> ("scope"); + flutter.initialisePlots (magicState); LookAndFeel::setDefaultLookAndFeel (&myLNF); diff --git a/Plugin/Source/Processors/Timing_Effects/Flutter.cpp b/Plugin/Source/Processors/Timing_Effects/Flutter.cpp @@ -1,4 +1,5 @@ #include "Flutter.h" +#include "../../GUI/LightMeter.h" namespace { @@ -20,6 +21,15 @@ Flutter::Flutter (AudioProcessorValueTreeState& vts) 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<AudioParameterFloat> ("rate", "Rate", 0.0f, 1.0f, 0.3f)); @@ -60,6 +70,11 @@ void Flutter::prepareToPlay (double sampleRate, int samplesPerBlock) 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*/) @@ -82,6 +97,11 @@ void Flutter::processBlock (AudioBuffer<float>& buffer, MidiBuffer& /*midiMessag 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 = depthSlewWow[0].getTargetValue() == depthSlewMin && depthSlewFlutter[0].getTargetValue() == depthSlewMin; if (! isOff && ! shouldTurnOff) // process normally @@ -116,6 +136,12 @@ void Flutter::processBlock (AudioBuffer<float>& buffer, MidiBuffer& /*midiMessag // 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) @@ -123,6 +149,8 @@ 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; @@ -142,6 +170,9 @@ void Flutter::processWetBuffer (AudioBuffer<float>& buffer) 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) diff --git a/Plugin/Source/Processors/Timing_Effects/Flutter.h b/Plugin/Source/Processors/Timing_Effects/Flutter.h @@ -9,6 +9,7 @@ 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); @@ -50,6 +51,9 @@ private: 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,