diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index 03ed3193..a988a36b 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -34,6 +34,7 @@ target_link_libraries(juce_plugin_modules chowdsp::chowdsp_plugin_base chowdsp::chowdsp_plugin_utils chowdsp::chowdsp_reverb + chowdsp::chowdsp_rhythm chowdsp::chowdsp_presets chowdsp::chowdsp_serialization chowdsp::chowdsp_units diff --git a/src/BYOD.cpp b/src/BYOD.cpp index 5a6dcc3f..4fc2f403 100644 --- a/src/BYOD.cpp +++ b/src/BYOD.cpp @@ -66,6 +66,9 @@ void BYOD::processBlock (AudioBuffer& buffer, MidiBuffer& midi) const juce::ScopedNoDenormals noDenormals {}; AudioProcessLoadMeasurer::ScopedTimer loadTimer { loadMeasurer, buffer.getNumSamples() }; + //get playhead + procs->getPlayheadHelper().process (getPlayHead(), buffer.getNumSamples()); + // push samples into bypass delay bypassScratchBuffer.makeCopyOf (buffer, true); processBypassDelay (bypassScratchBuffer); diff --git a/src/BYOD.h b/src/BYOD.h index afe3174a..e526a311 100644 --- a/src/BYOD.h +++ b/src/BYOD.h @@ -1,5 +1,6 @@ #pragma once +#include "processors/PlayheadHelpers.h" #include "processors/ProcessorStore.h" #include "processors/chain/ProcessorChain.h" #include "state/ParamForwardManager.h" @@ -54,7 +55,7 @@ class BYOD : public chowdsp::PluginBase [[maybe_unused]] chowdsp::SharedLNFAllocator lnfAllocator; // keep alive! ProcessorStore procStore; - std::unique_ptr procs; + std::unique_ptr procs; //ptrs to processor chain [[maybe_unused]] std::unique_ptr paramForwarder; AudioBuffer bypassScratchBuffer; diff --git a/src/headless/CMakeLists.txt b/src/headless/CMakeLists.txt index 9bee559b..64722963 100644 --- a/src/headless/CMakeLists.txt +++ b/src/headless/CMakeLists.txt @@ -42,6 +42,7 @@ target_include_directories(BYOD_headless ../../modules/chowdsp_utils/modules/gui ../../modules/chowdsp_utils/modules/plugin ../../modules/chowdsp_utils/modules/common + ../../modules/chowdsp_utils/modules/music ../../modules/JUCE/modules ../../modules/chowdsp_wdf/include ../../modules/ea_variant diff --git a/src/headless/tests/UnitTests.h b/src/headless/tests/UnitTests.h index 79c74447..5cf98d7a 100644 --- a/src/headless/tests/UnitTests.h +++ b/src/headless/tests/UnitTests.h @@ -2,6 +2,7 @@ static inline void runTestForAllProcessors (UnitTest* ut, const std::function& testFunc, const StringArray& procsToSkip = {}) { + PlayheadHelpers playheadHelper; for (auto [name, storeEntry] : ProcessorStore::getStoreMap()) { if (procsToSkip.contains (name)) @@ -11,6 +12,7 @@ static inline void runTestForAllProcessors (UnitTest* ut, const std::functionplayheadHelpers = &playheadHelper; ut->beginTest (proc->getName() + " Test"); testFunc (proc.get()); proc->freeInternalMemory(); diff --git a/src/processors/BaseProcessor.h b/src/processors/BaseProcessor.h index e0086215..f2155156 100644 --- a/src/processors/BaseProcessor.h +++ b/src/processors/BaseProcessor.h @@ -35,6 +35,7 @@ struct ProcessorUIOptions class BaseProcessor; class ProcessorEditor; +struct PlayheadHelpers; namespace netlist { struct CircuitQuantityList; @@ -197,6 +198,9 @@ class BaseProcessor : private JuceProcWrapper */ const MidiBuffer* midiBuffer = nullptr; + /** Provided by the processor chain */ + const PlayheadHelpers* playheadHelpers = nullptr; + /** Returns a tooltip string for a given port. */ virtual String getTooltipForPort (int portIndex, bool isInput); diff --git a/src/processors/PlayheadHelpers.h b/src/processors/PlayheadHelpers.h new file mode 100644 index 00000000..69d4dd95 --- /dev/null +++ b/src/processors/PlayheadHelpers.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +struct PlayheadHelpers +{ + void process (AudioPlayHead* playHead, int numSamples) + { + ignoreUnused (numSamples); + if (playHead != nullptr) + { + bpm.store (playHead->getPosition().orFallback (AudioPlayHead::PositionInfo {}).getBpm().orFallback (120.0)); + } + else + { + bpm.store (120.0); + } + } + + std::atomic bpm; +}; diff --git a/src/processors/chain/ProcessorChain.h b/src/processors/chain/ProcessorChain.h index b34646fe..5ea64753 100644 --- a/src/processors/chain/ProcessorChain.h +++ b/src/processors/chain/ProcessorChain.h @@ -5,6 +5,7 @@ #include "../utility/InputProcessor.h" #include "../utility/OutputProcessor.h" +#include "processors/PlayheadHelpers.h" class ProcessorChainActionHelper; class ProcessorChainPortMagnitudesHelper; @@ -34,6 +35,7 @@ class ProcessorChain : private AudioProcessorValueTreeState::Listener auto& getActionHelper() { return *actionHelper; } auto& getStateHelper() { return *stateHelper; } auto& getOversampling() { return ioProcessor.getOversampling(); } + auto& getPlayheadHelper() { return playheadHelper; } chowdsp::Broadcaster processorAddedBroadcaster; chowdsp::Broadcaster processorRemovedBroadcaster; @@ -78,6 +80,7 @@ class ProcessorChain : private AudioProcessorValueTreeState::Listener std::unique_ptr& paramForwardManager; MidiBuffer internalMidiBuffer; + PlayheadHelpers playheadHelper; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProcessorChain) }; diff --git a/src/processors/chain/ProcessorChainActions.cpp b/src/processors/chain/ProcessorChainActions.cpp index 28ff340c..f864796d 100644 --- a/src/processors/chain/ProcessorChainActions.cpp +++ b/src/processors/chain/ProcessorChainActions.cpp @@ -25,6 +25,7 @@ class ProcChainActions { Logger::writeToLog (String ("Creating processor: ") + newProc->getName()); + newProc->playheadHelpers = &chain.playheadHelper; auto osFactor = chain.ioProcessor.getOversamplingFactor(); newProc->prepareProcessing (osFactor * chain.mySampleRate, osFactor * chain.mySamplesPerBlock); diff --git a/src/processors/other/Delay.cpp b/src/processors/other/Delay.cpp index 4c62a325..348c4e31 100644 --- a/src/processors/other/Delay.cpp +++ b/src/processors/other/Delay.cpp @@ -1,24 +1,39 @@ #include "Delay.h" #include "../ParameterHelpers.h" +#include "gui/utils/ModulatableSlider.h" +#include "processors/PlayheadHelpers.h" + +using namespace chowdsp::RhythmUtils; +//using namespace chowdsp::RhythmParameter; namespace { const String delayTypeTag = "delay_type"; const String pingPongTag = "ping_pong"; +const String freqTag = "freq"; +const String feedBackTag = "feedback"; +const String mixTag = "mix"; + +const String delayTimeMsTag = "time_ms"; +const String tempoSyncTag = "tempo_sync"; +const String tempoSyncAmountTag = "time_tempo_sync"; } // namespace DelayModule::DelayModule (UndoManager* um) : BaseProcessor ("Delay", createParameterLayout(), um) { using namespace ParameterHelpers; - loadParameterPointer (delayTimeMsParam, vts, "time_ms"); - loadParameterPointer (freqParam, vts, "freq"); - loadParameterPointer (feedbackParam, vts, "feedback"); - loadParameterPointer (mixParam, vts, "mix"); + loadParameterPointer (freqParam, vts, freqTag); + loadParameterPointer (feedbackParam, vts, feedBackTag); + loadParameterPointer (mixParam, vts, mixTag); + loadParameterPointer (delayTimeMsParam, vts, delayTimeMsTag); + loadParameterPointer (delayTimeRhythmParam, vts, tempoSyncAmountTag); + tempoSyncOnOffParam = vts.getRawParameterValue (tempoSyncTag); delayTypeParam = vts.getRawParameterValue (delayTypeTag); pingPongParam = vts.getRawParameterValue (pingPongTag); addPopupMenuParameter (delayTypeTag); addPopupMenuParameter (pingPongTag); + addPopupMenuParameter (tempoSyncTag); uiOptions.backgroundColour = Colours::cyan.darker (0.1f); uiOptions.powerColour = Colours::gold; @@ -31,12 +46,17 @@ ParamLayout DelayModule::createParameterLayout() using namespace ParameterHelpers; auto params = createBaseParams(); - createTimeMsParameter (params, "time_ms", "Delay Time", createNormalisableRange (20.0f, 2000.0f, 200.0f), 100.0f); - - createFreqParameter (params, "freq", "Cutoff", 500.0f, 10000.0f, 4000.0f, 10000.0f); - createPercentParameter (params, "feedback", "Feedback", 0.0f); - createPercentParameter (params, "mix", "Mix", 0.5f); - + createTimeMsParameter (params, delayTimeMsTag, "Delay Time", createNormalisableRange (20.0f, 2000.0f, 200.0f), 100.0f); + createFreqParameter (params, freqTag, "Cutoff", 500.0f, 10000.0f, 4000.0f, 10000.0f); + createPercentParameter (params, feedBackTag, "Feedback", 0.0f); + createPercentParameter (params, mixTag, "Mix", 0.5f); + + emplace_param (params, + tempoSyncAmountTag, + "Delay Rhythm", + std::initializer_list { HALF, QUARTER, EIGHTH, EIGHTH_DOT }, + HALF); + emplace_param (params, tempoSyncTag, "Tempo Sync", false); emplace_param (params, delayTypeTag, "Delay Type", StringArray { "Clean", "Lo-Fi" }, 0); emplace_param (params, pingPongTag, "Ping-Pong", false); @@ -217,8 +237,21 @@ void DelayModule::processPingPongDelay (AudioBuffer& buffer, DelayType& d void DelayModule::processAudio (AudioBuffer& buffer) { + jassert (playheadHelpers != nullptr); + feedbackSmoothBuffer.process (std::pow (feedbackParam->getCurrentValue() * 0.67f, 0.9f), buffer.getNumSamples()); - delaySmooth.setTargetValue (fs * *delayTimeMsParam * 0.001f); + const auto tempo = playheadHelpers->bpm.load(); + const auto tempoSync = *tempoSyncOnOffParam == 1.0f; + if (! tempoSync) + { + delaySmooth.setTargetValue (fs * *delayTimeMsParam * 0.001f); + } + else + { + const auto delayInSeconds = delayTimeRhythmParam->getRhythmTimeSeconds (tempo); + const auto delayInSamples = static_cast (delayInSeconds) * fs; + delaySmooth.setTargetValue (delayInSamples); + } freqSmooth.setTargetValue (*freqParam); const auto delayTypeIndex = (int) *delayTypeParam; @@ -261,3 +294,103 @@ void DelayModule::processAudioBypassed (AudioBuffer& buffer) outputBuffers.getReference (0) = &buffer; } + +bool DelayModule::getCustomComponents (OwnedArray& customComps, chowdsp::HostContextProvider& hcp) +{ + using namespace chowdsp::ParamUtils; + class DelayTimeModeControl : public Slider + { + public: + DelayTimeModeControl (AudioProcessorValueTreeState& vtState, chowdsp::HostContextProvider& hcp) + : vts (vtState), + tempoSyncSelectorAttach (vts, tempoSyncAmountTag, tempoSyncSelector), + delayTimeSlider (*getParameterPointer (vts, delayTimeMsTag), hcp), + delayTimeAttach (vts, delayTimeMsTag, delayTimeSlider), + tempoSyncOnOffAttach ( + *vts.getParameter (tempoSyncTag), + [this] (float newValue) + { updateControlVisibility (newValue == 1.0f); }, + vts.undoManager) + { + addChildComponent (tempoSyncSelector); + addChildComponent (delayTimeSlider); + + const auto* modeChoiceParam = getParameterPointer (vts, tempoSyncAmountTag); + tempoSyncSelector.addItemList (modeChoiceParam->choices, 1); + tempoSyncSelector.setSelectedItemIndex (0); + tempoSyncSelector.setScrollWheelEnabled (true); + hcp.registerParameterComponent (tempoSyncSelector, *modeChoiceParam); + + hcp.registerParameterComponent (delayTimeSlider, delayTimeSlider.getParameter()); + + this->setName (tempoSyncAmountTag + "__" + delayTimeMsTag + "__"); + } + + void colourChanged() override + { + for (auto colourID : { Slider::textBoxOutlineColourId, + Slider::textBoxTextColourId, + Slider::textBoxBackgroundColourId, + Slider::textBoxHighlightColourId, + Slider::thumbColourId }) + { + delayTimeSlider.setColour (colourID, findColour (colourID, false)); + } + + for (auto colourID : { ComboBox::outlineColourId, + ComboBox::textColourId, + ComboBox::arrowColourId }) + { + tempoSyncSelector.setColour (colourID, findColour (Slider::textBoxTextColourId, false)); + } + } + + void updateControlVisibility (bool tempoSyncOn) + { + tempoSyncSelector.setVisible (tempoSyncOn); + delayTimeSlider.setVisible (! tempoSyncOn); + + setName (vts.getParameter (tempoSyncOn ? tempoSyncAmountTag : delayTimeMsTag)->name); + if (auto* parent = getParentComponent()) + parent->repaint(); + } + + void visibilityChanged() override + { + updateControlVisibility (vts.getRawParameterValue (tempoSyncTag)->load() == 1.0f); + } + + void resized() override + { + delayTimeSlider.setSliderStyle (getSliderStyle()); + delayTimeSlider.setTextBoxStyle (getTextBoxPosition(), false, getTextBoxWidth(), getTextBoxHeight()); + + const auto bounds = getLocalBounds(); + tempoSyncSelector.setBounds (bounds.proportionOfWidth (0.15f), + bounds.proportionOfHeight (0.1f), + bounds.proportionOfWidth (0.7f), + bounds.proportionOfHeight (0.25f)); + delayTimeSlider.setBounds (bounds); + } + + private: + using SliderAttachment = AudioProcessorValueTreeState::SliderAttachment; + using BoxAttachment = AudioProcessorValueTreeState::ComboBoxAttachment; + + AudioProcessorValueTreeState& vts; + + ComboBox tempoSyncSelector; + BoxAttachment tempoSyncSelectorAttach; + + ModulatableSlider delayTimeSlider; + SliderAttachment delayTimeAttach; + + ParameterAttachment tempoSyncOnOffAttach; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DelayTimeModeControl) + }; + + customComps.add (std::make_unique (vts, hcp)); + + return true; +} \ No newline at end of file diff --git a/src/processors/other/Delay.h b/src/processors/other/Delay.h index 0804f788..03e0513d 100644 --- a/src/processors/other/Delay.h +++ b/src/processors/other/Delay.h @@ -9,6 +9,7 @@ class DelayModule : public BaseProcessor ProcessorType getProcessorType() const override { return Other; } static ParamLayout createParameterLayout(); + bool getCustomComponents (OwnedArray& customComps, chowdsp::HostContextProvider& hcp) override; void prepare (double sampleRate, int samplesPerBlock) override; void releaseMemory() override; @@ -21,13 +22,16 @@ class DelayModule : public BaseProcessor template void processPingPongDelay (AudioBuffer& buffer, DelayType& delayLine); - chowdsp::FloatParameter* delayTimeMsParam = nullptr; chowdsp::FloatParameter* freqParam = nullptr; chowdsp::FloatParameter* feedbackParam = nullptr; chowdsp::FloatParameter* mixParam = nullptr; std::atomic* delayTypeParam = nullptr; std::atomic* pingPongParam = nullptr; + chowdsp::FloatParameter* delayTimeMsParam = nullptr; + chowdsp::RhythmParameter* delayTimeRhythmParam = nullptr; + std::atomic* tempoSyncOnOffParam = nullptr; + dsp::DryWetMixer dryWetMixer; dsp::DryWetMixer dryWetMixerMono;