AnalogTapeModel

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

commit 9e581e68736be43bbd2da000b5af97bb9c647a17
parent b785ee294ed321fef4bee98736a0ddebcb91cf8a
Author: jatinchowdhury18 <jatinchowdhury18@gmail.com>
Date:   Tue, 23 Mar 2021 20:25:50 -0700

Wow/Flutter rate sync (#162)

* Implement speed sync for flutter

* Implement tempo-sync for flutter

* Implement rate sync for wow

* {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:
MPlugin/Source/GUI/Assets/gui.xml | 6++++++
MPlugin/Source/GUI/CMakeLists.txt | 1+
APlugin/Source/GUI/WowFlutterMenu.cpp | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APlugin/Source/GUI/WowFlutterMenu.h | 46++++++++++++++++++++++++++++++++++++++++++++++
MPlugin/Source/PluginProcessor.cpp | 15+++++++++++++++
MPlugin/Source/PluginProcessor.h | 4+++-
6 files changed, 201 insertions(+), 1 deletion(-)

diff --git a/Plugin/Source/GUI/Assets/gui.xml b/Plugin/Source/GUI/Assets/gui.xml @@ -179,6 +179,9 @@ </View> <View display="tabbed" padding="0" margin="2" background-color="FF31323A" lookAndFeel="MyLNF"> <View tab-caption="Flutter" flex-direction="column" background-color="FF31323A"> + <FlutterMenu margin="0" padding="0" background-color="00000000" + max-height="30" name="Flutter Sync" tooltip="Snaps the flutter rate to a synchronized value."/> + <View margin="0" padding="0" flex-grow="0.1" background-color="00000000"/> <Slider caption="Depth" parameter="depth" max-height="150" class="Slider" name="Flutter Depth" tooltip="Sets depth of the tape flutter." margin="0" padding="0"/> @@ -192,6 +195,9 @@ 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" padding="0" margin="3"> + <WowMenu margin="0" padding="0" background-color="00000000" + max-height="30" name="Wow Sync" tooltip="Snaps the wow rate to a synchronized value."/> + <View margin="0" padding="0" flex-grow="0.1" background-color="00000000"/> <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" slider-type="linear-horizontal"/> diff --git a/Plugin/Source/GUI/CMakeLists.txt b/Plugin/Source/GUI/CMakeLists.txt @@ -8,4 +8,5 @@ target_sources(CHOWTapeModel PRIVATE TapeScope.cpp TitleComp.cpp TooltipComp.cpp + WowFlutterMenu.cpp ) diff --git a/Plugin/Source/GUI/WowFlutterMenu.cpp b/Plugin/Source/GUI/WowFlutterMenu.cpp @@ -0,0 +1,130 @@ +#include "WowFlutterMenu.h" + +class WowFlutterMenuLNF : public ComboBoxLNF +{ +public: + WowFlutterMenuLNF() = default; + + void drawComboBox (Graphics& g, int width, int height, bool, int, int, int, int, ComboBox& box) override + { + auto cornerSize = 5.0f; + Rectangle<int> boxBounds (0, 0, width, height); + + g.setColour (box.findColour (ComboBox::backgroundColourId)); + g.fillRoundedRectangle (boxBounds.toFloat(), cornerSize); + + g.setColour (box.findColour (ComboBox::outlineColourId)); + g.drawRoundedRectangle (boxBounds.toFloat().reduced (1.0f), cornerSize, 1.0f); + + if (box.getName().isNotEmpty()) + { + g.setColour (Colours::white); + g.setFont (getComboBoxFont (box).boldened()); + g.drawFittedText (box.getName(), boxBounds, Justification::centred, 1); + } + } + + void positionComboBoxText (ComboBox& box, Label& label) override + { + auto b = box.getBounds(); + label.setBounds (b); + label.setFont (getComboBoxFont (box).boldened()); + } + +private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WowFlutterMenuLNF) +}; + +//============================================================================ +namespace +{ +float flutterFreqToParam (float freq) +{ + return 0.144765f * std::log (10.0f * freq); +} + +float wowFreqToParam (float freq) +{ + return 0.664859f * std::log (freq + 1.0f); +} +} // namespace + +WowFlutterMenu::WowFlutterMenu (const ChowtapeModelAudioProcessor& proc, const String& type) : proc (proc) +{ + setupUI(); + + const bool isFlutter = type == "Flutter"; + setupRateParam (isFlutter); + + auto snycToTapeSpeed = [=, &proc] { + const auto& vts = proc.getVTS(); + auto speedParam = dynamic_cast<AudioParameterFloat*> (vts.getParameter ("speed")); + auto speedIps = speedParam->get(); + + auto motorFreq = speedIps / (6.0f * MathConstants<float>::pi); + auto newRate = isFlutter ? flutterFreqToParam (motorFreq) + : wowFreqToParam (std::sqrt (motorFreq)); + setRateValue (newRate); + }; + + auto syncToRhythm = [=, &proc] (float multipleOfQuarterNote) { + const auto& posInfo = proc.getPositionInfo(); + auto quarterNoteTime = 60.0f / (float) posInfo.bpm; + + auto newFreq = 1.0f / (quarterNoteTime * multipleOfQuarterNote); + auto newRate = isFlutter ? flutterFreqToParam (newFreq) : wowFreqToParam (newFreq); + setRateValue (newRate); + }; + + auto menu = getRootMenu(); + menu->addItem ("Sync to tape speed", snycToTapeSpeed); + + if (isFlutter) + { + menu->addItem ("Sync to eighth note", std::bind (syncToRhythm, 0.125f)); + menu->addItem ("Sync to quarter note", std::bind (syncToRhythm, 0.25f)); + menu->addItem ("Sync to half note", std::bind (syncToRhythm, 0.5f)); + menu->addItem ("Sync to whole note", std::bind (syncToRhythm, 1.0f)); + } + else + { + menu->addItem ("Sync to one bar", std::bind (syncToRhythm, 1.0f)); + menu->addItem ("Sync to two bars", std::bind (syncToRhythm, 2.0f)); + menu->addItem ("Sync to four bars", std::bind (syncToRhythm, 4.0f)); + menu->addItem ("Sync to eight bars", std::bind (syncToRhythm, 8.0f)); + } +} + +WowFlutterMenu::~WowFlutterMenu() +{ + setLookAndFeel (nullptr); +} + +void WowFlutterMenu::setupUI() +{ + setColour (ComboBox::backgroundColourId, Colours::transparentBlack); + setColour (ComboBox::outlineColourId, Colour (0xff595c6b)); + + lnf = std::make_unique<WowFlutterMenuLNF>(); + setLookAndFeel (lnf.get()); + + onChange = [=] { setSelectedItemIndex (-1); }; +} + +void WowFlutterMenu::setupRateParam (bool isFlutter) +{ + auto& vts = proc.getVTS(); + if (isFlutter) + rateParam = dynamic_cast<AudioParameterFloat*> (vts.getParameter ("rate")); + else // "Wow" + rateParam = dynamic_cast<AudioParameterFloat*> (vts.getParameter ("wow_rate")); + + jassert (rateParam); // this should never be nullptr! +} + +void WowFlutterMenu::setRateValue (float value) +{ + rateParam->beginChangeGesture(); + rateParam->setValueNotifyingHost (jlimit (0.0f, 1.0f, value)); + rateParam->endChangeGesture(); +} diff --git a/Plugin/Source/GUI/WowFlutterMenu.h b/Plugin/Source/GUI/WowFlutterMenu.h @@ -0,0 +1,46 @@ +#pragma once + +#include "../PluginProcessor.h" +#include "MyLNF.h" + +class WowFlutterMenuLNF; +class WowFlutterMenu : public ComboBox +{ +public: + WowFlutterMenu (const ChowtapeModelAudioProcessor& proc, const String& type); + ~WowFlutterMenu() override; + + void setupUI(); + void setupRateParam (bool isFlutter); + void setRateValue (float value); + +private: + AudioParameterFloat* rateParam = nullptr; + + const ChowtapeModelAudioProcessor& proc; + std::unique_ptr<WowFlutterMenuLNF> lnf; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WowFlutterMenu) +}; + +class WowFlutterMenuItem : public foleys::GuiItem +{ +public: + WowFlutterMenuItem (foleys::MagicGUIBuilder& builder, const ValueTree& node, const String& type) : foleys::GuiItem (builder, node) + { + const auto* proc = dynamic_cast<ChowtapeModelAudioProcessor*> (builder.getMagicState().getProcessor()); + jassert (proc); // this should never be nullptr! + + menu = std::make_unique<WowFlutterMenu> (*proc, type); + addAndMakeVisible (menu.get()); + } + + void update() override {} + + Component* getWrappedComponent() override { return menu.get(); } + +private: + std::unique_ptr<WowFlutterMenu> menu; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WowFlutterMenuItem) +}; diff --git a/Plugin/Source/PluginProcessor.cpp b/Plugin/Source/PluginProcessor.cpp @@ -13,6 +13,7 @@ #include "GUI/PowerButton.h" #include "GUI/TitleComp.h" #include "GUI/TooltipComp.h" +#include "GUI/WowFlutterMenu.h" namespace { @@ -33,6 +34,9 @@ ChowtapeModelAudioProcessor::ChowtapeModelAudioProcessor() onOffManager (vts, this), mixGroupsController (vts, this) { + positionInfo.bpm = 120.0; + positionInfo.timeSigNumerator = 4; + scope = magicState.createAndAddObject<TapeScope> ("scope", getMainBusNumInputChannels()); flutter.initialisePlots (magicState); @@ -220,6 +224,9 @@ void ChowtapeModelAudioProcessor::processBlock (AudioBuffer<float>& buffer, Midi { ScopedNoDenormals noDenormals; + if (auto playhead = getPlayHead()) + playhead->getCurrentPosition (positionInfo); + inGain.setGain (Decibels::decibelsToGain (vts.getRawParameterValue ("ingain")->load())); outGain.setGain (Decibels::decibelsToGain (vts.getRawParameterValue ("outgain")->load())); dryWet.setDryWet (*vts.getRawParameterValue ("drywet") / 100.0f); @@ -284,6 +291,14 @@ AudioProcessorEditor* ChowtapeModelAudioProcessor::createEditor() builder->registerFactory ("MixGroupViz", &MixGroupVizItem::factory); builder->registerFactory ("PowerButton", &PowerButtonItem::factory); + builder->registerFactory ("FlutterMenu", [] (foleys::MagicGUIBuilder& builder, const ValueTree& node) -> std::unique_ptr<foleys::GuiItem> { + return std::make_unique<WowFlutterMenuItem> (builder, node, "Flutter"); + }); + + builder->registerFactory ("WowMenu", [] (foleys::MagicGUIBuilder& builder, const ValueTree& node) -> std::unique_ptr<foleys::GuiItem> { + return std::make_unique<WowFlutterMenuItem> (builder, node, "Wow"); + }); + builder->registerJUCELookAndFeels(); builder->registerLookAndFeel ("MyLNF", std::make_unique<MyLNF>()); builder->registerLookAndFeel ("ComboBoxLNF", std::make_unique<ComboBoxLNF>()); diff --git a/Plugin/Source/PluginProcessor.h b/Plugin/Source/PluginProcessor.h @@ -73,7 +73,8 @@ public: void setStateInformation (const void* data, int sizeInBytes) override; PresetManager& getPresetManager() { return presetManager; } - const AudioProcessorValueTreeState& getVTS() { return vts; } + const AudioProcessorValueTreeState& getVTS() const { return vts; } + const AudioPlayHead::CurrentPositionInfo& getPositionInfo() const { return positionInfo; } private: using DryDelayType = chowdsp::DelayLine<float, chowdsp::DelayLineInterpolationTypes::Lagrange5th>; @@ -105,6 +106,7 @@ private: MyLNF myLNF; AutoUpdater updater; MixGroupsController mixGroupsController; + AudioPlayHead::CurrentPositionInfo positionInfo; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ChowtapeModelAudioProcessor)