From 68311384822920bbce5b3390a6fb6009cfbf7b7f Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 22 Apr 2026 10:42:57 +0300 Subject: [PATCH 01/31] add filterengine and stereofilter classes --- Source/FilterEngine.cpp | 143 ++++++++++++++++++++++++++++++++++++++++ Source/FilterEngine.h | 61 +++++++++++++++++ Source/StereoFilter.cpp | 40 +++++++++++ Source/StereoFilter.h | 25 +++++++ 4 files changed, 269 insertions(+) create mode 100644 Source/FilterEngine.cpp create mode 100644 Source/FilterEngine.h create mode 100644 Source/StereoFilter.cpp create mode 100644 Source/StereoFilter.h diff --git a/Source/FilterEngine.cpp b/Source/FilterEngine.cpp new file mode 100644 index 0000000..6e6039a --- /dev/null +++ b/Source/FilterEngine.cpp @@ -0,0 +1,143 @@ +#include "FilterEngine.h" + +FilterEngine::FilterEngine() +{ +} + +void FilterEngine::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept +{ + m_lowCut.prepareToPlay(sampleRate, samplesPerBlock); + m_highCut.prepareToPlay(sampleRate, samplesPerBlock); + for (auto& lfo : m_lowCutLfos) + lfo.prepareToPlay(static_cast(sampleRate)); + for (auto& lfo : m_highCutLfos) + lfo.prepareToPlay(static_cast(sampleRate)); +} + +void FilterEngine::reset() noexcept +{ + m_lowCut.reset(); + m_highCut.reset(); + for (auto& lfo : m_lowCutLfos) + lfo.reset(); + for (auto& lfo : m_highCutLfos) + lfo.reset(); + m_lowCutBase = -1.0f; + m_lowCutQ = -1.0f; + m_highCutBase = -1.0f; + m_highCutQ = -1.0f; +} + +float FilterEngine::modulatedCutoff(float base, float lfoValue, float modDepth) +{ + return juce::jlimit(Parameters::minFilterFreq, Parameters::maxFilterFreq, base + lfoValue * modDepth); +} + +void FilterEngine::setFilterFreq(float freq, float& current) +{ + if (!juce::approximatelyEqual(freq, current)) + current = freq; +} + +void FilterEngine::setFilterQ(float q, float& current, StereoFilter& filter) +{ + float newQ = std::min(q, Parameters::maxFilterQ); + if (!juce::approximatelyEqual(newQ, current)) { + current = newQ; + filter.setQ(current); + } +} + +void FilterEngine::setFilterModDepth(float depthHz, float& current) +{ + if (!juce::approximatelyEqual(depthHz, current)) + current = depthHz; +} + +void FilterEngine::setFilterLfoRate(float rate, float& current, LFO* lfos) +{ + if (!juce::approximatelyEqual(rate, current)) { + current = rate; + for (int i = 0; i < 2; i++) + lfos[i].setFrequency(current); + } +} + +void FilterEngine::setFilterPhaseOffset(float phase, float& current, LFO* lfos) +{ + if (!juce::approximatelyEqual(phase, current)) { + current = phase; + float normalized = std::fmod(std::fmod(phase, 360.0f) + 360.0f, 360.0f); + lfos[Channel::Right].setOffset(normalized); + } +} + +void FilterEngine::setLowCutFreq(float freq) +{ + setFilterFreq(freq, m_lowCutBase); +} + +void FilterEngine::setLowCutQ(float q) +{ + setFilterQ(q, m_lowCutQ, m_lowCut); +} + +void FilterEngine::setLowCutModDepth(float depth) +{ + setFilterModDepth(depth, m_lowCutModDepth); +} + +void FilterEngine::setLowCutLfoRate(float rate) +{ + setFilterLfoRate(rate, m_lowCutLfoRate, m_lowCutLfos); +} + +void FilterEngine::setLowCutPhaseOffset(float phase) +{ + setFilterPhaseOffset(phase, m_lowCutPhaseOffset, m_lowCutLfos); +} + +void FilterEngine::setHighCutFreq(float freq) +{ + setFilterFreq(freq, m_highCutBase); +} + +void FilterEngine::setHighCutQ(float q) +{ + setFilterQ(q, m_highCutQ, m_highCut); +} + +void FilterEngine::setHighCutModDepth(float depth) +{ + setFilterModDepth(depth, m_highCutModDepth); +} + +void FilterEngine::setHighCutLfoRate(float rate) +{ + setFilterLfoRate(rate, m_highCutLfoRate, m_highCutLfos); +} + +void FilterEngine::setHighCutPhaseOffset(float phase) +{ + setFilterPhaseOffset(phase, m_highCutPhaseOffset, m_highCutLfos); +} + +void FilterEngine::update(const Parameters& params) +{ + setLowCutFreq(params.lowCutFreq()); + setLowCutQ(params.lowCutQ()); + setHighCutFreq(params.highCutFreq()); + setHighCutQ(params.highCutQ()); +} + +void FilterEngine::processSample(const float& inL, const float& inR, float& outL, float& outR) +{ + m_lowCut.setCutoff(m_lowCutBase + m_lowCutLfos[Channel::Left].process() * m_lowCutModDepth, Channel::Left); + m_lowCut.setCutoff(m_lowCutBase + m_lowCutLfos[Channel::Right].process() * m_lowCutModDepth, Channel::Right); + m_highCut.setCutoff(m_highCutBase + m_highCutLfos[Channel::Left].process() * m_highCutModDepth, Channel::Left); + m_highCut.setCutoff(m_highCutBase + m_highCutLfos[Channel::Right].process() * m_highCutModDepth, Channel::Right); + + float lowL, lowR; + m_lowCut.processSample(inL, inR, lowL, lowR); + m_highCut.processSample(lowL, lowR, outL, outR); +} diff --git a/Source/FilterEngine.h b/Source/FilterEngine.h new file mode 100644 index 0000000..76003f8 --- /dev/null +++ b/Source/FilterEngine.h @@ -0,0 +1,61 @@ +#pragma once +#include +#include "Parameters.h" +#include "LFO.h" +#include "StereoFilter.h" +#include "Types.h" + +using FilterType = juce::dsp::StateVariableTPTFilterType; + +class FilterEngine +{ +public: + FilterEngine(); + ~FilterEngine() = default; + + void prepareToPlay(double sampleRate, int samplesPerBlock) noexcept; + void reset() noexcept; + + void update(const Parameters& params); + void processSample(const float& inL, const float& inR, float& outL, float& outR); + +private: + StereoFilter m_lowCut{ FilterType::highpass }; + StereoFilter m_highCut{ FilterType::lowpass }; + + LFO m_lowCutLfos[2] = { {1.0f, 0.0f, LFOShape::Sine}, {1.0f, 0.0f, LFOShape::Sine} }; + LFO m_highCutLfos[2] = { {1.0f, 0.0f, LFOShape::Sine}, {1.0f, 0.0f, LFOShape::Sine} }; + + float m_lowCutBase = -1.0f; + float m_lowCutQ = -1.0f; + float m_lowCutModDepth = 0.0f; + float m_lowCutLfoRate = 0.0f; + float m_lowCutPhaseOffset = 0.0f; + + float m_highCutBase = -1.0f; + float m_highCutQ = -1.0f; + float m_highCutModDepth = 0.0f; + float m_highCutLfoRate = 0.0f; + float m_highCutPhaseOffset = 0.0f; + + void setLowCutFreq(float freq); + void setLowCutQ(float q); + void setLowCutModDepth(float depthHz); + void setLowCutLfoRate(float rate); + void setLowCutPhaseOffset(float phase); + + void setHighCutFreq(float freq); + void setHighCutQ(float q); + void setHighCutModDepth(float depthHz); + void setHighCutLfoRate(float rate); + void setHighCutPhaseOffset(float phase); + + float modulatedCutoff(float base, float lfoValue, float modDepth); + void setFilterFreq(float freq, float& current); + void setFilterQ(float q, float& current, StereoFilter& filter); + void setFilterModDepth(float depthHz, float& current); + void setFilterLfoRate(float rate, float& current, LFO* lfos); + void setFilterPhaseOffset(float phase, float& current, LFO* lfos); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FilterEngine) +}; diff --git a/Source/StereoFilter.cpp b/Source/StereoFilter.cpp new file mode 100644 index 0000000..cb4c959 --- /dev/null +++ b/Source/StereoFilter.cpp @@ -0,0 +1,40 @@ +#include "StereoFilter.h" + +StereoFilter::StereoFilter(FilterType type) +{ + for (auto& f : m_filters) + f.setType(type); +} + +void StereoFilter::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept +{ + juce::dsp::ProcessSpec spec; + spec.sampleRate = sampleRate; + spec.maximumBlockSize = juce::uint32(samplesPerBlock); + spec.numChannels = 1; + for (auto& f : m_filters) + f.prepare(spec); +} + +void StereoFilter::reset() noexcept +{ + for (auto& f : m_filters) + f.reset(); +} + +void StereoFilter::setCutoff(float freq, Channel channel) +{ + m_filters[channel].setCutoffFrequency(freq); +} + +void StereoFilter::setQ(float q) +{ + for (auto& f : m_filters) + f.setResonance(q); +} + +void StereoFilter::processSample(const float& inL, const float& inR, float& outL, float& outR) +{ + outL = m_filters[Channel::Left].processSample(0, inL); + outR = m_filters[Channel::Right].processSample(0, inR); +} diff --git a/Source/StereoFilter.h b/Source/StereoFilter.h new file mode 100644 index 0000000..2fa2d5a --- /dev/null +++ b/Source/StereoFilter.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include "Types.h" + +using Filter = juce::dsp::StateVariableTPTFilter; +using FilterType = juce::dsp::StateVariableTPTFilterType; + +class StereoFilter +{ +public: + StereoFilter(FilterType type = FilterType::lowpass); + ~StereoFilter() = default; + + void prepareToPlay(double sampleRate, int samplesPerBlock) noexcept; + void reset() noexcept; + + void processSample(const float& inL, const float& inR, float& outL, float& outR); + void setCutoff(float freq, Channel channel); + void setQ(float q); + +private: + Filter m_filters[2]; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(StereoFilter) +}; From ab4df7a24dabe253e47592b5147eec4ff50ae6f5 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 22 Apr 2026 10:43:52 +0300 Subject: [PATCH 02/31] update cmakelists, wire filterengine to delayengine, create types.h for channel enum --- CMakeLists.txt | 2 ++ Source/DelayEngine.cpp | 27 +++++++++++++++------------ Source/DelayEngine.h | 2 ++ Source/Parameters.h | 2 ++ Source/StereoDelay.h | 3 +-- Source/Types.h | 3 +++ 6 files changed, 25 insertions(+), 14 deletions(-) create mode 100644 Source/Types.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e81347..1f5d0e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,9 +67,11 @@ get_property(project_targets DIRECTORY "${PROJECT_SOURCE_DIR}" PROPERTY BUILDSYS set_target_properties(${project_targets} PROPERTIES CXX_EXTENSIONS OFF) target_sources(viive PRIVATE + Source/FilterEngine.cpp Source/Tempo.cpp Source/SyncButton.cpp Source/StereoDelay.cpp + Source/StereoFilter.cpp Source/ChorusEngine.cpp Source/LFO.cpp Source/Chorus.cpp diff --git a/Source/DelayEngine.cpp b/Source/DelayEngine.cpp index ac537ff..0a2eb82 100644 --- a/Source/DelayEngine.cpp +++ b/Source/DelayEngine.cpp @@ -1,9 +1,9 @@ #include "DelayEngine.h" #include "DSP.h" +#include "Types.h" + DelayEngine::DelayEngine() { - m_lowCutFilter.setType(juce::dsp::StateVariableTPTFilterType::highpass); - m_highCutFilter.setType(juce::dsp::StateVariableTPTFilterType::lowpass); m_feedbackHighpass.setType(juce::dsp::StateVariableTPTFilterType::highpass); m_feedbackCompressor.setAttack(.1f); m_feedbackCompressor.setRelease(50.f); @@ -21,6 +21,7 @@ void DelayEngine::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept m_feedbackCompressor.prepare(spec); m_lowCutFilter.prepare(spec); m_highCutFilter.prepare(spec); + m_filterEngine.prepareToPlay(sampleRate, samplesPerBlock); m_feedbackHighpass.prepare(spec); m_chorusEngine.prepareToPlay(sampleRate, samplesPerBlock); m_stereoDelay.prepareToPlay(sampleRate, samplesPerBlock); @@ -34,6 +35,7 @@ void DelayEngine::reset() noexcept m_lowCutFilter.reset(); m_highCutFilter.reset(); m_chorusEngine.reset(); + m_filterEngine.reset(); m_feedbackHighpass.reset(); m_feedbackHighpass.setCutoffFrequency(60.0f); // just to remove rumble from fb loop m_feedbackHighpass.setResonance(Parameters::defaultFilterQ); @@ -98,14 +100,13 @@ void DelayEngine::setDelayTimes(const float targetL, const float targetR) { void DelayEngine::update(const Parameters& params) { - setLowCut(params); - setHighCut(params); setMixLevel(params.mix()); setGainLevel(params.gain()); setFeedbackLevel(params.feedback()); setWidthLevel(params.stereo()); setOffsetMs(params.offset()); setDelayTimes(params.delayTimeL(), params.delayTimeR()); + m_filterEngine.update(params); } void DelayEngine::processSample(const float& inL, const float& inR, float& outL, float& outR, const Parameters& params) @@ -114,7 +115,7 @@ void DelayEngine::processSample(const float& inL, const float& inR, float& outL, float dryR = inR; float mono = (dryL + dryR) * .5f; - float delayInL = 0.0f, delayInR = 0.0f, popL = 0.0f, popR = 0.0f, wetL = 0.0f, wetR = 0.0f; + float delayInL = 0.0f, delayInR = 0.0f, wetL = 0.0f, wetR = 0.0f; switch (m_delayMode) { case DelayMode::PingPongLR: delayInL = mono + m_feedbackR; @@ -137,17 +138,19 @@ void DelayEngine::processSample(const float& inL, const float& inR, float& outL, delayInR = dryR; } - m_stereoDelay.processSample(delayInL, popL, Channel::Left); - m_stereoDelay.processSample(delayInR, popR, Channel::Right); + m_stereoDelay.processSample(delayInL, wetL, Channel::Left); + m_stereoDelay.processSample(delayInR, wetR, Channel::Right); - m_chorusEngine.processSample(popL, popR, wetL, wetR, params); + m_chorusEngine.processSample(wetL, wetR, wetL, wetR, params); // sculpting filters, first left and then right channel - wetL = m_lowCutFilter.processSample(0, wetL); - wetL = m_highCutFilter.processSample(0, wetL); + // wetL = m_lowCutFilter.processSample(0, wetL); + // wetL = m_highCutFilter.processSample(0, wetL); + + // wetR = m_lowCutFilter.processSample(1, wetR); + // wetR = m_highCutFilter.processSample(1, wetR); - wetR = m_lowCutFilter.processSample(1, wetR); - wetR = m_highCutFilter.processSample(1, wetR); + m_filterEngine.processSample(wetL, wetR, wetL, wetR); // this removes rumble generated by low cut filter on higher q values wetL = m_feedbackHighpass.processSample(0, wetL); diff --git a/Source/DelayEngine.h b/Source/DelayEngine.h index f7f4004..4d993ac 100644 --- a/Source/DelayEngine.h +++ b/Source/DelayEngine.h @@ -3,6 +3,7 @@ #include "Parameters.h" #include "ChorusEngine.h" #include "StereoDelay.h" +#include "FilterEngine.h" using Filter = juce::dsp::StateVariableTPTFilter; @@ -35,6 +36,7 @@ class DelayEngine //juce::dsp::DelayLine m_delayLine; StereoDelay m_stereoDelay; DelayMode m_delayMode = DelayMode::Stereo; + FilterEngine m_filterEngine; Filter m_lowCutFilter; Filter m_highCutFilter; Filter m_feedbackHighpass; diff --git a/Source/Parameters.h b/Source/Parameters.h index b0b40e1..c48295a 100644 --- a/Source/Parameters.h +++ b/Source/Parameters.h @@ -30,6 +30,8 @@ class Parameters { static constexpr float minDelayTime = 5.0f; static constexpr float maxDelayTime = 5000.0f; static constexpr float defaultDelayTime = 100.0f; + static constexpr float minFilterFreq = 20.0f; + static constexpr float maxFilterFreq = 20000.0f; static constexpr float maxFilterQ = 2.0f; static constexpr float minFilterQ = 0.1f; static constexpr float defaultFilterQ = 0.707f; diff --git a/Source/StereoDelay.h b/Source/StereoDelay.h index e5cb725..a59e0f7 100644 --- a/Source/StereoDelay.h +++ b/Source/StereoDelay.h @@ -1,7 +1,6 @@ #pragma once #include - -enum Channel { Left = 0, Right = 1 }; +#include "Types.h" class StereoDelay { public: diff --git a/Source/Types.h b/Source/Types.h new file mode 100644 index 0000000..4bc447c --- /dev/null +++ b/Source/Types.h @@ -0,0 +1,3 @@ +#pragma once + +enum Channel { Left = 0, Right = 1 }; From 321b8ab7c6d57b9418a262aa36be7b0114ec2384 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 22 Apr 2026 13:38:49 +0300 Subject: [PATCH 03/31] add parameters for filter modulation --- Source/Parameters.cpp | 84 +++++++++++++++++++++++++++++++++++++++++++ Source/Parameters.h | 30 ++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/Source/Parameters.cpp b/Source/Parameters.cpp index 3f4c9e9..c976091 100644 --- a/Source/Parameters.cpp +++ b/Source/Parameters.cpp @@ -112,8 +112,14 @@ Parameters::Parameters(juce::AudioProcessorValueTreeState& apvts) castParameter(apvts, chorusIntensityParamID, m_chorusIntensityParam); castParameter(apvts, lowCutFreqParamID, m_lowCutFreqParam); castParameter(apvts, lowCutQParamID, m_lowCutQParam); + castParameter(apvts, lowCutModRateParamID, m_lowCutModRateParam); + castParameter(apvts, lowCutModDepthParamID, m_lowCutModDepthParam); + castParameter(apvts, lowCutModPhaseParamID, m_lowCutModPhaseParam); castParameter(apvts, highCutFreqParamID, m_highCutFreqParam); castParameter(apvts, highCutQParamID, m_highCutQParam); + castParameter(apvts, highCutModRateParamID, m_highCutModRateParam); + castParameter(apvts, highCutModDepthParamID, m_highCutModDepthParam); + castParameter(apvts, highCutModPhaseParamID, m_highCutModPhaseParam); castParameter(apvts, chorusModRateParamID, m_chorusModRateParam); castParameter(apvts, chorusModDepthParamID, m_chorusModDepthParam); } @@ -223,6 +229,30 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL juce::NormalisableRange(minFilterQ, maxFilterQ, 0.01f), defaultFilterQ )); + layout.add(std::make_unique( + lowCutModRateParamID.getParamID(), + "Low Cut Mod Rate", + juce::NormalisableRange(0.01f, 2.0f, 0.01f), + 0.0f, + juce::AudioParameterFloatAttributes() + .withStringFromValueFunction(stringFromHertz) + .withValueFromStringFunction(hzFromString) + )); + layout.add(std::make_unique( + lowCutModDepthParamID.getParamID(), + "Low Cut Mod Depth", + juce::NormalisableRange(0.0f, 600.0f, 0.1f), + 0.0f, + juce::AudioParameterFloatAttributes() + .withStringFromValueFunction(stringFromHertz) + .withValueFromStringFunction(hzFromString) + )); + layout.add(std::make_unique( + lowCutModPhaseParamID.getParamID(), + "Low Cut Mod Phase", + juce::NormalisableRange(0.0f, 360.0f, 0.1f), + 0.0f + )); layout.add(std::make_unique( highCutFreqParamID.getParamID(), "High Cut Frequency", @@ -238,6 +268,30 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL juce::NormalisableRange(minFilterQ, maxFilterQ, 0.01f), defaultFilterQ )); + layout.add(std::make_unique( + highCutModRateParamID.getParamID(), + "High Cut Mod Rate", + juce::NormalisableRange(0.01f, 2.0f, 0.01f), + 0.0f, + juce::AudioParameterFloatAttributes() + .withStringFromValueFunction(stringFromHertz) + .withValueFromStringFunction(hzFromString) + )); + layout.add(std::make_unique( + highCutModDepthParamID.getParamID(), + "High Cut Mod Depth", + juce::NormalisableRange(0.0f, 600.0f, 0.1f), + 0.0f, + juce::AudioParameterFloatAttributes() + .withStringFromValueFunction(stringFromHertz) + .withValueFromStringFunction(hzFromString) + )); + layout.add(std::make_unique( + highCutModPhaseParamID.getParamID(), + "High Cut Mod Phase", + juce::NormalisableRange(0.0f, 360.0f, 0.1f), + 0.0f + )); layout.add(std::make_unique( chorusIntensityParamID.getParamID(), "Chorus Intensity", @@ -270,8 +324,14 @@ void Parameters::prepareToPlay(double sampleRate) noexcept m_chorusIntensitySmoother.reset(sampleRate, duration); m_lowCutFreqSmoother.reset(sampleRate, duration); m_lowCutQSmoother.reset(sampleRate, duration); + m_lowCutModRateSmoother.reset(sampleRate, duration); + m_lowCutModDepthSmoother.reset(sampleRate, duration); + m_lowCutModPhaseSmoother.reset(sampleRate, duration); m_highCutFreqSmoother.reset(sampleRate, duration); m_highCutQSmoother.reset(sampleRate, duration); + m_highCutModRateSmoother.reset(sampleRate, duration); + m_highCutModDepthSmoother.reset(sampleRate, duration); + m_highCutModPhaseSmoother.reset(sampleRate, duration); m_chorusModRateSmoother.reset(sampleRate, duration); m_chorusModDepthSmoother.reset(sampleRate, duration); } @@ -288,8 +348,14 @@ void Parameters::reset() noexcept m_chorusIntensity = 0.0f; m_lowCutFreq = 20.0f; m_lowCutQ = defaultFilterQ; + m_lowCutModRate = 0.0f; + m_lowCutModDepth = 0.0f; + m_lowCutModPhase = 0.0f; m_highCutFreq = 20000.0f; m_highCutQ = defaultFilterQ; + m_highCutModRate = 0.0f; + m_highCutModDepth = 0.0f; + m_highCutModPhase = 0.0f; m_chorusModRate = defaultChorusModRate; m_chorusModDepth = defaultChorusModDepth; m_gainSmoother.setCurrentAndTargetValue(juce::Decibels::decibelsToGain(m_gainParam->get())); @@ -299,8 +365,14 @@ void Parameters::reset() noexcept m_chorusIntensitySmoother.setCurrentAndTargetValue(m_chorusIntensityParam->get()); m_lowCutFreqSmoother.setCurrentAndTargetValue(m_lowCutFreqParam->get()); m_lowCutQSmoother.setCurrentAndTargetValue(m_lowCutQParam->get()); + m_lowCutModRateSmoother.setCurrentAndTargetValue(m_lowCutModRateParam->get()); + m_lowCutModDepthSmoother.setCurrentAndTargetValue(m_lowCutModDepthParam->get()); + m_lowCutModPhaseSmoother.setCurrentAndTargetValue(m_lowCutModPhaseParam->get()); m_highCutFreqSmoother.setCurrentAndTargetValue(m_highCutFreqParam->get()); m_highCutQSmoother.setCurrentAndTargetValue(m_highCutQParam->get()); + m_highCutModRateSmoother.setCurrentAndTargetValue(m_highCutModRateParam->get()); + m_highCutModDepthSmoother.setCurrentAndTargetValue(m_highCutModDepthParam->get()); + m_highCutModPhaseSmoother.setCurrentAndTargetValue(m_highCutModPhaseParam->get()); m_chorusModRateSmoother.setCurrentAndTargetValue(m_chorusModRateParam->get()); m_chorusModDepthSmoother.setCurrentAndTargetValue(m_chorusModDepthParam->get()); } @@ -315,8 +387,14 @@ void Parameters::update(const Tempo& tempo) noexcept m_chorusIntensitySmoother.setTargetValue(m_chorusIntensityParam->get()); m_lowCutFreqSmoother.setTargetValue(m_lowCutFreqParam->get()); m_lowCutQSmoother.setTargetValue(m_lowCutQParam->get()); + m_lowCutModRateSmoother.setTargetValue(m_lowCutModRateParam->get()); + m_lowCutModDepthSmoother.setTargetValue(m_lowCutModDepthParam->get()); + m_lowCutModPhaseSmoother.setTargetValue(m_lowCutModPhaseParam->get()); m_highCutFreqSmoother.setTargetValue(m_highCutFreqParam->get()); m_highCutQSmoother.setTargetValue(m_highCutQParam->get()); + m_highCutModRateSmoother.setTargetValue(m_highCutModRateParam->get()); + m_highCutModDepthSmoother.setTargetValue(m_highCutModDepthParam->get()); + m_highCutModPhaseSmoother.setTargetValue(m_highCutModPhaseParam->get()); m_chorusModRateSmoother.setTargetValue(m_chorusModRateParam->get()); m_chorusModDepthSmoother.setTargetValue(m_chorusModDepthParam->get()); m_tempoSyncL = m_tempoSyncLParam->get(); @@ -346,8 +424,14 @@ void Parameters::smoothen() noexcept m_chorusIntensity = m_chorusIntensitySmoother.getNextValue(); m_lowCutFreq = m_lowCutFreqSmoother.getNextValue(); m_lowCutQ = m_lowCutQSmoother.getNextValue(); + m_lowCutModRate = m_lowCutModRateSmoother.getNextValue(); + m_lowCutModDepth = m_lowCutModDepthSmoother.getNextValue(); + m_lowCutModPhase = m_lowCutModPhaseSmoother.getNextValue(); m_highCutFreq = m_highCutFreqSmoother.getNextValue(); m_highCutQ = m_highCutQSmoother.getNextValue(); + m_highCutModRate = m_highCutModRateSmoother.getNextValue(); + m_highCutModDepth = m_highCutModDepthSmoother.getNextValue(); + m_highCutModPhase = m_highCutModPhaseSmoother.getNextValue(); m_chorusModRate = m_chorusModRateSmoother.getNextValue(); m_chorusModDepth = m_chorusModDepthSmoother.getNextValue(); } \ No newline at end of file diff --git a/Source/Parameters.h b/Source/Parameters.h index c48295a..905c20c 100644 --- a/Source/Parameters.h +++ b/Source/Parameters.h @@ -16,8 +16,14 @@ const juce::ParameterID stereoParamID{ "stereo", 1 }; const juce::ParameterID offsetParamID{ "offset", 1 }; const juce::ParameterID lowCutFreqParamID{ "lowCutFreq", 1 }; const juce::ParameterID lowCutQParamID{ "lowCutQ", 1 }; +const juce::ParameterID lowCutModRateParamID{ "lowCutModRate", 1 }; +const juce::ParameterID lowCutModDepthParamID{ "lowCutModDepth", 1 }; +const juce::ParameterID lowCutModPhaseParamID{ "lowCutModPhase", 1 }; const juce::ParameterID highCutFreqParamID{ "highCutFreq", 1 }; const juce::ParameterID highCutQParamID{ "highCutQ", 1 }; +const juce::ParameterID highCutModRateParamID{ "highCutModRate", 1 }; +const juce::ParameterID highCutModDepthParamID{ "highCutModDepth", 1 }; +const juce::ParameterID highCutModPhaseParamID{ "highCutModPhase", 1 }; const juce::ParameterID chorusIntensityParamID{ "chorusIntensity", 1 }; const juce::ParameterID chorusModRateParamID{ "chorusModRate", 1 }; const juce::ParameterID chorusModDepthParamID{ "chorusModDepth", 1 }; @@ -60,8 +66,14 @@ class Parameters { float offset() const { return m_offset; } float lowCutFreq() const { return m_lowCutFreq; } float lowCutQ() const { return m_lowCutQ; } + float lowCutModRate() const { return m_lowCutModRate; } + float lowCutModDepth() const { return m_lowCutModDepth; } + float lowCutModPhase() const { return m_lowCutModPhase; } float highCutFreq() const { return m_highCutFreq; } float highCutQ() const { return m_highCutQ; } + float highCutModRate() const { return m_highCutModRate; } + float highCutModDepth() const { return m_highCutModDepth; } + float highCutModPhase() const { return m_highCutModPhase; } float chorusIntensity() const { return m_chorusIntensity; } float chorusModRate() const { return m_chorusModRate; } float chorusModDepth() const { return m_chorusModDepth; } @@ -81,8 +93,14 @@ class Parameters { juce::AudioParameterFloat* m_offsetParam; juce::AudioParameterFloat* m_lowCutFreqParam; juce::AudioParameterFloat* m_lowCutQParam; + juce::AudioParameterFloat* m_lowCutModRateParam; + juce::AudioParameterFloat* m_lowCutModDepthParam; + juce::AudioParameterFloat* m_lowCutModPhaseParam; juce::AudioParameterFloat* m_highCutFreqParam; juce::AudioParameterFloat* m_highCutQParam; + juce::AudioParameterFloat* m_highCutModRateParam; + juce::AudioParameterFloat* m_highCutModDepthParam; + juce::AudioParameterFloat* m_highCutModPhaseParam; juce::AudioParameterFloat* m_chorusIntensityParam; juce::AudioParameterFloat* m_chorusModRateParam; juce::AudioParameterFloat* m_chorusModDepthParam; @@ -94,8 +112,14 @@ class Parameters { juce::LinearSmoothedValue m_offsetSmoother; juce::LinearSmoothedValue m_lowCutFreqSmoother; juce::LinearSmoothedValue m_lowCutQSmoother; + juce::LinearSmoothedValue m_lowCutModRateSmoother; + juce::LinearSmoothedValue m_lowCutModDepthSmoother; + juce::LinearSmoothedValue m_lowCutModPhaseSmoother; juce::LinearSmoothedValue m_highCutFreqSmoother; juce::LinearSmoothedValue m_highCutQSmoother; + juce::LinearSmoothedValue m_highCutModRateSmoother; + juce::LinearSmoothedValue m_highCutModDepthSmoother; + juce::LinearSmoothedValue m_highCutModPhaseSmoother; juce::LinearSmoothedValue m_chorusIntensitySmoother; juce::LinearSmoothedValue m_chorusModRateSmoother; juce::LinearSmoothedValue m_chorusModDepthSmoother; @@ -119,8 +143,14 @@ class Parameters { float m_offset = 0.0f; float m_lowCutFreq = 20.0f; float m_lowCutQ = defaultFilterQ; + float m_lowCutModRate = 0.0f; + float m_lowCutModDepth = 0.0f; + float m_lowCutModPhase = 0.0f; float m_highCutFreq = 20000.0f; float m_highCutQ = defaultFilterQ; + float m_highCutModRate = 0.0f; + float m_highCutModDepth = 0.0f; + float m_highCutModPhase = 0.0f; float m_chorusIntensity = 0.0f; float m_chorusModRate = defaultChorusModRate; float m_chorusModDepth = defaultChorusModDepth; From 03f67d19345ce98cd63861f2d744292ae15103b0 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 22 Apr 2026 13:39:36 +0300 Subject: [PATCH 04/31] add mod param update to filterengine, smoothen cutoff with onepolelp, rename params --- Source/FilterEngine.cpp | 76 +++++++++++++---------------------------- Source/FilterEngine.h | 28 +++++++-------- 2 files changed, 35 insertions(+), 69 deletions(-) diff --git a/Source/FilterEngine.cpp b/Source/FilterEngine.cpp index 6e6039a..37cc2da 100644 --- a/Source/FilterEngine.cpp +++ b/Source/FilterEngine.cpp @@ -12,6 +12,7 @@ void FilterEngine::prepareToPlay(double sampleRate, int samplesPerBlock) noexcep lfo.prepareToPlay(static_cast(sampleRate)); for (auto& lfo : m_highCutLfos) lfo.prepareToPlay(static_cast(sampleRate)); + m_cutoffSmoothCoeff = onePoleLowpassCoeff(5.0f, static_cast(sampleRate)); } void FilterEngine::reset() noexcept @@ -26,6 +27,8 @@ void FilterEngine::reset() noexcept m_lowCutQ = -1.0f; m_highCutBase = -1.0f; m_highCutQ = -1.0f; + m_lowCutSmoothed[0] = m_lowCutSmoothed[1] = Parameters::minFilterFreq; + m_highCutSmoothed[0] = m_highCutSmoothed[1] = Parameters::maxFilterFreq; } float FilterEngine::modulatedCutoff(float base, float lfoValue, float modDepth) @@ -54,7 +57,7 @@ void FilterEngine::setFilterModDepth(float depthHz, float& current) current = depthHz; } -void FilterEngine::setFilterLfoRate(float rate, float& current, LFO* lfos) +void FilterEngine::setFilterModRate(float rate, float& current, LFO* lfos) { if (!juce::approximatelyEqual(rate, current)) { current = rate; @@ -63,7 +66,7 @@ void FilterEngine::setFilterLfoRate(float rate, float& current, LFO* lfos) } } -void FilterEngine::setFilterPhaseOffset(float phase, float& current, LFO* lfos) +void FilterEngine::setFilterModPhase(float phase, float& current, LFO* lfos) { if (!juce::approximatelyEqual(phase, current)) { current = phase; @@ -72,70 +75,37 @@ void FilterEngine::setFilterPhaseOffset(float phase, float& current, LFO* lfos) } } -void FilterEngine::setLowCutFreq(float freq) +void FilterEngine::setLowCutFilter(const Parameters& params) { - setFilterFreq(freq, m_lowCutBase); + setFilterFreq(params.lowCutFreq(), m_lowCutBase); + setFilterQ(params.lowCutQ(), m_lowCutQ, m_lowCut); + setFilterModRate(params.lowCutModRate(), m_lowCutModRate, m_lowCutLfos); + setFilterModDepth(params.lowCutModDepth(), m_lowCutModDepth); + setFilterModPhase(params.lowCutModPhase(), m_lowCutModPhase, m_lowCutLfos ); } -void FilterEngine::setLowCutQ(float q) +void FilterEngine::setHighCutFilter(const Parameters& params) { - setFilterQ(q, m_lowCutQ, m_lowCut); + setFilterFreq(params.highCutFreq(), m_highCutBase); + setFilterQ(params.highCutQ(), m_highCutQ, m_highCut); + setFilterModRate(params.highCutModRate(), m_highCutModRate, m_highCutLfos); + setFilterModDepth(params.highCutModDepth(), m_highCutModDepth); + setFilterModPhase(params.highCutModPhase(), m_highCutModPhase, m_highCutLfos); } -void FilterEngine::setLowCutModDepth(float depth) -{ - setFilterModDepth(depth, m_lowCutModDepth); -} - -void FilterEngine::setLowCutLfoRate(float rate) -{ - setFilterLfoRate(rate, m_lowCutLfoRate, m_lowCutLfos); -} - -void FilterEngine::setLowCutPhaseOffset(float phase) -{ - setFilterPhaseOffset(phase, m_lowCutPhaseOffset, m_lowCutLfos); -} - -void FilterEngine::setHighCutFreq(float freq) -{ - setFilterFreq(freq, m_highCutBase); -} - -void FilterEngine::setHighCutQ(float q) -{ - setFilterQ(q, m_highCutQ, m_highCut); -} - -void FilterEngine::setHighCutModDepth(float depth) -{ - setFilterModDepth(depth, m_highCutModDepth); -} - -void FilterEngine::setHighCutLfoRate(float rate) -{ - setFilterLfoRate(rate, m_highCutLfoRate, m_highCutLfos); -} - -void FilterEngine::setHighCutPhaseOffset(float phase) -{ - setFilterPhaseOffset(phase, m_highCutPhaseOffset, m_highCutLfos); -} void FilterEngine::update(const Parameters& params) { - setLowCutFreq(params.lowCutFreq()); - setLowCutQ(params.lowCutQ()); - setHighCutFreq(params.highCutFreq()); - setHighCutQ(params.highCutQ()); + setLowCutFilter(params); + setHighCutFilter(params); } void FilterEngine::processSample(const float& inL, const float& inR, float& outL, float& outR) { - m_lowCut.setCutoff(m_lowCutBase + m_lowCutLfos[Channel::Left].process() * m_lowCutModDepth, Channel::Left); - m_lowCut.setCutoff(m_lowCutBase + m_lowCutLfos[Channel::Right].process() * m_lowCutModDepth, Channel::Right); - m_highCut.setCutoff(m_highCutBase + m_highCutLfos[Channel::Left].process() * m_highCutModDepth, Channel::Left); - m_highCut.setCutoff(m_highCutBase + m_highCutLfos[Channel::Right].process() * m_highCutModDepth, Channel::Right); + m_lowCut.setCutoff(onePoleLowpass(modulatedCutoff(m_lowCutBase, m_lowCutLfos[Channel::Left].process(), m_lowCutModDepth), m_lowCutSmoothed[Channel::Left], m_cutoffSmoothCoeff), Channel::Left); + m_lowCut.setCutoff(onePoleLowpass(modulatedCutoff(m_lowCutBase, m_lowCutLfos[Channel::Right].process(), m_lowCutModDepth), m_lowCutSmoothed[Channel::Right], m_cutoffSmoothCoeff), Channel::Right); + m_highCut.setCutoff(onePoleLowpass(modulatedCutoff(m_highCutBase, m_highCutLfos[Channel::Left].process(), m_highCutModDepth), m_highCutSmoothed[Channel::Left], m_cutoffSmoothCoeff), Channel::Left); + m_highCut.setCutoff(onePoleLowpass(modulatedCutoff(m_highCutBase, m_highCutLfos[Channel::Right].process(), m_highCutModDepth), m_highCutSmoothed[Channel::Right], m_cutoffSmoothCoeff), Channel::Right); float lowL, lowR; m_lowCut.processSample(inL, inR, lowL, lowR); diff --git a/Source/FilterEngine.h b/Source/FilterEngine.h index 76003f8..f897461 100644 --- a/Source/FilterEngine.h +++ b/Source/FilterEngine.h @@ -4,6 +4,7 @@ #include "LFO.h" #include "StereoFilter.h" #include "Types.h" +#include "DSP.h" using FilterType = juce::dsp::StateVariableTPTFilterType; @@ -29,33 +30,28 @@ class FilterEngine float m_lowCutBase = -1.0f; float m_lowCutQ = -1.0f; float m_lowCutModDepth = 0.0f; - float m_lowCutLfoRate = 0.0f; - float m_lowCutPhaseOffset = 0.0f; + float m_lowCutModRate = 0.0f; + float m_lowCutModPhase = 0.0f; float m_highCutBase = -1.0f; float m_highCutQ = -1.0f; float m_highCutModDepth = 0.0f; - float m_highCutLfoRate = 0.0f; - float m_highCutPhaseOffset = 0.0f; + float m_highCutModRate = 0.0f; + float m_highCutModPhase = 0.0f; - void setLowCutFreq(float freq); - void setLowCutQ(float q); - void setLowCutModDepth(float depthHz); - void setLowCutLfoRate(float rate); - void setLowCutPhaseOffset(float phase); + float m_lowCutSmoothed[2] = { Parameters::minFilterFreq, Parameters::minFilterFreq }; + float m_highCutSmoothed[2] = { Parameters::maxFilterFreq, Parameters::maxFilterFreq }; + float m_cutoffSmoothCoeff = 0.0f; - void setHighCutFreq(float freq); - void setHighCutQ(float q); - void setHighCutModDepth(float depthHz); - void setHighCutLfoRate(float rate); - void setHighCutPhaseOffset(float phase); + void setLowCutFilter(const Parameters& params); + void setHighCutFilter(const Parameters& params); float modulatedCutoff(float base, float lfoValue, float modDepth); void setFilterFreq(float freq, float& current); void setFilterQ(float q, float& current, StereoFilter& filter); void setFilterModDepth(float depthHz, float& current); - void setFilterLfoRate(float rate, float& current, LFO* lfos); - void setFilterPhaseOffset(float phase, float& current, LFO* lfos); + void setFilterModRate(float rate, float& current, LFO* lfos); + void setFilterModPhase(float phase, float& current, LFO* lfos); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FilterEngine) }; From 0cba6393d2dd304d6e22d8d1b1cd855f2568ca0b Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 22 Apr 2026 14:13:41 +0300 Subject: [PATCH 05/31] switch to octave based filter modulation, fix lfo phase bug --- Source/FilterEngine.cpp | 10 +++++----- Source/FilterEngine.h | 2 +- Source/Parameters.cpp | 15 +++++---------- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/Source/FilterEngine.cpp b/Source/FilterEngine.cpp index 37cc2da..e4a56f4 100644 --- a/Source/FilterEngine.cpp +++ b/Source/FilterEngine.cpp @@ -33,7 +33,7 @@ void FilterEngine::reset() noexcept float FilterEngine::modulatedCutoff(float base, float lfoValue, float modDepth) { - return juce::jlimit(Parameters::minFilterFreq, Parameters::maxFilterFreq, base + lfoValue * modDepth); + return juce::jlimit(Parameters::minFilterFreq, Parameters::maxFilterFreq, base * std::pow(2.0f, lfoValue * modDepth)); } void FilterEngine::setFilterFreq(float freq, float& current) @@ -51,10 +51,10 @@ void FilterEngine::setFilterQ(float q, float& current, StereoFilter& filter) } } -void FilterEngine::setFilterModDepth(float depthHz, float& current) +void FilterEngine::setFilterModDepth(float depth, float& current) { - if (!juce::approximatelyEqual(depthHz, current)) - current = depthHz; + if (!juce::approximatelyEqual(depth, current)) + current = depth; } void FilterEngine::setFilterModRate(float rate, float& current, LFO* lfos) @@ -70,7 +70,7 @@ void FilterEngine::setFilterModPhase(float phase, float& current, LFO* lfos) { if (!juce::approximatelyEqual(phase, current)) { current = phase; - float normalized = std::fmod(std::fmod(phase, 360.0f) + 360.0f, 360.0f); + float normalized = std::fmod(std::fmod(phase, 360.0f) + 360.0f, 360.0f) / 360.0f; lfos[Channel::Right].setOffset(normalized); } } diff --git a/Source/FilterEngine.h b/Source/FilterEngine.h index f897461..09a1c72 100644 --- a/Source/FilterEngine.h +++ b/Source/FilterEngine.h @@ -49,7 +49,7 @@ class FilterEngine float modulatedCutoff(float base, float lfoValue, float modDepth); void setFilterFreq(float freq, float& current); void setFilterQ(float q, float& current, StereoFilter& filter); - void setFilterModDepth(float depthHz, float& current); + void setFilterModDepth(float depth, float& current); void setFilterModRate(float rate, float& current, LFO* lfos); void setFilterModPhase(float phase, float& current, LFO* lfos); diff --git a/Source/Parameters.cpp b/Source/Parameters.cpp index c976091..3638537 100644 --- a/Source/Parameters.cpp +++ b/Source/Parameters.cpp @@ -47,6 +47,7 @@ static juce::String stringFromHertz(float value, int) } } + static float millisecondsFromString(const juce::String& text) { float value = text.getFloatValue(); @@ -241,11 +242,8 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL layout.add(std::make_unique( lowCutModDepthParamID.getParamID(), "Low Cut Mod Depth", - juce::NormalisableRange(0.0f, 600.0f, 0.1f), - 0.0f, - juce::AudioParameterFloatAttributes() - .withStringFromValueFunction(stringFromHertz) - .withValueFromStringFunction(hzFromString) + juce::NormalisableRange(0.0f, 2.0f, 0.01f), + 0.0f )); layout.add(std::make_unique( lowCutModPhaseParamID.getParamID(), @@ -280,11 +278,8 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL layout.add(std::make_unique( highCutModDepthParamID.getParamID(), "High Cut Mod Depth", - juce::NormalisableRange(0.0f, 600.0f, 0.1f), - 0.0f, - juce::AudioParameterFloatAttributes() - .withStringFromValueFunction(stringFromHertz) - .withValueFromStringFunction(hzFromString) + juce::NormalisableRange(0.0f, 2.0f, 0.01f), + 0.0f )); layout.add(std::make_unique( highCutModPhaseParamID.getParamID(), From 59f355f8295162d78de7f18656f0a8c956a8cfda Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 22 Apr 2026 15:13:04 +0300 Subject: [PATCH 06/31] add lfoscope component + buffer for debug purposes, add it to editor only present in debug build --- CMakeLists.txt | 3 +- Source/DelayEngine.h | 7 ++++ Source/FilterEngine.cpp | 20 ++++++++--- Source/FilterEngine.h | 16 +++++++-- Source/LFOScope.cpp | 71 ++++++++++++++++++++++++++++++++++++++++ Source/LFOScope.h | 23 +++++++++++++ Source/LFOScopeBuffer.h | 19 +++++++++++ Source/PluginEditor.cpp | 20 +++++++++-- Source/PluginEditor.h | 7 ++++ Source/PluginProcessor.h | 7 ++++ 10 files changed, 183 insertions(+), 10 deletions(-) create mode 100644 Source/LFOScope.cpp create mode 100644 Source/LFOScope.h create mode 100644 Source/LFOScopeBuffer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f5d0e6..135f24a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,7 +80,8 @@ target_sources(viive PRIVATE Source/DelayEngine.cpp Source/Parameters.cpp Source/PluginProcessor.cpp - Source/PluginEditor.cpp) + Source/PluginEditor.cpp + Source/LFOScope.cpp) juce_generate_juce_header(viive) diff --git a/Source/DelayEngine.h b/Source/DelayEngine.h index 4d993ac..9fde0d5 100644 --- a/Source/DelayEngine.h +++ b/Source/DelayEngine.h @@ -4,6 +4,9 @@ #include "ChorusEngine.h" #include "StereoDelay.h" #include "FilterEngine.h" +#if JUCE_DEBUG +#include "LFOScopeBuffer.h" +#endif using Filter = juce::dsp::StateVariableTPTFilter; @@ -32,6 +35,10 @@ class DelayEngine void setOffsetMs(const float value) { m_offsetMs = value * .5f; } void setDelayMode(const int modeIndex); +#if JUCE_DEBUG + LFOScopeBuffer& getLFOScopeBuffer() { return m_filterEngine.m_scopeBuffer; } +#endif + private: //juce::dsp::DelayLine m_delayLine; StereoDelay m_stereoDelay; diff --git a/Source/FilterEngine.cpp b/Source/FilterEngine.cpp index e4a56f4..d688aa4 100644 --- a/Source/FilterEngine.cpp +++ b/Source/FilterEngine.cpp @@ -102,10 +102,22 @@ void FilterEngine::update(const Parameters& params) void FilterEngine::processSample(const float& inL, const float& inR, float& outL, float& outR) { - m_lowCut.setCutoff(onePoleLowpass(modulatedCutoff(m_lowCutBase, m_lowCutLfos[Channel::Left].process(), m_lowCutModDepth), m_lowCutSmoothed[Channel::Left], m_cutoffSmoothCoeff), Channel::Left); - m_lowCut.setCutoff(onePoleLowpass(modulatedCutoff(m_lowCutBase, m_lowCutLfos[Channel::Right].process(), m_lowCutModDepth), m_lowCutSmoothed[Channel::Right], m_cutoffSmoothCoeff), Channel::Right); - m_highCut.setCutoff(onePoleLowpass(modulatedCutoff(m_highCutBase, m_highCutLfos[Channel::Left].process(), m_highCutModDepth), m_highCutSmoothed[Channel::Left], m_cutoffSmoothCoeff), Channel::Left); - m_highCut.setCutoff(onePoleLowpass(modulatedCutoff(m_highCutBase, m_highCutLfos[Channel::Right].process(), m_highCutModDepth), m_highCutSmoothed[Channel::Right], m_cutoffSmoothCoeff), Channel::Right); + float lcL = m_lowCutLfos[Channel::Left].process(); + float lcR = m_lowCutLfos[Channel::Right].process(); + float hcL = m_highCutLfos[Channel::Left].process(); + float hcR = m_highCutLfos[Channel::Right].process(); + +#if JUCE_DEBUG + if (++m_scopeCounter >= scopeDownsample) { + m_scopeCounter = 0; + m_scopeBuffer.push(lcL, lcR, hcL, hcR); + } +#endif + + m_lowCut.setCutoff(onePoleLowpass(modulatedCutoff(m_lowCutBase, lcL, m_lowCutModDepth), m_lowCutSmoothed[Channel::Left], m_cutoffSmoothCoeff), Channel::Left); + m_lowCut.setCutoff(onePoleLowpass(modulatedCutoff(m_lowCutBase, lcR, m_lowCutModDepth), m_lowCutSmoothed[Channel::Right], m_cutoffSmoothCoeff), Channel::Right); + m_highCut.setCutoff(onePoleLowpass(modulatedCutoff(m_highCutBase, hcL, m_highCutModDepth), m_highCutSmoothed[Channel::Left], m_cutoffSmoothCoeff), Channel::Left); + m_highCut.setCutoff(onePoleLowpass(modulatedCutoff(m_highCutBase, hcR, m_highCutModDepth), m_highCutSmoothed[Channel::Right], m_cutoffSmoothCoeff), Channel::Right); float lowL, lowR; m_lowCut.processSample(inL, inR, lowL, lowR); diff --git a/Source/FilterEngine.h b/Source/FilterEngine.h index 09a1c72..632bb11 100644 --- a/Source/FilterEngine.h +++ b/Source/FilterEngine.h @@ -5,6 +5,9 @@ #include "StereoFilter.h" #include "Types.h" #include "DSP.h" +#if JUCE_DEBUG +#include "LFOScopeBuffer.h" +#endif using FilterType = juce::dsp::StateVariableTPTFilterType; @@ -20,12 +23,16 @@ class FilterEngine void update(const Parameters& params); void processSample(const float& inL, const float& inR, float& outL, float& outR); +#if JUCE_DEBUG + LFOScopeBuffer m_scopeBuffer; +#endif + private: StereoFilter m_lowCut{ FilterType::highpass }; StereoFilter m_highCut{ FilterType::lowpass }; - LFO m_lowCutLfos[2] = { {1.0f, 0.0f, LFOShape::Sine}, {1.0f, 0.0f, LFOShape::Sine} }; - LFO m_highCutLfos[2] = { {1.0f, 0.0f, LFOShape::Sine}, {1.0f, 0.0f, LFOShape::Sine} }; + LFO m_lowCutLfos[2]; + LFO m_highCutLfos[2]; float m_lowCutBase = -1.0f; float m_lowCutQ = -1.0f; @@ -43,6 +50,11 @@ class FilterEngine float m_highCutSmoothed[2] = { Parameters::maxFilterFreq, Parameters::maxFilterFreq }; float m_cutoffSmoothCoeff = 0.0f; +#if JUCE_DEBUG + static constexpr int scopeDownsample = 64; + int m_scopeCounter = 0; +#endif + void setLowCutFilter(const Parameters& params); void setHighCutFilter(const Parameters& params); diff --git a/Source/LFOScope.cpp b/Source/LFOScope.cpp new file mode 100644 index 0000000..077a4b8 --- /dev/null +++ b/Source/LFOScope.cpp @@ -0,0 +1,71 @@ +#include "LFOScope.h" + +LFOScope::LFOScope(LFOScopeBuffer& buffer) : m_buffer(buffer) +{ + startTimerHz(refreshRate); +} + +LFOScope::~LFOScope() +{ + stopTimer(); +} + +void LFOScope::resized() {} + +void LFOScope::paint(juce::Graphics& g) +{ + auto bounds = getLocalBounds(); + int w = bounds.getWidth(); + int h = bounds.getHeight(); + + g.fillAll(juce::Colour(0xff1a1a1a)); + + g.setColour(juce::Colour(0xff333333)); + g.drawHorizontalLine(h / 2, 0.0f, (float)w); + g.drawHorizontalLine(h / 4, 0.0f, (float)w); + g.drawHorizontalLine(3 * h / 4, 0.0f, (float)w); + g.drawRect(bounds); + + int numSamples = std::min(w, LFOScopeBuffer::size); + int writeIdx = m_buffer.writeIndex.load(std::memory_order_acquire); + + const juce::Colour colours[LFOScopeBuffer::numChannels] = { + juce::Colour(0xff4fc3f7), // lowcut L — light blue + juce::Colour(0xff0288d1), // lowcut R — dark blue + juce::Colour(0xffffb74d), // highcut L — amber + juce::Colour(0xfff57c00), // highcut R — dark amber + }; + + const char* labels[LFOScopeBuffer::numChannels] = { + "LC L", "LC R", "HC L", "HC R" + }; + + for (int ch = 0; ch < LFOScopeBuffer::numChannels; ++ch) + drawChannel(g, ch, colours[ch], writeIdx, numSamples); + + g.setFont(11.0f); + for (int ch = 0; ch < LFOScopeBuffer::numChannels; ++ch) { + g.setColour(colours[ch]); + g.drawText(labels[ch], 4 + ch * 48, 4, 44, 14, juce::Justification::left); + } +} + +void LFOScope::drawChannel(juce::Graphics& g, int channel, juce::Colour colour, int writeIdx, int numSamples) +{ + int h = getHeight(); + juce::Path path; + + for (int x = 0; x < numSamples; ++x) { + int bufIdx = (writeIdx - numSamples + x + LFOScopeBuffer::size) & (LFOScopeBuffer::size - 1); + float value = m_buffer.data[channel][bufIdx]; + float y = juce::jmap(value, -1.0f, 1.0f, (float)(h - 1), 0.0f); + + if (x == 0) + path.startNewSubPath(0.0f, y); + else + path.lineTo((float)x, y); + } + + g.setColour(colour); + g.strokePath(path, juce::PathStrokeType(1.5f)); +} diff --git a/Source/LFOScope.h b/Source/LFOScope.h new file mode 100644 index 0000000..e556f5b --- /dev/null +++ b/Source/LFOScope.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include "LFOScopeBuffer.h" + +class LFOScope : public juce::Component, private juce::Timer +{ +public: + LFOScope(LFOScopeBuffer& buffer); + ~LFOScope() override; + + void paint(juce::Graphics&) override; + void resized() override; + +private: + LFOScopeBuffer& m_buffer; + + static constexpr int refreshRate = 30; + + void timerCallback() override { repaint(); } + void drawChannel(juce::Graphics& g, int channel, juce::Colour colour, int writeIdx, int numSamples); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LFOScope) +}; diff --git a/Source/LFOScopeBuffer.h b/Source/LFOScopeBuffer.h new file mode 100644 index 0000000..bd537e3 --- /dev/null +++ b/Source/LFOScopeBuffer.h @@ -0,0 +1,19 @@ +#pragma once +#include + +struct LFOScopeBuffer { + static constexpr int numChannels = 4; + static constexpr int size = 4096; + + float data[numChannels][size] = {}; + std::atomic writeIndex { 0 }; + + void push(float lcL, float lcR, float hcL, float hcR) noexcept { + int idx = writeIndex.load(std::memory_order_relaxed); + data[0][idx] = lcL; + data[1][idx] = lcR; + data[2][idx] = hcL; + data[3][idx] = hcR; + writeIndex.store((idx + 1) & (size - 1), std::memory_order_release); + } +}; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index a8bd991..c9509d0 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -54,6 +54,11 @@ ViiveAudioProcessorEditor::ViiveAudioProcessorEditor(ViiveAudioProcessor& p) addAndMakeVisible(m_meter); +#if JUCE_DEBUG + m_lfoScope = std::make_unique(p.getLFOScopeBuffer()); + addAndMakeVisible(*m_lfoScope); +#endif + bool syncL = m_audioProcessor.apvts.getParameter(tempoSyncLParamID.getParamID())->getValue() > 0.5f; bool syncR = m_audioProcessor.apvts.getParameter(tempoSyncRParamID.getParamID())->getValue() > 0.5f; @@ -61,7 +66,11 @@ ViiveAudioProcessorEditor::ViiveAudioProcessorEditor(ViiveAudioProcessor& p) m_audioProcessor.apvts.addParameterListener(tempoSyncLParamID.getParamID(), this); m_audioProcessor.apvts.addParameterListener(tempoSyncRParamID.getParamID(), this); - setSize (770, 370); +#if JUCE_DEBUG + setSize(770, 570); +#else + setSize(770, 370); +#endif } ViiveAudioProcessorEditor::~ViiveAudioProcessorEditor() @@ -84,8 +93,11 @@ void ViiveAudioProcessorEditor::resized() { auto bounds = getLocalBounds(); int y = 10; +#ifdef JUCE_DEBUG + int height = bounds.getHeight() - 30 - 180; +#else int height = bounds.getHeight() - 20; - +#endif m_chorusGroup.setBounds(bounds.getWidth() - 360, y + 35, 300, height / 2 - 10); m_delayGroup.setBounds(10, y + 35, m_chorusGroup.getX() - 20, height / 2 - 10); m_outputGroup.setBounds(bounds.getWidth() - 360, height / 2 + 45, 300, height / 2 - 30); @@ -116,7 +128,9 @@ void ViiveAudioProcessorEditor::resized() m_meter.setBounds(m_outputGroup.getRight() + 15, y + 45, 35, height - 45); - +#if JUCE_DEBUG + m_lfoScope->setBounds(10, 380, bounds.getWidth() - 20, 170); +#endif } void ViiveAudioProcessorEditor::updateDelayKnobs(bool syncLActive, bool syncRActive) diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 508f533..d426e50 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -6,6 +6,9 @@ #include "RotaryKnob.h" #include "SyncButton.h" #include "LevelMeter.h" +#if JUCE_DEBUG +#include "LFOScope.h" +#endif //============================================================================== /** @@ -56,6 +59,10 @@ class ViiveAudioProcessorEditor : public juce::AudioProcessorEditor, juce::GroupComponent m_delayGroup, m_filterGroup, m_outputGroup, m_chorusGroup; +#if JUCE_DEBUG + std::unique_ptr m_lfoScope; +#endif + virtual void parameterChanged(const juce::String& paramID, float newValue) override; void modeSelectorChanged(); void updateDelayKnobs(bool syncLActive, bool syncRActive); diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index df8009a..b66a1df 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -4,6 +4,9 @@ #include "Parameters.h" #include "DelayEngine.h" #include "Tempo.h" +#if JUCE_DEBUG +#include "LFOScopeBuffer.h" +#endif //============================================================================== /** @@ -44,6 +47,10 @@ class ViiveAudioProcessor : public juce::AudioProcessor std::atomic& getOutputLevelL() { return m_outputLevelL; } std::atomic& getOutputLevelR() { return m_outputLevelR; } +#if JUCE_DEBUG + LFOScopeBuffer& getLFOScopeBuffer() { return m_delayEngine.getLFOScopeBuffer(); } +#endif + //============================================================================== int getNumPrograms() override; int getCurrentProgram() override; From 486a22424492af0d44451b1918388c77cf3a6d96 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 22 Apr 2026 15:14:42 +0300 Subject: [PATCH 07/31] finetune filter mod range and value display --- Source/Parameters.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Source/Parameters.cpp b/Source/Parameters.cpp index 3638537..7920f1f 100644 --- a/Source/Parameters.cpp +++ b/Source/Parameters.cpp @@ -33,6 +33,9 @@ static juce::String stringFromPercent(float value, int) static juce::String stringFromHertz(float value, int) { + if (value < 5) { + return juce::String(value, 3) + " Hz"; + } if (value < 10) { return juce::String(value, 2) + " Hz"; } @@ -233,8 +236,8 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL layout.add(std::make_unique( lowCutModRateParamID.getParamID(), "Low Cut Mod Rate", - juce::NormalisableRange(0.01f, 2.0f, 0.01f), - 0.0f, + juce::NormalisableRange(0.05f, 2.0f, 0.001f), + 0.05f, juce::AudioParameterFloatAttributes() .withStringFromValueFunction(stringFromHertz) .withValueFromStringFunction(hzFromString) @@ -269,8 +272,8 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL layout.add(std::make_unique( highCutModRateParamID.getParamID(), "High Cut Mod Rate", - juce::NormalisableRange(0.01f, 2.0f, 0.01f), - 0.0f, + juce::NormalisableRange(0.05f, 2.0f, 0.001f), + 0.05f, juce::AudioParameterFloatAttributes() .withStringFromValueFunction(stringFromHertz) .withValueFromStringFunction(hzFromString) From ff662dcb3717422a85c566f7747420115272d99b Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 22 Apr 2026 15:15:04 +0300 Subject: [PATCH 08/31] add square to lfo shapes --- Source/LFO.cpp | 6 ++++++ Source/LFO.h | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Source/LFO.cpp b/Source/LFO.cpp index d63d220..fde345f 100644 --- a/Source/LFO.cpp +++ b/Source/LFO.cpp @@ -46,6 +46,8 @@ float LFO::process() { return processSine(offsetPhase); case (LFOShape::Triangle): return processTriangle(offsetPhase); + case (LFOShape::Square): + return processSquare(offsetPhase); default: return 0.0f; } @@ -57,4 +59,8 @@ float LFO::processSine(float phase) { float LFO::processTriangle(float phase) { return 1.0f - 4.0f * std::abs(phase - 0.5f); +} + +float LFO::processSquare(float phase) { + return (phase < 0.5f) ? 1.0f : -1.0f; } \ No newline at end of file diff --git a/Source/LFO.h b/Source/LFO.h index 9e91150..9357a6c 100644 --- a/Source/LFO.h +++ b/Source/LFO.h @@ -1,7 +1,8 @@ #pragma once #include -enum class LFOShape { Sine, Triangle }; +enum class LFOShape { Sine, Triangle, Square }; + class LFO { public: LFO(float frequency = 1.0f, float offset = 0.0f, LFOShape shape = LFOShape::Sine); @@ -15,7 +16,7 @@ class LFO { float process(); private: - LFOShape m_shape = LFOShape::Sine; + LFOShape m_shape; float m_sampleRate = 0.0f; @@ -27,4 +28,5 @@ class LFO { void setIncrement(); float processSine(float phase); float processTriangle(float phase); + float processSquare(float phase); }; \ No newline at end of file From b870c7ca0f3d7f8b799bf022406b140ad875ffed Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 22 Apr 2026 15:45:40 +0300 Subject: [PATCH 09/31] flip lfo phase for high cut --- Source/FilterEngine.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/FilterEngine.cpp b/Source/FilterEngine.cpp index d688aa4..eef0b52 100644 --- a/Source/FilterEngine.cpp +++ b/Source/FilterEngine.cpp @@ -104,8 +104,8 @@ void FilterEngine::processSample(const float& inL, const float& inR, float& outL { float lcL = m_lowCutLfos[Channel::Left].process(); float lcR = m_lowCutLfos[Channel::Right].process(); - float hcL = m_highCutLfos[Channel::Left].process(); - float hcR = m_highCutLfos[Channel::Right].process(); + float hcL = -m_highCutLfos[Channel::Left].process(); + float hcR = -m_highCutLfos[Channel::Right].process(); #if JUCE_DEBUG if (++m_scopeCounter >= scopeDownsample) { From bcdc0721b180b46f0495a74dc0b6774444f99648 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 22 Apr 2026 15:46:15 +0300 Subject: [PATCH 10/31] clean up old code from delay engine --- Source/DelayEngine.cpp | 40 ---------------------------------------- Source/DelayEngine.h | 11 ----------- 2 files changed, 51 deletions(-) diff --git a/Source/DelayEngine.cpp b/Source/DelayEngine.cpp index 0a2eb82..402bac8 100644 --- a/Source/DelayEngine.cpp +++ b/Source/DelayEngine.cpp @@ -19,8 +19,6 @@ void DelayEngine::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept spec.numChannels = 2; m_sampleRate = static_cast(sampleRate); m_feedbackCompressor.prepare(spec); - m_lowCutFilter.prepare(spec); - m_highCutFilter.prepare(spec); m_filterEngine.prepareToPlay(sampleRate, samplesPerBlock); m_feedbackHighpass.prepare(spec); m_chorusEngine.prepareToPlay(sampleRate, samplesPerBlock); @@ -32,8 +30,6 @@ void DelayEngine::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept void DelayEngine::reset() noexcept { m_stereoDelay.reset(); - m_lowCutFilter.reset(); - m_highCutFilter.reset(); m_chorusEngine.reset(); m_filterEngine.reset(); m_feedbackHighpass.reset(); @@ -42,10 +38,6 @@ void DelayEngine::reset() noexcept m_feedbackCompressor.reset(); m_feedbackL = 0.0f; m_feedbackR = 0.0f; - m_lowCutFreq = -1.0f; - m_lowCutQ = -1.0f; - m_highCutFreq = -1.0f; - m_highCutQ = -1.0f; m_baseDelayTimeMs = -1.0f; m_mixLevel = 0.5f; m_feedbackLevel = 0.0f; @@ -60,31 +52,6 @@ void DelayEngine::setDelayMode(const int modeIndex) } } -void DelayEngine::setLowCut(const Parameters& params) { - setFilterFreq(params.lowCutFreq(), m_lowCutFreq, m_lowCutFilter); - setFilterQ(params.lowCutQ(), m_lowCutQ, m_lowCutFilter); -} - -void DelayEngine::setHighCut(const Parameters& params) { - setFilterFreq(params.highCutFreq(), m_highCutFreq, m_highCutFilter); - setFilterQ(params.highCutQ(), m_highCutQ, m_highCutFilter); -} - -void DelayEngine::setFilterFreq(const float freq, float& currentFreq, Filter& filter) { - if (!juce::approximatelyEqual(freq, currentFreq)) { - filter.setCutoffFrequency(freq); - currentFreq = freq; - } -} - -void DelayEngine::setFilterQ(const float q, float& currentQ, Filter& filter) { - float newQ = std::min(q, Parameters::maxFilterQ); - if (!juce::approximatelyEqual(newQ, currentQ)) { - filter.setResonance(newQ); - currentQ = newQ; - } -} - void DelayEngine::setDelayTimes(const float targetL, const float targetR) { float targetLeftMs = targetL - m_offsetMs; float targetRightMs = targetR + m_offsetMs; @@ -143,13 +110,6 @@ void DelayEngine::processSample(const float& inL, const float& inR, float& outL, m_chorusEngine.processSample(wetL, wetR, wetL, wetR, params); - // sculpting filters, first left and then right channel - // wetL = m_lowCutFilter.processSample(0, wetL); - // wetL = m_highCutFilter.processSample(0, wetL); - - // wetR = m_lowCutFilter.processSample(1, wetR); - // wetR = m_highCutFilter.processSample(1, wetR); - m_filterEngine.processSample(wetL, wetR, wetL, wetR); // this removes rumble generated by low cut filter on higher q values diff --git a/Source/DelayEngine.h b/Source/DelayEngine.h index 9fde0d5..12b3b94 100644 --- a/Source/DelayEngine.h +++ b/Source/DelayEngine.h @@ -23,10 +23,6 @@ class DelayEngine void update(const Parameters& params); void processSample(const float& inL, const float& inR, float& outL, float& outR, const Parameters& params); - void setLowCut(const Parameters& params); - void setHighCut(const Parameters& params); - void setFilterFreq(const float freq, float& currentFreq, Filter& filter); - void setFilterQ(const float q, float& currentQ, Filter& filter); void setDelayTimes(const float delayInMsL, const float delayInMsR); void setFeedbackLevel(const float value) { m_feedbackLevel = value; } void setMixLevel(const float value) { m_mixLevel = value; } @@ -40,12 +36,9 @@ class DelayEngine #endif private: - //juce::dsp::DelayLine m_delayLine; StereoDelay m_stereoDelay; DelayMode m_delayMode = DelayMode::Stereo; FilterEngine m_filterEngine; - Filter m_lowCutFilter; - Filter m_highCutFilter; Filter m_feedbackHighpass; juce::dsp::Compressor m_feedbackCompressor; ChorusEngine m_chorusEngine; @@ -66,10 +59,6 @@ class DelayEngine float m_gainLevel = 1.0f; float m_widthLevel = 1.0f; float m_offsetMs = 0.0f; - float m_lowCutFreq = -1.0f; - float m_lowCutQ = -1.0f; - float m_highCutFreq = -1.0f; - float m_highCutQ = -1.0f; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DelayEngine) }; \ No newline at end of file From c4f14c633cecd5813a0f2d67d555c3beec2041bf Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 22 Apr 2026 22:01:43 +0300 Subject: [PATCH 11/31] add tempo lfo tempo sync + parameters --- Source/Parameters.cpp | 60 +++++++++++++++++++++++++++++++++++++++---- Source/Parameters.h | 19 +++++++++++++- Source/Tempo.cpp | 22 ++++++++++++++-- Source/Tempo.h | 1 + 4 files changed, 94 insertions(+), 8 deletions(-) diff --git a/Source/Parameters.cpp b/Source/Parameters.cpp index 7920f1f..d0d726d 100644 --- a/Source/Parameters.cpp +++ b/Source/Parameters.cpp @@ -79,7 +79,7 @@ const juce::StringArray Parameters::delayModes = { "Ping Pong RL", }; -const juce::StringArray Parameters::noteLengths = { +const juce::StringArray Parameters::delayNoteLengths = { "1/32", "1/16 trip", "1/32 dot", @@ -98,6 +98,18 @@ const juce::StringArray Parameters::noteLengths = { "1/1", }; +const juce::StringArray Parameters::modNoteLengths = { + "1/16", + "1/8", + "1/4", + "1/4 dot", + "1/2", + "1/1", + "2/1", + "4/1", + "8/1", +}; + Parameters::Parameters(juce::AudioProcessorValueTreeState& apvts) { @@ -119,11 +131,15 @@ Parameters::Parameters(juce::AudioProcessorValueTreeState& apvts) castParameter(apvts, lowCutModRateParamID, m_lowCutModRateParam); castParameter(apvts, lowCutModDepthParamID, m_lowCutModDepthParam); castParameter(apvts, lowCutModPhaseParamID, m_lowCutModPhaseParam); + castParameter(apvts, lowCutModTempoSyncParamID, m_lowCutModTempoSyncParam); + castParameter(apvts, lowCutModNoteParamID, m_lowCutModNoteParam); castParameter(apvts, highCutFreqParamID, m_highCutFreqParam); castParameter(apvts, highCutQParamID, m_highCutQParam); castParameter(apvts, highCutModRateParamID, m_highCutModRateParam); castParameter(apvts, highCutModDepthParamID, m_highCutModDepthParam); castParameter(apvts, highCutModPhaseParamID, m_highCutModPhaseParam); + castParameter(apvts, highCutModTempoSyncParamID, m_highCutModTempoSyncParam); + castParameter(apvts, highCutModNoteParamID, m_highCutModNoteParam); castParameter(apvts, chorusModRateParamID, m_chorusModRateParam); castParameter(apvts, chorusModDepthParamID, m_chorusModDepthParam); } @@ -158,13 +174,13 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL layout.add(std::make_unique( delayNoteLParamID, "Delay Note L", - noteLengths, + delayNoteLengths, 9 )); layout.add(std::make_unique( delayNoteRParamID, "Delay Note R", - noteLengths, + delayNoteLengths, 9 )); layout.add(std::make_unique( @@ -236,12 +252,23 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL layout.add(std::make_unique( lowCutModRateParamID.getParamID(), "Low Cut Mod Rate", - juce::NormalisableRange(0.05f, 2.0f, 0.001f), + juce::NormalisableRange(0.01f, 20.0f, 0.001f, 0.3f), 0.05f, juce::AudioParameterFloatAttributes() .withStringFromValueFunction(stringFromHertz) .withValueFromStringFunction(hzFromString) )); + layout.add(std::make_unique( + lowCutModNoteParamID, + "Low Cut Mod Note", + modNoteLengths, + 5 + )); + layout.add(std::make_unique( + lowCutModTempoSyncParamID.getParamID(), + "Low Cut Mod Tempo Sync", + false + )); layout.add(std::make_unique( lowCutModDepthParamID.getParamID(), "Low Cut Mod Depth", @@ -272,12 +299,23 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL layout.add(std::make_unique( highCutModRateParamID.getParamID(), "High Cut Mod Rate", - juce::NormalisableRange(0.05f, 2.0f, 0.001f), + juce::NormalisableRange(0.01f, 20.0f, 0.001f, 0.3f), 0.05f, juce::AudioParameterFloatAttributes() .withStringFromValueFunction(stringFromHertz) .withValueFromStringFunction(hzFromString) )); + layout.add(std::make_unique( + highCutModNoteParamID, + "High Cut Mod Note", + modNoteLengths, + 5 + )); + layout.add(std::make_unique( + highCutModTempoSyncParamID.getParamID(), + "High Cut Mod Tempo Sync", + false + )); layout.add(std::make_unique( highCutModDepthParamID.getParamID(), "High Cut Mod Depth", @@ -400,6 +438,18 @@ void Parameters::update(const Tempo& tempo) noexcept m_delayNoteL = m_delayNoteLParam->getIndex(); m_delayNoteR = m_delayNoteRParam->getIndex(); m_delayMode = m_delayModeParam->getIndex(); + m_lowCutModTempoSync = m_lowCutModTempoSyncParam->get(); + m_lowCutModNote = m_lowCutModNoteParam->getIndex(); + m_highCutModTempoSync = m_highCutModTempoSyncParam->get(); + m_highCutModNote = m_highCutModNoteParam->getIndex(); + if (m_lowCutModTempoSync) { + float syncedHz = float(tempo.noteLengthToHz(m_lowCutModNote)); + m_lowCutModRateParam->setValueNotifyingHost(m_lowCutModRateParam->convertTo0to1(syncedHz)); + } + if (m_highCutModTempoSync) { + float syncedHz = float(tempo.noteLengthToHz(m_highCutModNote)); + m_highCutModRateParam->setValueNotifyingHost(m_highCutModRateParam->convertTo0to1(syncedHz)); + } if (m_tempoSyncL) { float syncedMs = juce::jmin(float(tempo.noteLengthToMs(m_delayNoteL)), maxDelayTime); m_delayTimeLParam->setValueNotifyingHost(m_delayTimeLParam->convertTo0to1(syncedMs)); diff --git a/Source/Parameters.h b/Source/Parameters.h index 905c20c..9396727 100644 --- a/Source/Parameters.h +++ b/Source/Parameters.h @@ -19,11 +19,15 @@ const juce::ParameterID lowCutQParamID{ "lowCutQ", 1 }; const juce::ParameterID lowCutModRateParamID{ "lowCutModRate", 1 }; const juce::ParameterID lowCutModDepthParamID{ "lowCutModDepth", 1 }; const juce::ParameterID lowCutModPhaseParamID{ "lowCutModPhase", 1 }; +const juce::ParameterID lowCutModTempoSyncParamID{ "lowCutModTempoSync", 1 }; +const juce::ParameterID lowCutModNoteParamID{ "lowCutModNote", 1 }; const juce::ParameterID highCutFreqParamID{ "highCutFreq", 1 }; const juce::ParameterID highCutQParamID{ "highCutQ", 1 }; const juce::ParameterID highCutModRateParamID{ "highCutModRate", 1 }; const juce::ParameterID highCutModDepthParamID{ "highCutModDepth", 1 }; const juce::ParameterID highCutModPhaseParamID{ "highCutModPhase", 1 }; +const juce::ParameterID highCutModTempoSyncParamID{ "highCutModTempoSync", 1 }; +const juce::ParameterID highCutModNoteParamID{ "highCutModNote", 1 }; const juce::ParameterID chorusIntensityParamID{ "chorusIntensity", 1 }; const juce::ParameterID chorusModRateParamID{ "chorusModRate", 1 }; const juce::ParameterID chorusModDepthParamID{ "chorusModDepth", 1 }; @@ -45,7 +49,8 @@ class Parameters { static constexpr float defaultChorusModRate2 = 0.45f; // this is not a passed param but default value for 2nd chorus static constexpr float defaultChorusModDepth = 4.0f; static const juce::StringArray delayModes; - static const juce::StringArray noteLengths; + static const juce::StringArray delayNoteLengths; + static const juce::StringArray modNoteLengths; void prepareToPlay(double sampleRate) noexcept; void update(const Tempo& tempo) noexcept; @@ -69,11 +74,15 @@ class Parameters { float lowCutModRate() const { return m_lowCutModRate; } float lowCutModDepth() const { return m_lowCutModDepth; } float lowCutModPhase() const { return m_lowCutModPhase; } + bool lowCutModTempoSync() const { return m_lowCutModTempoSync; } + int lowCutModNote() const { return m_lowCutModNote; } float highCutFreq() const { return m_highCutFreq; } float highCutQ() const { return m_highCutQ; } float highCutModRate() const { return m_highCutModRate; } float highCutModDepth() const { return m_highCutModDepth; } float highCutModPhase() const { return m_highCutModPhase; } + bool highCutModTempoSync() const { return m_highCutModTempoSync; } + int highCutModNote() const { return m_highCutModNote; } float chorusIntensity() const { return m_chorusIntensity; } float chorusModRate() const { return m_chorusModRate; } float chorusModDepth() const { return m_chorusModDepth; } @@ -96,11 +105,15 @@ class Parameters { juce::AudioParameterFloat* m_lowCutModRateParam; juce::AudioParameterFloat* m_lowCutModDepthParam; juce::AudioParameterFloat* m_lowCutModPhaseParam; + juce::AudioParameterBool* m_lowCutModTempoSyncParam; + juce::AudioParameterChoice* m_lowCutModNoteParam; juce::AudioParameterFloat* m_highCutFreqParam; juce::AudioParameterFloat* m_highCutQParam; juce::AudioParameterFloat* m_highCutModRateParam; juce::AudioParameterFloat* m_highCutModDepthParam; juce::AudioParameterFloat* m_highCutModPhaseParam; + juce::AudioParameterBool* m_highCutModTempoSyncParam; + juce::AudioParameterChoice* m_highCutModNoteParam; juce::AudioParameterFloat* m_chorusIntensityParam; juce::AudioParameterFloat* m_chorusModRateParam; juce::AudioParameterFloat* m_chorusModDepthParam; @@ -146,11 +159,15 @@ class Parameters { float m_lowCutModRate = 0.0f; float m_lowCutModDepth = 0.0f; float m_lowCutModPhase = 0.0f; + bool m_lowCutModTempoSync = false; + int m_lowCutModNote = 0; float m_highCutFreq = 20000.0f; float m_highCutQ = defaultFilterQ; float m_highCutModRate = 0.0f; float m_highCutModDepth = 0.0f; float m_highCutModPhase = 0.0f; + bool m_highCutModTempoSync = false; + int m_highCutModNote = 0; float m_chorusIntensity = 0.0f; float m_chorusModRate = defaultChorusModRate; float m_chorusModDepth = defaultChorusModDepth; diff --git a/Source/Tempo.cpp b/Source/Tempo.cpp index 3d785d3..df774ae 100644 --- a/Source/Tempo.cpp +++ b/Source/Tempo.cpp @@ -1,6 +1,6 @@ #include "Tempo.h" -static std::array noteLengthMultipliers = +static std::array delayNoteLengthMultipliers = { 0.125, // 0 = 1/32 0.5 / 3.0, // 1 = 1/16 triplet @@ -20,6 +20,19 @@ static std::array noteLengthMultipliers = 4.0, // 15 = 1/1 }; +static std::array modNoteLengthMultipliers = +{ + 0.25, // 0 = 1/16 + 0.5, // 1 = 1/8 + 1.0, // 2 = 1/4 + 1.5, // 3 = 1/4 dotted + 2.0, // 4 = 1/2 + 4.0, // 5 = 1/1 + 8.0, // 6 = 2/1 + 16.0, // 7 = 4/1 + 32.0, // 8 = 8/1 +}; + void Tempo::reset() noexcept { m_bpm = 120; @@ -45,5 +58,10 @@ void Tempo::update(const juce::AudioPlayHead* playhead) noexcept double Tempo::noteLengthToMs(int index) const noexcept { - return 60000.0 * noteLengthMultipliers[size_t(index)] / m_bpm; + return 60000.0 * delayNoteLengthMultipliers[size_t(index)] / m_bpm; +} + +double Tempo::noteLengthToHz(int index) const noexcept +{ + return m_bpm / (60.0 * modNoteLengthMultipliers[size_t(index)]); } \ No newline at end of file diff --git a/Source/Tempo.h b/Source/Tempo.h index 0b6425c..f545467 100644 --- a/Source/Tempo.h +++ b/Source/Tempo.h @@ -10,6 +10,7 @@ class Tempo void update(const juce::AudioPlayHead* playhead) noexcept; double noteLengthToMs(int index) const noexcept; + double noteLengthToHz(int index) const noexcept; double getTempo() const noexcept { return m_bpm; } From f10dc35e954c4c9d5d9b998c54c2699a2a70ef8b Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Thu, 23 Apr 2026 11:19:54 +0300 Subject: [PATCH 12/31] add size options and custom look and feel for small rotary knob --- Source/RotaryKnob.cpp | 63 +++++++++++++++++++++++++++++++++++++++---- Source/RotaryKnob.h | 13 ++++++++- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/Source/RotaryKnob.cpp b/Source/RotaryKnob.cpp index 6c2b3dd..819aafa 100644 --- a/Source/RotaryKnob.cpp +++ b/Source/RotaryKnob.cpp @@ -1,27 +1,80 @@ #include #include "RotaryKnob.h" +juce::Font RotaryKnob::KnobLookAndFeel::getLabelFont(juce::Label&) +{ + return juce::Font(juce::FontOptions().withHeight(12.0f)); +} + //============================================================================== -RotaryKnob::RotaryKnob(const juce::String& text, juce::AudioProcessorValueTreeState& apvts, const juce::ParameterID& parameterID) +void RotaryKnob::KnobLookAndFeel::drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height, + float sliderPos, float rotaryStartAngle, float rotaryEndAngle, juce::Slider& slider) +{ + auto bounds = juce::Rectangle(x, y, width, height).toFloat().reduced(4); + auto radius = jmin(bounds.getWidth(), bounds.getHeight()) / 2.0f; + auto toAngle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle); + auto lineW = jmin(5.0f, radius * 0.3f); + auto arcRadius = radius - lineW * 0.5f; + + juce::Path backgroundArc; + backgroundArc.addCentredArc(bounds.getCentreX(), bounds.getCentreY(), + arcRadius, arcRadius, 0.0f, + rotaryStartAngle, rotaryEndAngle, true); + g.setColour(slider.findColour(juce::Slider::rotarySliderOutlineColourId)); + g.strokePath(backgroundArc, juce::PathStrokeType(lineW, juce::PathStrokeType::curved, juce::PathStrokeType::rounded)); + + if (slider.isEnabled()) + { + juce::Path valueArc; + valueArc.addCentredArc(bounds.getCentreX(), bounds.getCentreY(), + arcRadius, arcRadius, 0.0f, + rotaryStartAngle, toAngle, true); + g.setColour(slider.findColour(juce::Slider::rotarySliderFillColourId)); + g.strokePath(valueArc, juce::PathStrokeType(lineW, juce::PathStrokeType::curved, juce::PathStrokeType::rounded)); + } + + auto thumbWidth = lineW * 1.6f; // default is lineW * 2.0f + juce::Point thumb( + bounds.getCentreX() + arcRadius * std::cos(toAngle - juce::MathConstants::halfPi), + bounds.getCentreY() + arcRadius * std::sin(toAngle - juce::MathConstants::halfPi)); + g.setColour(slider.findColour(juce::Slider::thumbColourId)); + g.fillEllipse(juce::Rectangle(thumbWidth, thumbWidth).withCentre(thumb)); +} + +//============================================================================== +RotaryKnob::RotaryKnob(const juce::String& text, juce::AudioProcessorValueTreeState& apvts, const juce::ParameterID& parameterID, Size size) : m_attachment(apvts, parameterID.getParamID(), m_slider) { + const bool isSmall = size == Size::Small; + const int w = isSmall ? 45 : 70; + const int textBoxH = isSmall ? 12 : 16; + const int gap = isSmall ? 0 : 2; + const int labelH = isSmall ? 14 : 24; + + if (isSmall) + { + m_slider.setLookAndFeel(&m_laf); + m_slider.setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentBlack); + } m_slider.setSliderStyle(juce::Slider::SliderStyle::RotaryHorizontalVerticalDrag); - m_slider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 70, 16); - m_slider.setBounds(0, 0, 70, 86); + m_slider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, w, textBoxH); + m_slider.setBounds(0, 0, w, w + textBoxH + gap); addAndMakeVisible(m_slider); m_label.setText(text, juce::NotificationType::dontSendNotification); + m_label.setFont(juce::Font(juce::FontOptions().withHeight(isSmall ? 13.0f : 14.0f))); m_label.setJustificationType(juce::Justification::horizontallyCentred); m_label.setBorderSize(juce::BorderSize(0)); m_label.attachToComponent(&m_slider, false); addAndMakeVisible(m_label); - setSize(70, 110); + setSize(w, w + textBoxH + gap + labelH); } RotaryKnob::~RotaryKnob() { + m_slider.setLookAndFeel(nullptr); } void RotaryKnob::resized() { - m_slider.setTopLeftPosition(0, 24); + m_slider.setTopLeftPosition(0, getHeight() - m_slider.getHeight()); } diff --git a/Source/RotaryKnob.h b/Source/RotaryKnob.h index a1ddc35..68ae0ad 100644 --- a/Source/RotaryKnob.h +++ b/Source/RotaryKnob.h @@ -8,7 +8,9 @@ class RotaryKnob : public juce::Component { public: - RotaryKnob(const juce::String& text, juce::AudioProcessorValueTreeState& apvts, const juce::ParameterID& parameterID); + enum class Size { Normal, Small }; + + RotaryKnob(const juce::String& text, juce::AudioProcessorValueTreeState& apvts, const juce::ParameterID& parameterID, Size size = Size::Normal); ~RotaryKnob() override; void resized() override; @@ -16,6 +18,15 @@ class RotaryKnob : public juce::Component juce::Label& getLabel() { return m_label; } private: + struct KnobLookAndFeel : public juce::LookAndFeel_V4 + { + void drawRotarySlider(juce::Graphics&, int x, int y, int width, int height, + float sliderPos, float rotaryStartAngle, float rotaryEndAngle, + juce::Slider&) override; + juce::Font getLabelFont(juce::Label&) override; + }; + + KnobLookAndFeel m_laf; juce::Slider m_slider; juce::Label m_label; From 13d46e82a69487c1965ef300b90ec37fb0b5ec3a Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Thu, 23 Apr 2026 11:20:24 +0300 Subject: [PATCH 13/31] add filter mod depth controls to gui --- Source/Parameters.cpp | 5 +--- Source/PluginEditor.cpp | 66 +++++++++++++++++++++++++++++++++-------- Source/PluginEditor.h | 16 ++++++++-- 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/Source/Parameters.cpp b/Source/Parameters.cpp index d0d726d..0820497 100644 --- a/Source/Parameters.cpp +++ b/Source/Parameters.cpp @@ -33,10 +33,7 @@ static juce::String stringFromPercent(float value, int) static juce::String stringFromHertz(float value, int) { - if (value < 5) { - return juce::String(value, 3) + " Hz"; - } - if (value < 10) { + if (value < 20) { return juce::String(value, 2) + " Hz"; } if (value < 1000.0f) { diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index c9509d0..e601250 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -30,8 +30,23 @@ ViiveAudioProcessorEditor::ViiveAudioProcessorEditor(ViiveAudioProcessor& p) m_modeSelector.setSelectedId(param->getIndex() + 1, juce::dontSendNotification); addAndMakeVisible(m_delayGroup); - m_filterGroup.setText("Filter"); + m_filterModGroup.setText("Modulation"); + m_filterModGroup.setTextLabelPosition(juce::Justification::horizontallyCentred); + m_filterModGroup.setColour(juce::GroupComponent::outlineColourId, juce::Colours::transparentBlack); + m_filterModGroup.addAndMakeVisible(m_lowCutModRateKnob); + m_filterModGroup.addChildComponent(m_lowCutModNoteKnob); + m_filterModGroup.addAndMakeVisible(m_lowCutModDepthKnob); + m_filterModGroup.addAndMakeVisible(m_lowCutModPhaseKnob); + m_filterModGroup.addAndMakeVisible(m_highCutModRateKnob); + m_filterModGroup.addChildComponent(m_highCutModNoteKnob); + m_filterModGroup.addAndMakeVisible(m_highCutModDepthKnob); + m_filterModGroup.addAndMakeVisible(m_highCutModPhaseKnob); + m_filterModGroup.addAndMakeVisible(m_lowCutModTempoSyncButton); + m_filterModGroup.addAndMakeVisible(m_highCutModTempoSyncButton); + + m_filterGroup.setText("Filter"); m_filterGroup.setTextLabelPosition(juce::Justification::horizontallyCentred); + m_filterGroup.addAndMakeVisible(m_filterModGroup); m_filterGroup.addAndMakeVisible(m_lowCutFreqKnob); m_filterGroup.addAndMakeVisible(m_lowCutQKnob); m_filterGroup.addAndMakeVisible(m_highCutFreqKnob); @@ -61,15 +76,19 @@ ViiveAudioProcessorEditor::ViiveAudioProcessorEditor(ViiveAudioProcessor& p) bool syncL = m_audioProcessor.apvts.getParameter(tempoSyncLParamID.getParamID())->getValue() > 0.5f; bool syncR = m_audioProcessor.apvts.getParameter(tempoSyncRParamID.getParamID())->getValue() > 0.5f; + bool lowCutSync = m_audioProcessor.apvts.getParameter(lowCutModTempoSyncParamID.getParamID())->getValue() > 0.5f; + bool highCutSync = m_audioProcessor.apvts.getParameter(highCutModTempoSyncParamID.getParamID())->getValue() > 0.5f; - updateDelayKnobs(syncL, syncR); + updateSyncedKnobs(syncL, syncR, lowCutSync, highCutSync); m_audioProcessor.apvts.addParameterListener(tempoSyncLParamID.getParamID(), this); m_audioProcessor.apvts.addParameterListener(tempoSyncRParamID.getParamID(), this); + m_audioProcessor.apvts.addParameterListener(lowCutModTempoSyncParamID.getParamID(), this); + m_audioProcessor.apvts.addParameterListener(highCutModTempoSyncParamID.getParamID(), this); #if JUCE_DEBUG - setSize(770, 570); + setSize(770, 670); #else - setSize(770, 370); + setSize(770, 480); #endif } @@ -77,6 +96,8 @@ ViiveAudioProcessorEditor::~ViiveAudioProcessorEditor() { m_audioProcessor.apvts.removeParameterListener(tempoSyncLParamID.getParamID(), this); m_audioProcessor.apvts.removeParameterListener(tempoSyncRParamID.getParamID(), this); + m_audioProcessor.apvts.removeParameterListener(lowCutModTempoSyncParamID.getParamID(), this); + m_audioProcessor.apvts.removeParameterListener(highCutModTempoSyncParamID.getParamID(), this); } //============================================================================== @@ -98,11 +119,12 @@ void ViiveAudioProcessorEditor::resized() #else int height = bounds.getHeight() - 20; #endif - m_chorusGroup.setBounds(bounds.getWidth() - 360, y + 35, 300, height / 2 - 10); - m_delayGroup.setBounds(10, y + 35, m_chorusGroup.getX() - 20, height / 2 - 10); - m_outputGroup.setBounds(bounds.getWidth() - 360, height / 2 + 45, 300, height / 2 - 30); - m_filterGroup.setBounds(10, height / 2 + 45, m_outputGroup.getX() - 20, height / 2 - 30); - + m_chorusGroup.setBounds(bounds.getWidth() - 360, y + 35, 300, 165); + m_delayGroup.setBounds(10, y + 35, m_chorusGroup.getX() - 20, 165); + m_outputGroup.setBounds(bounds.getWidth() - 360, m_delayGroup.getBottom() + 10, 300, 155); + m_filterGroup.setBounds(10, m_delayGroup.getBottom() + 10, m_outputGroup.getX() - 20, 265); + m_filterModGroup.setBounds(0, m_lowCutFreqKnob.getBottom() + 30, m_filterGroup.getWidth(), 120); + m_delayTimeLKnob.setTopLeftPosition(20, 20); m_delayTimeRKnob.setTopLeftPosition(m_delayTimeLKnob.getRight() + 20, 20); m_delayNoteLKnob.setTopLeftPosition(m_delayTimeLKnob.getX(), m_delayTimeLKnob.getY()); @@ -117,8 +139,18 @@ void ViiveAudioProcessorEditor::resized() m_offsetKnob.setTopLeftPosition(m_feedbackKnob.getRight() + 20, 20); m_lowCutFreqKnob.setTopLeftPosition(20, 20); m_lowCutQKnob.setTopLeftPosition(m_lowCutFreqKnob.getRight() + 20, 20); + m_lowCutModRateKnob.setTopLeftPosition(20, 20); + m_lowCutModNoteKnob.setTopLeftPosition(m_lowCutModRateKnob.getX(), m_lowCutModRateKnob.getY()); + m_lowCutModDepthKnob.setTopLeftPosition(m_lowCutModRateKnob.getRight() + 10, 20); + m_lowCutModPhaseKnob.setTopLeftPosition(m_lowCutModDepthKnob.getRight() + 10, 20); + m_lowCutModTempoSyncButton.setTopLeftPosition(20, m_lowCutModRateKnob.getBottom() + 5); m_highCutFreqKnob.setTopLeftPosition(m_lowCutQKnob.getRight() + 20, 20); m_highCutQKnob.setTopLeftPosition(m_highCutFreqKnob.getRight() + 20, 20); + m_highCutModPhaseKnob.setTopRightPosition(m_highCutQKnob.getRight(), 20); + m_highCutModDepthKnob.setTopRightPosition(m_highCutModPhaseKnob.getX() - 10, 20); + m_highCutModRateKnob.setTopRightPosition(m_highCutModDepthKnob.getX() - 10, 20); + m_highCutModNoteKnob.setTopLeftPosition(m_highCutModRateKnob.getX(), m_highCutModRateKnob.getY()); + m_highCutModTempoSyncButton.setTopLeftPosition(m_highCutModRateKnob.getX(), m_highCutModRateKnob.getBottom() + 5); m_stereoKnob.setTopLeftPosition(20, 20); m_mixKnob.setTopLeftPosition(m_stereoKnob.getRight() + 20, 20); m_gainKnob.setTopLeftPosition(m_mixKnob.getRight() + 20, 20); @@ -129,24 +161,32 @@ void ViiveAudioProcessorEditor::resized() m_meter.setBounds(m_outputGroup.getRight() + 15, y + 45, 35, height - 45); #if JUCE_DEBUG - m_lfoScope->setBounds(10, 380, bounds.getWidth() - 20, 170); + m_lfoScope->setBounds(10, 490, bounds.getWidth() - 20, 170); #endif } -void ViiveAudioProcessorEditor::updateDelayKnobs(bool syncLActive, bool syncRActive) +void ViiveAudioProcessorEditor::updateSyncedKnobs(bool syncLActive, bool syncRActive, bool lowCutSyncActive, bool highCutSyncActive) { m_delayTimeLKnob.setVisible(!syncLActive); m_delayNoteLKnob.setVisible(syncLActive); m_delayTimeRKnob.setVisible(!syncRActive); m_delayNoteRKnob.setVisible(syncRActive); + m_lowCutModRateKnob.setVisible(!lowCutSyncActive); + m_lowCutModNoteKnob.setVisible(lowCutSyncActive); + m_highCutModRateKnob.setVisible(!highCutSyncActive); + m_highCutModNoteKnob.setVisible(highCutSyncActive); + DBG("Updated synced knobs: " << (syncLActive ? "Sync L active, " : "Sync L inactive, ") << (syncRActive ? "Sync R active, " : "Sync R inactive, ") << (lowCutSyncActive ? "Low Cut Sync active, " : "Low Cut Sync inactive, ") << (highCutSyncActive ? "High Cut Sync active" : "High Cut Sync inactive")); + } void ViiveAudioProcessorEditor::parameterChanged(const juce::String& paramID, float newValue) { - if (paramID == tempoSyncLParamID.getParamID() || paramID == tempoSyncRParamID.getParamID()) { + if (paramID == tempoSyncLParamID.getParamID() || paramID == tempoSyncRParamID.getParamID() || paramID == lowCutModTempoSyncParamID.getParamID() || paramID == highCutModTempoSyncParamID.getParamID()){ juce::MessageManager::callAsync([this] { bool syncL = m_audioProcessor.apvts.getParameter(tempoSyncLParamID.getParamID())->getValue() > 0.5f; bool syncR = m_audioProcessor.apvts.getParameter(tempoSyncRParamID.getParamID())->getValue() > 0.5f; - updateDelayKnobs(syncL, syncR); + bool lowCutSync = m_audioProcessor.apvts.getParameter(lowCutModTempoSyncParamID.getParamID())->getValue() > 0.5f; + bool highCutSync = m_audioProcessor.apvts.getParameter(highCutModTempoSyncParamID.getParamID())->getValue() > 0.5f; + updateSyncedKnobs(syncL, syncR, lowCutSync, highCutSync); }); } } diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index d426e50..ba28ade 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -39,8 +39,16 @@ class ViiveAudioProcessorEditor : public juce::AudioProcessorEditor, RotaryKnob m_lowCutFreqKnob{ "Low Cut Freq", m_audioProcessor.apvts, lowCutFreqParamID.getParamID() }; RotaryKnob m_lowCutQKnob{ "Low Cut Q", m_audioProcessor.apvts, lowCutQParamID.getParamID() }; + RotaryKnob m_lowCutModRateKnob{ "Rate", m_audioProcessor.apvts, lowCutModRateParamID.getParamID(), RotaryKnob::Size::Small }; + RotaryKnob m_lowCutModNoteKnob{ "Note", m_audioProcessor.apvts, lowCutModNoteParamID.getParamID(), RotaryKnob::Size::Small }; + RotaryKnob m_lowCutModDepthKnob{ "Depth", m_audioProcessor.apvts, lowCutModDepthParamID.getParamID(), RotaryKnob::Size::Small }; + RotaryKnob m_lowCutModPhaseKnob{ "Phase", m_audioProcessor.apvts, lowCutModPhaseParamID.getParamID(), RotaryKnob::Size::Small }; RotaryKnob m_highCutFreqKnob{ "High Cut Freq", m_audioProcessor.apvts, highCutFreqParamID.getParamID() }; RotaryKnob m_highCutQKnob{ "High Cut Q", m_audioProcessor.apvts, highCutQParamID.getParamID() }; + RotaryKnob m_highCutModRateKnob{ "Rate", m_audioProcessor.apvts, highCutModRateParamID.getParamID(), RotaryKnob::Size::Small }; + RotaryKnob m_highCutModNoteKnob{ "Note", m_audioProcessor.apvts, highCutModNoteParamID.getParamID(), RotaryKnob::Size::Small }; + RotaryKnob m_highCutModDepthKnob{ "Depth", m_audioProcessor.apvts, highCutModDepthParamID.getParamID(), RotaryKnob::Size::Small }; + RotaryKnob m_highCutModPhaseKnob{ "Phase", m_audioProcessor.apvts, highCutModPhaseParamID.getParamID(), RotaryKnob::Size::Small }; RotaryKnob m_gainKnob{ "Gain", m_audioProcessor.apvts, gainParamID.getParamID() }; RotaryKnob m_mixKnob{ "Mix", m_audioProcessor.apvts, mixParamID.getParamID() }; @@ -51,13 +59,17 @@ class ViiveAudioProcessorEditor : public juce::AudioProcessorEditor, SyncButton m_tempoSyncLButton{ "Sync", m_audioProcessor.apvts, tempoSyncLParamID.getParamID() }; SyncButton m_tempoSyncRButton{ "Sync", m_audioProcessor.apvts, tempoSyncRParamID.getParamID() }; + SyncButton m_lowCutModTempoSyncButton{ "Sync", m_audioProcessor.apvts, lowCutModTempoSyncParamID.getParamID() }; + SyncButton m_highCutModTempoSyncButton{ "Sync", m_audioProcessor.apvts, highCutModTempoSyncParamID.getParamID() }; juce::Label m_delayModeLabel; juce::ComboBox m_modeSelector; + juce::Label m_modulationLabel; + juce::HashMap m_linkedParams; - juce::GroupComponent m_delayGroup, m_filterGroup, m_outputGroup, m_chorusGroup; + juce::GroupComponent m_delayGroup, m_filterGroup, m_outputGroup, m_chorusGroup, m_filterModGroup; #if JUCE_DEBUG std::unique_ptr m_lfoScope; @@ -65,7 +77,7 @@ class ViiveAudioProcessorEditor : public juce::AudioProcessorEditor, virtual void parameterChanged(const juce::String& paramID, float newValue) override; void modeSelectorChanged(); - void updateDelayKnobs(bool syncLActive, bool syncRActive); + void updateSyncedKnobs(bool syncLActive, bool syncRActive, bool lowCutSyncActive, bool highCutSyncActive); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ViiveAudioProcessorEditor) }; From 6c0bdd9baf99d79b8470c1f8d9ee8c6757bfb6c8 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Thu, 23 Apr 2026 11:54:28 +0300 Subject: [PATCH 14/31] display phase in degrees --- Source/Parameters.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Source/Parameters.cpp b/Source/Parameters.cpp index 0820497..a02643f 100644 --- a/Source/Parameters.cpp +++ b/Source/Parameters.cpp @@ -47,6 +47,10 @@ static juce::String stringFromHertz(float value, int) } } +static juce::String stringFromDegrees(float value, int) +{ + return juce::String(int(value)) + "°"; +} static float millisecondsFromString(const juce::String& text) { @@ -69,6 +73,11 @@ static float hzFromString(const juce::String& text) return value; } +static float degreesFromString(const juce::String& text) +{ + return text.getFloatValue(); +} + const juce::StringArray Parameters::delayModes = { "Stereo", "Cross Fb", @@ -276,7 +285,10 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL lowCutModPhaseParamID.getParamID(), "Low Cut Mod Phase", juce::NormalisableRange(0.0f, 360.0f, 0.1f), - 0.0f + 0.0f, + juce::AudioParameterFloatAttributes() + .withStringFromValueFunction(stringFromDegrees) + .withValueFromStringFunction(degreesFromString) )); layout.add(std::make_unique( highCutFreqParamID.getParamID(), @@ -323,7 +335,10 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL highCutModPhaseParamID.getParamID(), "High Cut Mod Phase", juce::NormalisableRange(0.0f, 360.0f, 0.1f), - 0.0f + 0.0f, + juce::AudioParameterFloatAttributes() + .withStringFromValueFunction(stringFromDegrees) + .withValueFromStringFunction(degreesFromString) )); layout.add(std::make_unique( chorusIntensityParamID.getParamID(), From e17e1419e2665ab8960fd62a51ca577895909a25 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Fri, 24 Apr 2026 09:31:47 +0300 Subject: [PATCH 15/31] add saw up/down shapes to lfo --- Source/LFO.cpp | 12 ++++++++++++ Source/LFO.h | 4 +++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Source/LFO.cpp b/Source/LFO.cpp index fde345f..10208e9 100644 --- a/Source/LFO.cpp +++ b/Source/LFO.cpp @@ -48,6 +48,10 @@ float LFO::process() { return processTriangle(offsetPhase); case (LFOShape::Square): return processSquare(offsetPhase); + case (LFOShape::SawDown): + return processSawDown(offsetPhase); + case (LFOShape::SawUp): + return processSawUp(offsetPhase); default: return 0.0f; } @@ -63,4 +67,12 @@ float LFO::processTriangle(float phase) { float LFO::processSquare(float phase) { return (phase < 0.5f) ? 1.0f : -1.0f; +} + +float LFO::processSawUp(float phase) { + return phase * 2.0f - 1.0f; +} + +float LFO::processSawDown(float phase) { + return phase * -2.0f + 1.0f; } \ No newline at end of file diff --git a/Source/LFO.h b/Source/LFO.h index 9357a6c..c25e930 100644 --- a/Source/LFO.h +++ b/Source/LFO.h @@ -1,7 +1,7 @@ #pragma once #include -enum class LFOShape { Sine, Triangle, Square }; +enum class LFOShape { Sine, Triangle, Square, SawUp, SawDown }; class LFO { public: @@ -29,4 +29,6 @@ class LFO { float processSine(float phase); float processTriangle(float phase); float processSquare(float phase); + float processSawUp(float phase); + float processSawDown(float phase); }; \ No newline at end of file From d22b3979fcc8c8b61b3f7bc7c30e7398086ae42b Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Fri, 24 Apr 2026 09:35:05 +0300 Subject: [PATCH 16/31] add parameter for highcut mod invert toggle --- Source/FilterEngine.cpp | 8 ++++++-- Source/FilterEngine.h | 2 ++ Source/Parameters.cpp | 7 +++++++ Source/Parameters.h | 4 ++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Source/FilterEngine.cpp b/Source/FilterEngine.cpp index eef0b52..465ad7e 100644 --- a/Source/FilterEngine.cpp +++ b/Source/FilterEngine.cpp @@ -98,14 +98,18 @@ void FilterEngine::update(const Parameters& params) { setLowCutFilter(params); setHighCutFilter(params); + m_highCutModInvert = params.highCutModInvert(); } void FilterEngine::processSample(const float& inL, const float& inR, float& outL, float& outR) { float lcL = m_lowCutLfos[Channel::Left].process(); float lcR = m_lowCutLfos[Channel::Right].process(); - float hcL = -m_highCutLfos[Channel::Left].process(); - float hcR = -m_highCutLfos[Channel::Right].process(); + float hcL = m_highCutLfos[Channel::Left].process(); + float hcR = m_highCutLfos[Channel::Right].process(); + + hcL = m_highCutModInvert ? -hcL : hcL; + hcR = m_highCutModInvert ? -hcR : hcR; #if JUCE_DEBUG if (++m_scopeCounter >= scopeDownsample) { diff --git a/Source/FilterEngine.h b/Source/FilterEngine.h index 632bb11..6e8e938 100644 --- a/Source/FilterEngine.h +++ b/Source/FilterEngine.h @@ -46,6 +46,8 @@ class FilterEngine float m_highCutModRate = 0.0f; float m_highCutModPhase = 0.0f; + bool m_highCutModInvert = false; + float m_lowCutSmoothed[2] = { Parameters::minFilterFreq, Parameters::minFilterFreq }; float m_highCutSmoothed[2] = { Parameters::maxFilterFreq, Parameters::maxFilterFreq }; float m_cutoffSmoothCoeff = 0.0f; diff --git a/Source/Parameters.cpp b/Source/Parameters.cpp index a02643f..4aed702 100644 --- a/Source/Parameters.cpp +++ b/Source/Parameters.cpp @@ -146,6 +146,7 @@ Parameters::Parameters(juce::AudioProcessorValueTreeState& apvts) castParameter(apvts, highCutModPhaseParamID, m_highCutModPhaseParam); castParameter(apvts, highCutModTempoSyncParamID, m_highCutModTempoSyncParam); castParameter(apvts, highCutModNoteParamID, m_highCutModNoteParam); + castParameter(apvts, highCutModInvertParamID, m_highCutModInvertParam); castParameter(apvts, chorusModRateParamID, m_chorusModRateParam); castParameter(apvts, chorusModDepthParamID, m_chorusModDepthParam); } @@ -340,6 +341,11 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL .withStringFromValueFunction(stringFromDegrees) .withValueFromStringFunction(degreesFromString) )); + layout.add(std::make_unique( + highCutModInvertParamID.getParamID(), + "High Cut Mod Invert", + true + )); layout.add(std::make_unique( chorusIntensityParamID.getParamID(), "Chorus Intensity", @@ -454,6 +460,7 @@ void Parameters::update(const Tempo& tempo) noexcept m_lowCutModNote = m_lowCutModNoteParam->getIndex(); m_highCutModTempoSync = m_highCutModTempoSyncParam->get(); m_highCutModNote = m_highCutModNoteParam->getIndex(); + m_highCutModInvert = m_highCutModInvertParam->get(); if (m_lowCutModTempoSync) { float syncedHz = float(tempo.noteLengthToHz(m_lowCutModNote)); m_lowCutModRateParam->setValueNotifyingHost(m_lowCutModRateParam->convertTo0to1(syncedHz)); diff --git a/Source/Parameters.h b/Source/Parameters.h index 9396727..9c32e55 100644 --- a/Source/Parameters.h +++ b/Source/Parameters.h @@ -28,6 +28,7 @@ const juce::ParameterID highCutModDepthParamID{ "highCutModDepth", 1 }; const juce::ParameterID highCutModPhaseParamID{ "highCutModPhase", 1 }; const juce::ParameterID highCutModTempoSyncParamID{ "highCutModTempoSync", 1 }; const juce::ParameterID highCutModNoteParamID{ "highCutModNote", 1 }; +const juce::ParameterID highCutModInvertParamID{ "highCutInvert", 1 }; const juce::ParameterID chorusIntensityParamID{ "chorusIntensity", 1 }; const juce::ParameterID chorusModRateParamID{ "chorusModRate", 1 }; const juce::ParameterID chorusModDepthParamID{ "chorusModDepth", 1 }; @@ -83,6 +84,7 @@ class Parameters { float highCutModPhase() const { return m_highCutModPhase; } bool highCutModTempoSync() const { return m_highCutModTempoSync; } int highCutModNote() const { return m_highCutModNote; } + bool highCutModInvert() const { return m_highCutModInvert; } float chorusIntensity() const { return m_chorusIntensity; } float chorusModRate() const { return m_chorusModRate; } float chorusModDepth() const { return m_chorusModDepth; } @@ -114,6 +116,7 @@ class Parameters { juce::AudioParameterFloat* m_highCutModPhaseParam; juce::AudioParameterBool* m_highCutModTempoSyncParam; juce::AudioParameterChoice* m_highCutModNoteParam; + juce::AudioParameterBool* m_highCutModInvertParam; juce::AudioParameterFloat* m_chorusIntensityParam; juce::AudioParameterFloat* m_chorusModRateParam; juce::AudioParameterFloat* m_chorusModDepthParam; @@ -168,6 +171,7 @@ class Parameters { float m_highCutModPhase = 0.0f; bool m_highCutModTempoSync = false; int m_highCutModNote = 0; + bool m_highCutModInvert = false; float m_chorusIntensity = 0.0f; float m_chorusModRate = defaultChorusModRate; float m_chorusModDepth = defaultChorusModDepth; From 64e57ffacfbf80cb0d3ad472bcd01eeac007f2ba Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Fri, 24 Apr 2026 11:40:53 +0300 Subject: [PATCH 17/31] add params for setting filter lfo shape --- Source/FilterEngine.cpp | 24 ++++++++++++++++++++++++ Source/FilterEngine.h | 5 +++++ Source/LFO.h | 3 +-- Source/Parameters.cpp | 24 ++++++++++++++++++++++++ Source/Parameters.h | 11 ++++++++++- Source/Types.h | 2 ++ 6 files changed, 66 insertions(+), 3 deletions(-) diff --git a/Source/FilterEngine.cpp b/Source/FilterEngine.cpp index 465ad7e..b22df35 100644 --- a/Source/FilterEngine.cpp +++ b/Source/FilterEngine.cpp @@ -75,6 +75,16 @@ void FilterEngine::setFilterModPhase(float phase, float& current, LFO* lfos) } } +void FilterEngine::setFilterModShape(int shapeIndex, LFOShape& current, LFO* lfos) +{ + LFOShape newShape = static_cast(shapeIndex); + if (newShape != current) { + current = newShape; + for (int i = 0; i < 2; i++) + lfos[i].setShape(current); + } +} + void FilterEngine::setLowCutFilter(const Parameters& params) { setFilterFreq(params.lowCutFreq(), m_lowCutBase); @@ -82,6 +92,8 @@ void FilterEngine::setLowCutFilter(const Parameters& params) setFilterModRate(params.lowCutModRate(), m_lowCutModRate, m_lowCutLfos); setFilterModDepth(params.lowCutModDepth(), m_lowCutModDepth); setFilterModPhase(params.lowCutModPhase(), m_lowCutModPhase, m_lowCutLfos ); + setFilterModShape(params.lowCutModShape(), m_lowCutModShape, m_lowCutLfos); + m_lowCutModTempoSync = params.lowCutModTempoSync(); } void FilterEngine::setHighCutFilter(const Parameters& params) @@ -91,6 +103,8 @@ void FilterEngine::setHighCutFilter(const Parameters& params) setFilterModRate(params.highCutModRate(), m_highCutModRate, m_highCutLfos); setFilterModDepth(params.highCutModDepth(), m_highCutModDepth); setFilterModPhase(params.highCutModPhase(), m_highCutModPhase, m_highCutLfos); + setFilterModShape(params.highCutModShape(), m_highCutModShape, m_highCutLfos); + m_highCutModTempoSync = params.highCutModTempoSync(); } @@ -101,6 +115,16 @@ void FilterEngine::update(const Parameters& params) m_highCutModInvert = params.highCutModInvert(); } +void FilterEngine::resetLFOs() +{ + if (m_lowCutModTempoSync) + for (auto& lfo : m_lowCutLfos) + lfo.reset(); + if (m_highCutModTempoSync) + for (auto& lfo : m_highCutLfos) + lfo.reset(); +} + void FilterEngine::processSample(const float& inL, const float& inR, float& outL, float& outR) { float lcL = m_lowCutLfos[Channel::Left].process(); diff --git a/Source/FilterEngine.h b/Source/FilterEngine.h index 6e8e938..62f38b6 100644 --- a/Source/FilterEngine.h +++ b/Source/FilterEngine.h @@ -39,12 +39,16 @@ class FilterEngine float m_lowCutModDepth = 0.0f; float m_lowCutModRate = 0.0f; float m_lowCutModPhase = 0.0f; + LFOShape m_lowCutModShape = LFOShape::Sine; + bool m_lowCutModTempoSync = false; float m_highCutBase = -1.0f; float m_highCutQ = -1.0f; float m_highCutModDepth = 0.0f; float m_highCutModRate = 0.0f; float m_highCutModPhase = 0.0f; + LFOShape m_highCutModShape = LFOShape::Sine; + bool m_highCutModTempoSync = false; bool m_highCutModInvert = false; @@ -66,6 +70,7 @@ class FilterEngine void setFilterModDepth(float depth, float& current); void setFilterModRate(float rate, float& current, LFO* lfos); void setFilterModPhase(float phase, float& current, LFO* lfos); + void setFilterModShape(int shapeIndex, LFOShape& current, LFO* lfos); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FilterEngine) }; diff --git a/Source/LFO.h b/Source/LFO.h index c25e930..42f78f4 100644 --- a/Source/LFO.h +++ b/Source/LFO.h @@ -1,7 +1,6 @@ #pragma once #include - -enum class LFOShape { Sine, Triangle, Square, SawUp, SawDown }; +#include "Types.h" class LFO { public: diff --git a/Source/Parameters.cpp b/Source/Parameters.cpp index 4aed702..099d652 100644 --- a/Source/Parameters.cpp +++ b/Source/Parameters.cpp @@ -116,6 +116,14 @@ const juce::StringArray Parameters::modNoteLengths = { "8/1", }; +const juce::StringArray Parameters::lfoShapes = { + "SIN", + "TRI", + "SQR", + "SAW+", + "SAW-", +}; + Parameters::Parameters(juce::AudioProcessorValueTreeState& apvts) { @@ -139,6 +147,7 @@ Parameters::Parameters(juce::AudioProcessorValueTreeState& apvts) castParameter(apvts, lowCutModPhaseParamID, m_lowCutModPhaseParam); castParameter(apvts, lowCutModTempoSyncParamID, m_lowCutModTempoSyncParam); castParameter(apvts, lowCutModNoteParamID, m_lowCutModNoteParam); + castParameter(apvts, lowCutModShapeParamID, m_lowCutModShapeParam); castParameter(apvts, highCutFreqParamID, m_highCutFreqParam); castParameter(apvts, highCutQParamID, m_highCutQParam); castParameter(apvts, highCutModRateParamID, m_highCutModRateParam); @@ -146,6 +155,7 @@ Parameters::Parameters(juce::AudioProcessorValueTreeState& apvts) castParameter(apvts, highCutModPhaseParamID, m_highCutModPhaseParam); castParameter(apvts, highCutModTempoSyncParamID, m_highCutModTempoSyncParam); castParameter(apvts, highCutModNoteParamID, m_highCutModNoteParam); + castParameter(apvts, highCutModShapeParamID, m_highCutModShapeParam); castParameter(apvts, highCutModInvertParamID, m_highCutModInvertParam); castParameter(apvts, chorusModRateParamID, m_chorusModRateParam); castParameter(apvts, chorusModDepthParamID, m_chorusModDepthParam); @@ -291,6 +301,12 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL .withStringFromValueFunction(stringFromDegrees) .withValueFromStringFunction(degreesFromString) )); + layout.add(std::make_unique( + lowCutModShapeParamID, + "Low Cut Mod Shape", + lfoShapes, + 0 + )); layout.add(std::make_unique( highCutFreqParamID.getParamID(), "High Cut Frequency", @@ -321,6 +337,12 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL modNoteLengths, 5 )); + layout.add(std::make_unique( + highCutModShapeParamID, + "High Cut Mod Shape", + lfoShapes, + 0 + )); layout.add(std::make_unique( highCutModTempoSyncParamID.getParamID(), "High Cut Mod Tempo Sync", @@ -461,6 +483,8 @@ void Parameters::update(const Tempo& tempo) noexcept m_highCutModTempoSync = m_highCutModTempoSyncParam->get(); m_highCutModNote = m_highCutModNoteParam->getIndex(); m_highCutModInvert = m_highCutModInvertParam->get(); + m_lowCutModShape = m_lowCutModShapeParam->getIndex(); + m_highCutModShape = m_highCutModShapeParam->getIndex(); if (m_lowCutModTempoSync) { float syncedHz = float(tempo.noteLengthToHz(m_lowCutModNote)); m_lowCutModRateParam->setValueNotifyingHost(m_lowCutModRateParam->convertTo0to1(syncedHz)); diff --git a/Source/Parameters.h b/Source/Parameters.h index 9c32e55..3798636 100644 --- a/Source/Parameters.h +++ b/Source/Parameters.h @@ -21,6 +21,7 @@ const juce::ParameterID lowCutModDepthParamID{ "lowCutModDepth", 1 }; const juce::ParameterID lowCutModPhaseParamID{ "lowCutModPhase", 1 }; const juce::ParameterID lowCutModTempoSyncParamID{ "lowCutModTempoSync", 1 }; const juce::ParameterID lowCutModNoteParamID{ "lowCutModNote", 1 }; +const juce::ParameterID lowCutModShapeParamID{ "lowCutModShape", 1 }; const juce::ParameterID highCutFreqParamID{ "highCutFreq", 1 }; const juce::ParameterID highCutQParamID{ "highCutQ", 1 }; const juce::ParameterID highCutModRateParamID{ "highCutModRate", 1 }; @@ -28,7 +29,8 @@ const juce::ParameterID highCutModDepthParamID{ "highCutModDepth", 1 }; const juce::ParameterID highCutModPhaseParamID{ "highCutModPhase", 1 }; const juce::ParameterID highCutModTempoSyncParamID{ "highCutModTempoSync", 1 }; const juce::ParameterID highCutModNoteParamID{ "highCutModNote", 1 }; -const juce::ParameterID highCutModInvertParamID{ "highCutInvert", 1 }; +const juce::ParameterID highCutModShapeParamID{ "highCutModShape", 1 }; +const juce::ParameterID highCutModInvertParamID{ "highCutModInvert", 1 }; const juce::ParameterID chorusIntensityParamID{ "chorusIntensity", 1 }; const juce::ParameterID chorusModRateParamID{ "chorusModRate", 1 }; const juce::ParameterID chorusModDepthParamID{ "chorusModDepth", 1 }; @@ -52,6 +54,7 @@ class Parameters { static const juce::StringArray delayModes; static const juce::StringArray delayNoteLengths; static const juce::StringArray modNoteLengths; + static const juce::StringArray lfoShapes; void prepareToPlay(double sampleRate) noexcept; void update(const Tempo& tempo) noexcept; @@ -77,6 +80,7 @@ class Parameters { float lowCutModPhase() const { return m_lowCutModPhase; } bool lowCutModTempoSync() const { return m_lowCutModTempoSync; } int lowCutModNote() const { return m_lowCutModNote; } + int lowCutModShape() const { return m_lowCutModShape; } float highCutFreq() const { return m_highCutFreq; } float highCutQ() const { return m_highCutQ; } float highCutModRate() const { return m_highCutModRate; } @@ -84,6 +88,7 @@ class Parameters { float highCutModPhase() const { return m_highCutModPhase; } bool highCutModTempoSync() const { return m_highCutModTempoSync; } int highCutModNote() const { return m_highCutModNote; } + int highCutModShape() const { return m_highCutModShape; } bool highCutModInvert() const { return m_highCutModInvert; } float chorusIntensity() const { return m_chorusIntensity; } float chorusModRate() const { return m_chorusModRate; } @@ -109,6 +114,7 @@ class Parameters { juce::AudioParameterFloat* m_lowCutModPhaseParam; juce::AudioParameterBool* m_lowCutModTempoSyncParam; juce::AudioParameterChoice* m_lowCutModNoteParam; + juce::AudioParameterChoice* m_lowCutModShapeParam; juce::AudioParameterFloat* m_highCutFreqParam; juce::AudioParameterFloat* m_highCutQParam; juce::AudioParameterFloat* m_highCutModRateParam; @@ -116,6 +122,7 @@ class Parameters { juce::AudioParameterFloat* m_highCutModPhaseParam; juce::AudioParameterBool* m_highCutModTempoSyncParam; juce::AudioParameterChoice* m_highCutModNoteParam; + juce::AudioParameterChoice* m_highCutModShapeParam; juce::AudioParameterBool* m_highCutModInvertParam; juce::AudioParameterFloat* m_chorusIntensityParam; juce::AudioParameterFloat* m_chorusModRateParam; @@ -164,6 +171,7 @@ class Parameters { float m_lowCutModPhase = 0.0f; bool m_lowCutModTempoSync = false; int m_lowCutModNote = 0; + int m_lowCutModShape = 0; float m_highCutFreq = 20000.0f; float m_highCutQ = defaultFilterQ; float m_highCutModRate = 0.0f; @@ -171,6 +179,7 @@ class Parameters { float m_highCutModPhase = 0.0f; bool m_highCutModTempoSync = false; int m_highCutModNote = 0; + int m_highCutModShape = 0; bool m_highCutModInvert = false; float m_chorusIntensity = 0.0f; float m_chorusModRate = defaultChorusModRate; diff --git a/Source/Types.h b/Source/Types.h index 4bc447c..a27e55b 100644 --- a/Source/Types.h +++ b/Source/Types.h @@ -1,3 +1,5 @@ #pragma once enum Channel { Left = 0, Right = 1 }; + +enum class LFOShape { Sine, Triangle, Square, SawUp, SawDown }; From ecc33a830e3261790cf3503e86fe9dcb37f091f2 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Fri, 24 Apr 2026 14:22:54 +0300 Subject: [PATCH 18/31] sync tempo synced lfo to song position --- Source/DelayEngine.cpp | 5 +++++ Source/DelayEngine.h | 1 + Source/FilterEngine.cpp | 31 ++++++++++++++++++++++------ Source/FilterEngine.h | 5 +++++ Source/LFO.h | 1 + Source/PluginProcessor.cpp | 2 ++ Source/Sync.h | 41 ++++++++++++++++++++++++++++++++++++++ Source/Tempo.cpp | 39 ++++++------------------------------ Source/Tempo.h | 4 ++++ 9 files changed, 90 insertions(+), 39 deletions(-) create mode 100644 Source/Sync.h diff --git a/Source/DelayEngine.cpp b/Source/DelayEngine.cpp index 402bac8..d8b4a32 100644 --- a/Source/DelayEngine.cpp +++ b/Source/DelayEngine.cpp @@ -76,6 +76,11 @@ void DelayEngine::update(const Parameters& params) m_filterEngine.update(params); } +void DelayEngine::updateSyncedModulators(const double ppqPosition) +{ + m_filterEngine.updateLFOs(ppqPosition); +} + void DelayEngine::processSample(const float& inL, const float& inR, float& outL, float& outR, const Parameters& params) { float dryL = inL; diff --git a/Source/DelayEngine.h b/Source/DelayEngine.h index 12b3b94..1108f0f 100644 --- a/Source/DelayEngine.h +++ b/Source/DelayEngine.h @@ -30,6 +30,7 @@ class DelayEngine void setWidthLevel(const float value) { m_widthLevel = value; } void setOffsetMs(const float value) { m_offsetMs = value * .5f; } void setDelayMode(const int modeIndex); + void updateSyncedModulators(const double ppqPosition); #if JUCE_DEBUG LFOScopeBuffer& getLFOScopeBuffer() { return m_filterEngine.m_scopeBuffer; } diff --git a/Source/FilterEngine.cpp b/Source/FilterEngine.cpp index b22df35..de1b7c3 100644 --- a/Source/FilterEngine.cpp +++ b/Source/FilterEngine.cpp @@ -1,7 +1,13 @@ #include "FilterEngine.h" - +#include "Sync.h" FilterEngine::FilterEngine() { + int seedLow = m_random.nextInt(); + int seedHigh = m_random.nextInt(); + for (int i = 0; i < 2; i++) { + m_lowCutLfos[i].setSeed(seedLow); + m_highCutLfos[i].setSeed(seedHigh); + } } void FilterEngine::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept @@ -80,6 +86,11 @@ void FilterEngine::setFilterModShape(int shapeIndex, LFOShape& current, LFO* lfo LFOShape newShape = static_cast(shapeIndex); if (newShape != current) { current = newShape; + if (current == LFOShape::SampleAndHold) { + int seed = m_random.nextInt(); + for (int i = 0; i < 2; i++) + lfos[i].setSeed(seed); + } for (int i = 0; i < 2; i++) lfos[i].setShape(current); } @@ -94,6 +105,7 @@ void FilterEngine::setLowCutFilter(const Parameters& params) setFilterModPhase(params.lowCutModPhase(), m_lowCutModPhase, m_lowCutLfos ); setFilterModShape(params.lowCutModShape(), m_lowCutModShape, m_lowCutLfos); m_lowCutModTempoSync = params.lowCutModTempoSync(); + m_lowCutModNote = params.lowCutModNote(); } void FilterEngine::setHighCutFilter(const Parameters& params) @@ -105,6 +117,7 @@ void FilterEngine::setHighCutFilter(const Parameters& params) setFilterModPhase(params.highCutModPhase(), m_highCutModPhase, m_highCutLfos); setFilterModShape(params.highCutModShape(), m_highCutModShape, m_highCutLfos); m_highCutModTempoSync = params.highCutModTempoSync(); + m_highCutModNote = params.highCutModNote(); } @@ -115,14 +128,20 @@ void FilterEngine::update(const Parameters& params) m_highCutModInvert = params.highCutModInvert(); } -void FilterEngine::resetLFOs() +void FilterEngine::updateLFOs(const double ppqPosition) noexcept { if (m_lowCutModTempoSync) - for (auto& lfo : m_lowCutLfos) - lfo.reset(); + { + float phase = getSyncedPhase(m_lowCutModNote, ppqPosition); + m_lowCutLfos[Channel::Left].setPhase(phase); + m_lowCutLfos[Channel::Right].setPhase(phase); + } if (m_highCutModTempoSync) - for (auto& lfo : m_highCutLfos) - lfo.reset(); + { + float phase = getSyncedPhase(m_highCutModNote, ppqPosition); + m_highCutLfos[Channel::Left].setPhase(phase); + m_highCutLfos[Channel::Right].setPhase(phase); + } } void FilterEngine::processSample(const float& inL, const float& inR, float& outL, float& outR) diff --git a/Source/FilterEngine.h b/Source/FilterEngine.h index 62f38b6..0332645 100644 --- a/Source/FilterEngine.h +++ b/Source/FilterEngine.h @@ -23,6 +23,8 @@ class FilterEngine void update(const Parameters& params); void processSample(const float& inL, const float& inR, float& outL, float& outR); + void updateLFOs(const double ppqPosition) noexcept; + #if JUCE_DEBUG LFOScopeBuffer m_scopeBuffer; #endif @@ -30,6 +32,7 @@ class FilterEngine private: StereoFilter m_lowCut{ FilterType::highpass }; StereoFilter m_highCut{ FilterType::lowpass }; + juce::Random m_random; LFO m_lowCutLfos[2]; LFO m_highCutLfos[2]; @@ -41,6 +44,7 @@ class FilterEngine float m_lowCutModPhase = 0.0f; LFOShape m_lowCutModShape = LFOShape::Sine; bool m_lowCutModTempoSync = false; + int m_lowCutModNote = 0; float m_highCutBase = -1.0f; float m_highCutQ = -1.0f; @@ -49,6 +53,7 @@ class FilterEngine float m_highCutModPhase = 0.0f; LFOShape m_highCutModShape = LFOShape::Sine; bool m_highCutModTempoSync = false; + int m_highCutModNote = 0; bool m_highCutModInvert = false; diff --git a/Source/LFO.h b/Source/LFO.h index 42f78f4..85c08a7 100644 --- a/Source/LFO.h +++ b/Source/LFO.h @@ -12,6 +12,7 @@ class LFO { void setFrequency(float frequency); void setOffset(float offset); void setShape(LFOShape shape); + void setPhase(float phase) { m_phase = std::fmod(phase, 1.0f); } float process(); private: diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 9834a85..42022e9 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -116,6 +116,8 @@ void ViiveAudioProcessor::processBlock(juce::AudioBuffer& buffer, juce::M m_tempo.update(getPlayHead()); m_params.update(m_tempo); + if (m_tempo.getIsPlaying()) + m_delayEngine.updateSyncedModulators(m_tempo.getPpqPosition()); m_delayEngine.setDelayMode(m_params.delayMode()); //float syncedTimeL = static_cast(m_tempo.noteLengthToMs(m_params.delayNoteL())); diff --git a/Source/Sync.h b/Source/Sync.h new file mode 100644 index 0000000..5286100 --- /dev/null +++ b/Source/Sync.h @@ -0,0 +1,41 @@ +#pragma once +#include + +inline static std::array delayNoteLengthMultipliers = +{ + 0.125, // 0 = 1/32 + 0.5 / 3.0, // 1 = 1/16 triplet + 0.1875, // 2 = 1/32 dotted + 0.25, // 3 = 1/16 + 1.0 / 3.0, // 4 = 1/8 triplet + 0.375, // 5 = 1/16 dotted + 0.5, // 6 = 1/8 + 2.0 / 3.0, // 7 = 1/4 triplet + 0.75, // 8 = 1/8 dotted + 1.0, // 9 = 1/4 + 4.0 / 3.0, // 10 = 1/2 triplet + 1.5, // 11 = 1/4 dotted + 2.0, // 12 = 1/2 + 8.0 / 3.0, // 13 = 1/1 triplet + 3.0, // 14 = 1/2 dotted + 4.0, // 15 = 1/1 +}; + +inline static std::array modNoteLengthMultipliers = +{ + 0.25, // 0 = 1/16 + 0.5, // 1 = 1/8 + 1.0, // 2 = 1/4 + 1.5, // 3 = 1/4 dotted + 2.0, // 4 = 1/2 + 4.0, // 5 = 1/1 + 8.0, // 6 = 2/1 + 16.0, // 7 = 4/1 + 32.0, // 8 = 8/1 +}; + +inline float getSyncedPhase(int index, double ppqPosition) +{ + double cycleLengthInBeats = modNoteLengthMultipliers[size_t(index)]; + return static_cast(std::fmod(ppqPosition, cycleLengthInBeats) / cycleLengthInBeats); +} \ No newline at end of file diff --git a/Source/Tempo.cpp b/Source/Tempo.cpp index df774ae..e3281e3 100644 --- a/Source/Tempo.cpp +++ b/Source/Tempo.cpp @@ -1,37 +1,5 @@ #include "Tempo.h" - -static std::array delayNoteLengthMultipliers = -{ - 0.125, // 0 = 1/32 - 0.5 / 3.0, // 1 = 1/16 triplet - 0.1875, // 2 = 1/32 dotted - 0.25, // 3 = 1/16 - 1.0 / 3.0, // 4 = 1/8 triplet - 0.375, // 5 = 1/16 dotted - 0.5, // 6 = 1/8 - 2.0 / 3.0, // 7 = 1/4 triplet - 0.75, // 8 = 1/8 dotted - 1.0, // 9 = 1/4 - 4.0 / 3.0, // 10 = 1/2 triplet - 1.5, // 11 = 1/4 dotted - 2.0, // 12 = 1/2 - 8.0 / 3.0, // 13 = 1/1 triplet - 3.0, // 14 = 1/2 dotted - 4.0, // 15 = 1/1 -}; - -static std::array modNoteLengthMultipliers = -{ - 0.25, // 0 = 1/16 - 0.5, // 1 = 1/8 - 1.0, // 2 = 1/4 - 1.5, // 3 = 1/4 dotted - 2.0, // 4 = 1/2 - 4.0, // 5 = 1/1 - 8.0, // 6 = 2/1 - 16.0, // 7 = 4/1 - 32.0, // 8 = 8/1 -}; +#include "Sync.h" void Tempo::reset() noexcept { @@ -54,6 +22,11 @@ void Tempo::update(const juce::AudioPlayHead* playhead) noexcept if (pos.getBpm().hasValue()) m_bpm = *pos.getBpm(); + + m_isPlaying = pos.getIsPlaying(); + + m_ppqPosition = pos.getPpqPosition().hasValue() ? *pos.getPpqPosition() : 0.0; + } double Tempo::noteLengthToMs(int index) const noexcept diff --git a/Source/Tempo.h b/Source/Tempo.h index f545467..b488cf0 100644 --- a/Source/Tempo.h +++ b/Source/Tempo.h @@ -13,7 +13,11 @@ class Tempo double noteLengthToHz(int index) const noexcept; double getTempo() const noexcept { return m_bpm; } + bool getIsPlaying() const noexcept { return m_isPlaying; } + double getPpqPosition() const noexcept { return m_ppqPosition; } private: double m_bpm = 120.0; + bool m_isPlaying = false; + double m_ppqPosition = 0.0; }; \ No newline at end of file From fd62362d0fb54359e122f83f5ead96fefa95346f Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Fri, 24 Apr 2026 15:53:32 +0300 Subject: [PATCH 19/31] add sample & hold modulation --- Source/FilterEngine.cpp | 13 ++++--------- Source/FilterEngine.h | 2 +- Source/LFO.cpp | 10 ++++++++++ Source/LFO.h | 8 ++++++++ Source/Parameters.cpp | 1 + Source/Types.h | 2 +- 6 files changed, 25 insertions(+), 11 deletions(-) diff --git a/Source/FilterEngine.cpp b/Source/FilterEngine.cpp index de1b7c3..394f9ad 100644 --- a/Source/FilterEngine.cpp +++ b/Source/FilterEngine.cpp @@ -2,11 +2,11 @@ #include "Sync.h" FilterEngine::FilterEngine() { - int seedLow = m_random.nextInt(); - int seedHigh = m_random.nextInt(); + int lowCutSeed = juce::Random::getSystemRandom().nextInt(); + int highCutSeed = juce::Random::getSystemRandom().nextInt(); for (int i = 0; i < 2; i++) { - m_lowCutLfos[i].setSeed(seedLow); - m_highCutLfos[i].setSeed(seedHigh); + m_lowCutLfos[i].setSeed(lowCutSeed); + m_highCutLfos[i].setSeed(highCutSeed); } } @@ -86,11 +86,6 @@ void FilterEngine::setFilterModShape(int shapeIndex, LFOShape& current, LFO* lfo LFOShape newShape = static_cast(shapeIndex); if (newShape != current) { current = newShape; - if (current == LFOShape::SampleAndHold) { - int seed = m_random.nextInt(); - for (int i = 0; i < 2; i++) - lfos[i].setSeed(seed); - } for (int i = 0; i < 2; i++) lfos[i].setShape(current); } diff --git a/Source/FilterEngine.h b/Source/FilterEngine.h index 0332645..009f064 100644 --- a/Source/FilterEngine.h +++ b/Source/FilterEngine.h @@ -33,7 +33,7 @@ class FilterEngine StereoFilter m_lowCut{ FilterType::highpass }; StereoFilter m_highCut{ FilterType::lowpass }; juce::Random m_random; - + LFO m_lowCutLfos[2]; LFO m_highCutLfos[2]; diff --git a/Source/LFO.cpp b/Source/LFO.cpp index 10208e9..5cdba11 100644 --- a/Source/LFO.cpp +++ b/Source/LFO.cpp @@ -40,6 +40,8 @@ void LFO::setIncrement() { float LFO::process() { m_phase += m_increment; m_phase = std::fmod(m_phase, 1.0f); + m_nextSample = (m_phase < m_lastPhase) ? m_random.nextFloat() * 2.0f - 1.0f : m_nextSample; + m_lastPhase = m_phase; float offsetPhase = std::fmod(m_phase + m_offset, 1.0f); switch (m_shape) { case (LFOShape::Sine): @@ -52,6 +54,8 @@ float LFO::process() { return processSawDown(offsetPhase); case (LFOShape::SawUp): return processSawUp(offsetPhase); + case (LFOShape::SampleAndHold): + return processSampleAndHold(offsetPhase); default: return 0.0f; } @@ -75,4 +79,10 @@ float LFO::processSawUp(float phase) { float LFO::processSawDown(float phase) { return phase * -2.0f + 1.0f; +} + +float LFO::processSampleAndHold(float phase) { + float out = (phase < m_lastOffsetPhase) ? m_currentSample = m_nextSample : m_currentSample; + m_lastOffsetPhase = phase; + return out; } \ No newline at end of file diff --git a/Source/LFO.h b/Source/LFO.h index 85c08a7..5eb31e0 100644 --- a/Source/LFO.h +++ b/Source/LFO.h @@ -13,10 +13,12 @@ class LFO { void setOffset(float offset); void setShape(LFOShape shape); void setPhase(float phase) { m_phase = std::fmod(phase, 1.0f); } + void setSeed(int seed) { m_random.setSeed(seed); } float process(); private: LFOShape m_shape; + juce::Random m_random; float m_sampleRate = 0.0f; @@ -24,6 +26,11 @@ class LFO { float m_phase = 0.0f; float m_offset = 0.0f; float m_increment = 0.0f; + float m_lastPhase = 0.0f; + float m_lastOffsetPhase = 0.0f; + float m_currentSample = 0.0f; + float m_nextSample = 0.0f; + void setIncrement(); float processSine(float phase); @@ -31,4 +38,5 @@ class LFO { float processSquare(float phase); float processSawUp(float phase); float processSawDown(float phase); + float processSampleAndHold(float phase); }; \ No newline at end of file diff --git a/Source/Parameters.cpp b/Source/Parameters.cpp index 099d652..a2bb051 100644 --- a/Source/Parameters.cpp +++ b/Source/Parameters.cpp @@ -122,6 +122,7 @@ const juce::StringArray Parameters::lfoShapes = { "SQR", "SAW+", "SAW-", + "S&H", }; diff --git a/Source/Types.h b/Source/Types.h index a27e55b..e0374c0 100644 --- a/Source/Types.h +++ b/Source/Types.h @@ -2,4 +2,4 @@ enum Channel { Left = 0, Right = 1 }; -enum class LFOShape { Sine, Triangle, Square, SawUp, SawDown }; +enum class LFOShape { Sine, Triangle, Square, SawUp, SawDown, SampleAndHold }; From 9c84129f4ee5e32d46c6aad1a8364050f119672d Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Tue, 5 May 2026 13:42:25 +0300 Subject: [PATCH 20/31] redefine lfo tempo sync note values, set max mod rate to 8hz --- Source/Parameters.cpp | 16 +++++++++++----- Source/Sync.h | 26 ++++++++++++++++---------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/Source/Parameters.cpp b/Source/Parameters.cpp index a2bb051..864bca6 100644 --- a/Source/Parameters.cpp +++ b/Source/Parameters.cpp @@ -105,14 +105,20 @@ const juce::StringArray Parameters::delayNoteLengths = { }; const juce::StringArray Parameters::modNoteLengths = { - "1/16", "1/8", + "1/8 dot", "1/4", "1/4 dot", "1/2", + "1/2 dot", "1/1", + "1/1 dot", "2/1", + "3/1", "4/1", + "5/1", + "6/1", + "7/1", "8/1", }; @@ -270,7 +276,7 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL layout.add(std::make_unique( lowCutModRateParamID.getParamID(), "Low Cut Mod Rate", - juce::NormalisableRange(0.01f, 20.0f, 0.001f, 0.3f), + juce::NormalisableRange(0.01f, 8.0f, 0.001f, 0.3f), 0.05f, juce::AudioParameterFloatAttributes() .withStringFromValueFunction(stringFromHertz) @@ -280,7 +286,7 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL lowCutModNoteParamID, "Low Cut Mod Note", modNoteLengths, - 5 + 6 )); layout.add(std::make_unique( lowCutModTempoSyncParamID.getParamID(), @@ -326,7 +332,7 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL layout.add(std::make_unique( highCutModRateParamID.getParamID(), "High Cut Mod Rate", - juce::NormalisableRange(0.01f, 20.0f, 0.001f, 0.3f), + juce::NormalisableRange(0.01f, 8.0f, 0.001f, 0.3f), 0.05f, juce::AudioParameterFloatAttributes() .withStringFromValueFunction(stringFromHertz) @@ -336,7 +342,7 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL highCutModNoteParamID, "High Cut Mod Note", modNoteLengths, - 5 + 6 )); layout.add(std::make_unique( highCutModShapeParamID, diff --git a/Source/Sync.h b/Source/Sync.h index 5286100..7b0adf4 100644 --- a/Source/Sync.h +++ b/Source/Sync.h @@ -21,17 +21,23 @@ inline static std::array delayNoteLengthMultipliers = 4.0, // 15 = 1/1 }; -inline static std::array modNoteLengthMultipliers = +inline static std::array modNoteLengthMultipliers = { - 0.25, // 0 = 1/16 - 0.5, // 1 = 1/8 - 1.0, // 2 = 1/4 - 1.5, // 3 = 1/4 dotted - 2.0, // 4 = 1/2 - 4.0, // 5 = 1/1 - 8.0, // 6 = 2/1 - 16.0, // 7 = 4/1 - 32.0, // 8 = 8/1 + 0.5, // 0 = 1/8 + 0.75, // 1 = 1/8 dotted + 1.0, // 2 = 1/4 + 1.5, // 3 = 1/4 dotted + 2.0, // 4 = 1/2 + 3.0, // 5 = 1/2 dotted + 4.0, // 6 = 1/1 + 6.0, // 7 = 1/1 dotted + 8.0, // 8 = 2/1 + 12.0, // 9 = 3/1 + 16.0, // 10 = 4/1 + 20.0, // 11 = 5/1 + 24.0, // 12 = 6/1 + 28.0, // 13 = 7/1 + 32.0, // 14 = 8/1 }; inline float getSyncedPhase(int index, double ppqPosition) From 6600b7f0321bfe4d572807777a7f4b9a0dff484e Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Tue, 5 May 2026 13:45:58 +0300 Subject: [PATCH 21/31] set max filter q to 1.5 --- Source/Parameters.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Parameters.h b/Source/Parameters.h index 3798636..d292683 100644 --- a/Source/Parameters.h +++ b/Source/Parameters.h @@ -45,7 +45,7 @@ class Parameters { static constexpr float defaultDelayTime = 100.0f; static constexpr float minFilterFreq = 20.0f; static constexpr float maxFilterFreq = 20000.0f; - static constexpr float maxFilterQ = 2.0f; + static constexpr float maxFilterQ = 1.5f; static constexpr float minFilterQ = 0.1f; static constexpr float defaultFilterQ = 0.707f; static constexpr float defaultChorusModRate = 0.2f; From 3fd84e0949e13651db330196d4caef3bbf2d7929 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Tue, 5 May 2026 13:47:34 +0300 Subject: [PATCH 22/31] update changelog --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a32176c..eaefbe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to this project will be documented in this file. +## \[Unreleased] + +### Added + +* Filter modulation for High and Low cuts with independent LFOs + * LFO rate control + * LFO Tempo sync linked song position during playback + * LFO phase offset per filter + * LFO Shape selection + * Filter modulation depth control +* Square, Ramp Up/Down and Sample&Hold LFO Shapes + +### Changed + +* Refactored filters to StereoFilter primitive and FilterEngine class +* Set maximum resonance for low and high cut filters to 1.5 + ## \[0.1.0] - 2026-04-17 ### Added From 63e0947ceea75d8ade11fe52be13a41aea1c278c Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Tue, 5 May 2026 15:24:50 +0300 Subject: [PATCH 23/31] add selector component and lfo waveform gui selector element --- CMakeLists.txt | 1 + Source/Parameters.cpp | 4 ++-- Source/PluginEditor.cpp | 24 ++++++--------------- Source/PluginEditor.h | 7 +++--- Source/Selector.cpp | 47 +++++++++++++++++++++++++++++++++++++++++ Source/Selector.h | 38 +++++++++++++++++++++++++++++++++ 6 files changed, 98 insertions(+), 23 deletions(-) create mode 100644 Source/Selector.cpp create mode 100644 Source/Selector.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 135f24a..7c2427b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,7 @@ set_target_properties(${project_targets} PROPERTIES CXX_EXTENSIONS OFF) target_sources(viive PRIVATE Source/FilterEngine.cpp Source/Tempo.cpp + Source/Selector.cpp Source/SyncButton.cpp Source/StereoDelay.cpp Source/StereoFilter.cpp diff --git a/Source/Parameters.cpp b/Source/Parameters.cpp index 864bca6..984b92a 100644 --- a/Source/Parameters.cpp +++ b/Source/Parameters.cpp @@ -126,8 +126,8 @@ const juce::StringArray Parameters::lfoShapes = { "SIN", "TRI", "SQR", - "SAW+", - "SAW-", + "SW+", + "SW-", "S&H", }; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index e601250..087d8e8 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -17,17 +17,7 @@ ViiveAudioProcessorEditor::ViiveAudioProcessorEditor(ViiveAudioProcessor& p) m_delayGroup.addAndMakeVisible(m_offsetKnob); m_delayGroup.addAndMakeVisible(m_tempoSyncLButton); m_delayGroup.addAndMakeVisible(m_tempoSyncRButton); - addAndMakeVisible(m_delayModeLabel); - m_delayModeLabel.setText("Delay Mode:", juce::NotificationType::dontSendNotification); - m_delayModeLabel.setEditable(false); - m_delayModeLabel.setSize(80, 25); addAndMakeVisible(m_modeSelector); - m_modeSelector.addItemList(Parameters::delayModes, 1); - m_modeSelector.setEditableText(false); - m_modeSelector.setSize(130, 25); - m_modeSelector.onChange = [this] { modeSelectorChanged();}; - auto* param = dynamic_cast(m_audioProcessor.apvts.getParameter(delayModeParamID.getParamID())); - m_modeSelector.setSelectedId(param->getIndex() + 1, juce::dontSendNotification); addAndMakeVisible(m_delayGroup); m_filterModGroup.setText("Modulation"); @@ -43,6 +33,8 @@ ViiveAudioProcessorEditor::ViiveAudioProcessorEditor(ViiveAudioProcessor& p) m_filterModGroup.addAndMakeVisible(m_highCutModPhaseKnob); m_filterModGroup.addAndMakeVisible(m_lowCutModTempoSyncButton); m_filterModGroup.addAndMakeVisible(m_highCutModTempoSyncButton); + m_filterModGroup.addAndMakeVisible(m_lowCutModShapeSelector); + m_filterModGroup.addAndMakeVisible(m_highCutModShapeSelector); m_filterGroup.setText("Filter"); m_filterGroup.setTextLabelPosition(juce::Justification::horizontallyCentred); @@ -88,7 +80,7 @@ ViiveAudioProcessorEditor::ViiveAudioProcessorEditor(ViiveAudioProcessor& p) #if JUCE_DEBUG setSize(770, 670); #else - setSize(770, 480); + setSize(770, 490); #endif } @@ -133,8 +125,7 @@ void ViiveAudioProcessorEditor::resized() m_tempoSyncRButton.setCentrePosition(m_delayTimeRKnob.getRight() - 35, m_delayTimeRKnob.getBottom() + 15); m_feedbackKnob.setTopLeftPosition(m_delayTimeRKnob.getRight() + 20, 20); - m_delayModeLabel.setTopLeftPosition(10, y); - m_modeSelector.setTopLeftPosition(95, y); + m_modeSelector.setTopLeftPosition(10, y); m_offsetKnob.setTopLeftPosition(m_feedbackKnob.getRight() + 20, 20); m_lowCutFreqKnob.setTopLeftPosition(20, 20); @@ -144,6 +135,7 @@ void ViiveAudioProcessorEditor::resized() m_lowCutModDepthKnob.setTopLeftPosition(m_lowCutModRateKnob.getRight() + 10, 20); m_lowCutModPhaseKnob.setTopLeftPosition(m_lowCutModDepthKnob.getRight() + 10, 20); m_lowCutModTempoSyncButton.setTopLeftPosition(20, m_lowCutModRateKnob.getBottom() + 5); + m_lowCutModShapeSelector.setTopLeftPosition(m_lowCutModDepthKnob.getX(), m_lowCutModTempoSyncButton.getY()); m_highCutFreqKnob.setTopLeftPosition(m_lowCutQKnob.getRight() + 20, 20); m_highCutQKnob.setTopLeftPosition(m_highCutFreqKnob.getRight() + 20, 20); m_highCutModPhaseKnob.setTopRightPosition(m_highCutQKnob.getRight(), 20); @@ -151,6 +143,7 @@ void ViiveAudioProcessorEditor::resized() m_highCutModRateKnob.setTopRightPosition(m_highCutModDepthKnob.getX() - 10, 20); m_highCutModNoteKnob.setTopLeftPosition(m_highCutModRateKnob.getX(), m_highCutModRateKnob.getY()); m_highCutModTempoSyncButton.setTopLeftPosition(m_highCutModRateKnob.getX(), m_highCutModRateKnob.getBottom() + 5); + m_highCutModShapeSelector.setTopLeftPosition(m_highCutModDepthKnob.getX(), m_highCutModTempoSyncButton.getY()); m_stereoKnob.setTopLeftPosition(20, 20); m_mixKnob.setTopLeftPosition(m_stereoKnob.getRight() + 20, 20); m_gainKnob.setTopLeftPosition(m_mixKnob.getRight() + 20, 20); @@ -191,8 +184,3 @@ void ViiveAudioProcessorEditor::parameterChanged(const juce::String& paramID, fl } } -void ViiveAudioProcessorEditor::modeSelectorChanged() { - int selectedID = m_modeSelector.getSelectedId(); - auto* param = dynamic_cast(m_audioProcessor.apvts.getParameter(delayModeParamID.getParamID())); - param->setValueNotifyingHost(param->convertTo0to1(selectedID - 1)); -} \ No newline at end of file diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index ba28ade..f7e2816 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -5,6 +5,7 @@ #include "Parameters.h" #include "RotaryKnob.h" #include "SyncButton.h" +#include "Selector.h" #include "LevelMeter.h" #if JUCE_DEBUG #include "LFOScope.h" @@ -62,8 +63,9 @@ class ViiveAudioProcessorEditor : public juce::AudioProcessorEditor, SyncButton m_lowCutModTempoSyncButton{ "Sync", m_audioProcessor.apvts, lowCutModTempoSyncParamID.getParamID() }; SyncButton m_highCutModTempoSyncButton{ "Sync", m_audioProcessor.apvts, highCutModTempoSyncParamID.getParamID() }; - juce::Label m_delayModeLabel; - juce::ComboBox m_modeSelector; + Selector m_modeSelector{ "Mode", m_audioProcessor.apvts, delayModeParamID.getParamID(), Parameters::delayModes, 130 }; + Selector m_lowCutModShapeSelector{ "", m_audioProcessor.apvts, lowCutModShapeParamID.getParamID(), Parameters::lfoShapes, 60, 12.0f }; + Selector m_highCutModShapeSelector{ "", m_audioProcessor.apvts, highCutModShapeParamID.getParamID(), Parameters::lfoShapes, 60, 12.0f }; juce::Label m_modulationLabel; @@ -76,7 +78,6 @@ class ViiveAudioProcessorEditor : public juce::AudioProcessorEditor, #endif virtual void parameterChanged(const juce::String& paramID, float newValue) override; - void modeSelectorChanged(); void updateSyncedKnobs(bool syncLActive, bool syncRActive, bool lowCutSyncActive, bool highCutSyncActive); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ViiveAudioProcessorEditor) diff --git a/Source/Selector.cpp b/Source/Selector.cpp new file mode 100644 index 0000000..5953824 --- /dev/null +++ b/Source/Selector.cpp @@ -0,0 +1,47 @@ +#include "Selector.h" + +Selector::Selector(const juce::String& labelText, + juce::AudioProcessorValueTreeState& apvts, + const juce::ParameterID& parameterID, + const juce::StringArray& items, + int width, + float fontSize) + : m_hasLabel(labelText.isNotEmpty()) +{ + if (fontSize > 0.0f) + { + m_laf.fontSize = fontSize; + m_comboBox.setLookAndFeel(&m_laf); + } + + m_comboBox.addItemList(items, 1); + m_comboBox.setEditableText(false); + addAndMakeVisible(m_comboBox); + + m_attachment = std::make_unique( + apvts, parameterID.getParamID(), m_comboBox); + + if (m_hasLabel) + { + m_label.setText(labelText, juce::NotificationType::dontSendNotification); + m_label.setFont(juce::Font(juce::FontOptions().withHeight(13.0f))); + m_label.setJustificationType(juce::Justification::horizontallyCentred); + m_label.setBorderSize(juce::BorderSize(0)); + addAndMakeVisible(m_label); + } + + setSize(width, (m_hasLabel ? s_labelH : 0) + s_comboH); +} + +Selector::~Selector() +{ + m_comboBox.setLookAndFeel(nullptr); +} + +void Selector::resized() +{ + const int comboY = m_hasLabel ? s_labelH : 0; + if (m_hasLabel) + m_label.setBounds(0, 0, getWidth(), s_labelH); + m_comboBox.setBounds(0, comboY, getWidth(), s_comboH); +} diff --git a/Source/Selector.h b/Source/Selector.h new file mode 100644 index 0000000..1dd969b --- /dev/null +++ b/Source/Selector.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +class Selector : public juce::Component +{ +public: + Selector(const juce::String& labelText, + juce::AudioProcessorValueTreeState& apvts, + const juce::ParameterID& parameterID, + const juce::StringArray& items, + int width = 60, + float fontSize = 0.0f); + ~Selector() override; + + void resized() override; + +private: + static constexpr int s_labelH = 14; + static constexpr int s_comboH = 20; + + struct SelectorLookAndFeel : public juce::LookAndFeel_V4 + { + float fontSize = 0.0f; + juce::Font getComboBoxFont(juce::ComboBox&) override + { + return juce::Font(juce::FontOptions().withHeight(fontSize)); + } + }; + + SelectorLookAndFeel m_laf; + juce::Label m_label; + juce::ComboBox m_comboBox; + std::unique_ptr m_attachment; + bool m_hasLabel; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Selector) +}; From b54070bf54540c6cdefffdcd326dd1665f909a57 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Tue, 5 May 2026 15:25:06 +0300 Subject: [PATCH 24/31] fix tempo sync bug in s&h lfo --- Source/LFO.cpp | 5 +++++ Source/LFO.h | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Source/LFO.cpp b/Source/LFO.cpp index 5cdba11..e577899 100644 --- a/Source/LFO.cpp +++ b/Source/LFO.cpp @@ -29,6 +29,11 @@ void LFO::setShape(LFOShape shape) { m_shape = shape; } +void LFO::setPhase(float phase) { + m_phase = std::fmod(phase, 1.0f); + m_lastOffsetPhase = std::fmod(m_phase + m_offset, 1.0f); +} + void LFO::setIncrement() { if (m_sampleRate > 0.0f) { m_increment = m_frequency / m_sampleRate; diff --git a/Source/LFO.h b/Source/LFO.h index 5eb31e0..e827f5c 100644 --- a/Source/LFO.h +++ b/Source/LFO.h @@ -12,7 +12,7 @@ class LFO { void setFrequency(float frequency); void setOffset(float offset); void setShape(LFOShape shape); - void setPhase(float phase) { m_phase = std::fmod(phase, 1.0f); } + void setPhase(float phase); void setSeed(int seed) { m_random.setSeed(seed); } float process(); From 259e1d8817661f2a707d503b228026d00c279cb5 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Tue, 5 May 2026 21:50:48 +0300 Subject: [PATCH 25/31] add lfo value indicators to filter mod section --- Source/DelayEngine.h | 3 +++ Source/FilterEngine.cpp | 3 +++ Source/FilterEngine.h | 6 ++++++ Source/LFOIndicator.h | 35 +++++++++++++++++++++++++++++++++++ Source/PluginEditor.cpp | 11 +++++++++++ Source/PluginEditor.h | 4 ++++ Source/PluginProcessor.h | 2 ++ 7 files changed, 64 insertions(+) create mode 100644 Source/LFOIndicator.h diff --git a/Source/DelayEngine.h b/Source/DelayEngine.h index 1108f0f..f360c71 100644 --- a/Source/DelayEngine.h +++ b/Source/DelayEngine.h @@ -32,6 +32,9 @@ class DelayEngine void setDelayMode(const int modeIndex); void updateSyncedModulators(const double ppqPosition); + std::atomic& getLowCutLfoValue() { return m_filterEngine.getLowCutLfoValue(); } + std::atomic& getHighCutLfoValue() { return m_filterEngine.getHighCutLfoValue(); } + #if JUCE_DEBUG LFOScopeBuffer& getLFOScopeBuffer() { return m_filterEngine.m_scopeBuffer; } #endif diff --git a/Source/FilterEngine.cpp b/Source/FilterEngine.cpp index 394f9ad..1514adf 100644 --- a/Source/FilterEngine.cpp +++ b/Source/FilterEngine.cpp @@ -149,6 +149,9 @@ void FilterEngine::processSample(const float& inL, const float& inR, float& outL hcL = m_highCutModInvert ? -hcL : hcL; hcR = m_highCutModInvert ? -hcR : hcR; + m_lowCutLfoValue.store(lcL, std::memory_order_relaxed); + m_highCutLfoValue.store(hcL, std::memory_order_relaxed); + #if JUCE_DEBUG if (++m_scopeCounter >= scopeDownsample) { m_scopeCounter = 0; diff --git a/Source/FilterEngine.h b/Source/FilterEngine.h index 009f064..a298813 100644 --- a/Source/FilterEngine.h +++ b/Source/FilterEngine.h @@ -25,6 +25,9 @@ class FilterEngine void updateLFOs(const double ppqPosition) noexcept; + std::atomic& getLowCutLfoValue() { return m_lowCutLfoValue; } + std::atomic& getHighCutLfoValue() { return m_highCutLfoValue; } + #if JUCE_DEBUG LFOScopeBuffer m_scopeBuffer; #endif @@ -57,6 +60,9 @@ class FilterEngine bool m_highCutModInvert = false; + std::atomic m_lowCutLfoValue { 0.0f }; + std::atomic m_highCutLfoValue { 0.0f }; + float m_lowCutSmoothed[2] = { Parameters::minFilterFreq, Parameters::minFilterFreq }; float m_highCutSmoothed[2] = { Parameters::maxFilterFreq, Parameters::maxFilterFreq }; float m_cutoffSmoothCoeff = 0.0f; diff --git a/Source/LFOIndicator.h b/Source/LFOIndicator.h new file mode 100644 index 0000000..ede16fb --- /dev/null +++ b/Source/LFOIndicator.h @@ -0,0 +1,35 @@ +#pragma once +#include + +class LFOIndicator : public juce::Component, private juce::Timer +{ +public: + LFOIndicator(std::atomic& value) : m_value(value) + { + startTimerHz(60); + } + + ~LFOIndicator() override { stopTimer(); } + + void paint(juce::Graphics& g) override + { + auto bounds = getLocalBounds().toFloat().reduced(1.0f); + float v = m_value.load(); + juce::Colour fill; + if (v >= 0.0f) + fill = juce::Colours::black.interpolatedWith(juce::Colours::green, v); + else + fill = juce::Colours::black.interpolatedWith(juce::Colours::red, -v); + + g.setColour(juce::Colours::darkgrey.darker(0.5f)); + g.drawEllipse(bounds, 1.0f); + g.setColour(fill); + g.fillEllipse(bounds.reduced(1.0f)); + } + +private: + std::atomic& m_value; + void timerCallback() override { repaint(); } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LFOIndicator) +}; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 087d8e8..b5fc8f3 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -35,6 +35,8 @@ ViiveAudioProcessorEditor::ViiveAudioProcessorEditor(ViiveAudioProcessor& p) m_filterModGroup.addAndMakeVisible(m_highCutModTempoSyncButton); m_filterModGroup.addAndMakeVisible(m_lowCutModShapeSelector); m_filterModGroup.addAndMakeVisible(m_highCutModShapeSelector); + m_filterModGroup.addAndMakeVisible(m_lowCutLfoIndicator); + m_filterModGroup.addAndMakeVisible(m_highCutLfoIndicator); m_filterGroup.setText("Filter"); m_filterGroup.setTextLabelPosition(juce::Justification::horizontallyCentred); @@ -136,6 +138,11 @@ void ViiveAudioProcessorEditor::resized() m_lowCutModPhaseKnob.setTopLeftPosition(m_lowCutModDepthKnob.getRight() + 10, 20); m_lowCutModTempoSyncButton.setTopLeftPosition(20, m_lowCutModRateKnob.getBottom() + 5); m_lowCutModShapeSelector.setTopLeftPosition(m_lowCutModDepthKnob.getX(), m_lowCutModTempoSyncButton.getY()); + + constexpr int indicatorSize = 8; + m_lowCutLfoIndicator.setSize(indicatorSize, indicatorSize); + m_lowCutLfoIndicator.setCentrePosition(m_lowCutModPhaseKnob.getBounds().getCentreX(), + m_lowCutModPhaseKnob.getBottom() + 15); m_highCutFreqKnob.setTopLeftPosition(m_lowCutQKnob.getRight() + 20, 20); m_highCutQKnob.setTopLeftPosition(m_highCutFreqKnob.getRight() + 20, 20); m_highCutModPhaseKnob.setTopRightPosition(m_highCutQKnob.getRight(), 20); @@ -144,6 +151,10 @@ void ViiveAudioProcessorEditor::resized() m_highCutModNoteKnob.setTopLeftPosition(m_highCutModRateKnob.getX(), m_highCutModRateKnob.getY()); m_highCutModTempoSyncButton.setTopLeftPosition(m_highCutModRateKnob.getX(), m_highCutModRateKnob.getBottom() + 5); m_highCutModShapeSelector.setTopLeftPosition(m_highCutModDepthKnob.getX(), m_highCutModTempoSyncButton.getY()); + + m_highCutLfoIndicator.setSize(indicatorSize, indicatorSize); + m_highCutLfoIndicator.setCentrePosition(m_highCutModPhaseKnob.getBounds().getCentreX(), + m_highCutModPhaseKnob.getBottom() + 15); m_stereoKnob.setTopLeftPosition(20, 20); m_mixKnob.setTopLeftPosition(m_stereoKnob.getRight() + 20, 20); m_gainKnob.setTopLeftPosition(m_mixKnob.getRight() + 20, 20); diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index f7e2816..f1a3024 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -7,6 +7,7 @@ #include "SyncButton.h" #include "Selector.h" #include "LevelMeter.h" +#include "LFOIndicator.h" #if JUCE_DEBUG #include "LFOScope.h" #endif @@ -67,6 +68,9 @@ class ViiveAudioProcessorEditor : public juce::AudioProcessorEditor, Selector m_lowCutModShapeSelector{ "", m_audioProcessor.apvts, lowCutModShapeParamID.getParamID(), Parameters::lfoShapes, 60, 12.0f }; Selector m_highCutModShapeSelector{ "", m_audioProcessor.apvts, highCutModShapeParamID.getParamID(), Parameters::lfoShapes, 60, 12.0f }; + LFOIndicator m_lowCutLfoIndicator { m_audioProcessor.getLowCutLfoValue() }; + LFOIndicator m_highCutLfoIndicator { m_audioProcessor.getHighCutLfoValue() }; + juce::Label m_modulationLabel; juce::HashMap m_linkedParams; diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index b66a1df..b0b35e0 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -46,6 +46,8 @@ class ViiveAudioProcessor : public juce::AudioProcessor std::atomic& getOutputLevelL() { return m_outputLevelL; } std::atomic& getOutputLevelR() { return m_outputLevelR; } + std::atomic& getLowCutLfoValue() { return m_delayEngine.getLowCutLfoValue(); } + std::atomic& getHighCutLfoValue() { return m_delayEngine.getHighCutLfoValue(); } #if JUCE_DEBUG LFOScopeBuffer& getLFOScopeBuffer() { return m_delayEngine.getLFOScopeBuffer(); } From 28497bb4f6b540a0c9bd6f2705b1bd937aa03c5f Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Tue, 5 May 2026 21:59:43 +0300 Subject: [PATCH 26/31] add high cut lfo invert toggle to gui --- Source/PluginEditor.cpp | 3 +++ Source/PluginEditor.h | 1 + 2 files changed, 4 insertions(+) diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index b5fc8f3..8e6831e 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -37,6 +37,7 @@ ViiveAudioProcessorEditor::ViiveAudioProcessorEditor(ViiveAudioProcessor& p) m_filterModGroup.addAndMakeVisible(m_highCutModShapeSelector); m_filterModGroup.addAndMakeVisible(m_lowCutLfoIndicator); m_filterModGroup.addAndMakeVisible(m_highCutLfoIndicator); + m_filterModGroup.addAndMakeVisible(m_highCutModInvertButton); m_filterGroup.setText("Filter"); m_filterGroup.setTextLabelPosition(juce::Justification::horizontallyCentred); @@ -155,6 +156,8 @@ void ViiveAudioProcessorEditor::resized() m_highCutLfoIndicator.setSize(indicatorSize, indicatorSize); m_highCutLfoIndicator.setCentrePosition(m_highCutModPhaseKnob.getBounds().getCentreX(), m_highCutModPhaseKnob.getBottom() + 15); + m_highCutModInvertButton.setCentrePosition(m_highCutLfoIndicator.getBounds().getCentreX() + 25, + m_highCutLfoIndicator.getBounds().getCentreY()); m_stereoKnob.setTopLeftPosition(20, 20); m_mixKnob.setTopLeftPosition(m_stereoKnob.getRight() + 20, 20); m_gainKnob.setTopLeftPosition(m_mixKnob.getRight() + 20, 20); diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index f1a3024..c1f2540 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -70,6 +70,7 @@ class ViiveAudioProcessorEditor : public juce::AudioProcessorEditor, LFOIndicator m_lowCutLfoIndicator { m_audioProcessor.getLowCutLfoValue() }; LFOIndicator m_highCutLfoIndicator { m_audioProcessor.getHighCutLfoValue() }; + SyncButton m_highCutModInvertButton{ juce::String::fromUTF8("\xC3\x98"), m_audioProcessor.apvts, highCutModInvertParamID.getParamID(), 20}; juce::Label m_modulationLabel; From 87f6e57051cdae58fba572c365801ec0f0c76cef Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Tue, 5 May 2026 22:47:48 +0300 Subject: [PATCH 27/31] move delay mode selector label to left side of menu --- Source/PluginEditor.cpp | 2 +- Source/PluginEditor.h | 2 +- Source/Selector.cpp | 28 ++++++++++++++++++---------- Source/Selector.h | 5 ++++- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 8e6831e..e03d99d 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -102,7 +102,7 @@ void ViiveAudioProcessorEditor::paint (juce::Graphics& g) g.fillAll(juce::Colours::darkgrey); g.setColour(juce::Colours::white); g.setFont(12.0f); - g.drawText(JucePlugin_VersionString, getLocalBounds().reduced(5), juce::Justification::topRight); + g.drawText("viive " JucePlugin_VersionString, getLocalBounds().reduced(5), juce::Justification::topRight); } void ViiveAudioProcessorEditor::resized() diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index c1f2540..8f83751 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -64,7 +64,7 @@ class ViiveAudioProcessorEditor : public juce::AudioProcessorEditor, SyncButton m_lowCutModTempoSyncButton{ "Sync", m_audioProcessor.apvts, lowCutModTempoSyncParamID.getParamID() }; SyncButton m_highCutModTempoSyncButton{ "Sync", m_audioProcessor.apvts, highCutModTempoSyncParamID.getParamID() }; - Selector m_modeSelector{ "Mode", m_audioProcessor.apvts, delayModeParamID.getParamID(), Parameters::delayModes, 130 }; + Selector m_modeSelector{ "Delay Mode:", m_audioProcessor.apvts, delayModeParamID.getParamID(), Parameters::delayModes, 215, 0.0f, 85, 25 }; Selector m_lowCutModShapeSelector{ "", m_audioProcessor.apvts, lowCutModShapeParamID.getParamID(), Parameters::lfoShapes, 60, 12.0f }; Selector m_highCutModShapeSelector{ "", m_audioProcessor.apvts, highCutModShapeParamID.getParamID(), Parameters::lfoShapes, 60, 12.0f }; diff --git a/Source/Selector.cpp b/Source/Selector.cpp index 5953824..b9aa86c 100644 --- a/Source/Selector.cpp +++ b/Source/Selector.cpp @@ -5,8 +5,10 @@ Selector::Selector(const juce::String& labelText, const juce::ParameterID& parameterID, const juce::StringArray& items, int width, - float fontSize) - : m_hasLabel(labelText.isNotEmpty()) + float fontSize, + int leftLabelWidth, + int height) + : m_hasLabel(labelText.isNotEmpty()), m_leftLabelWidth(leftLabelWidth) { if (fontSize > 0.0f) { @@ -24,13 +26,11 @@ Selector::Selector(const juce::String& labelText, if (m_hasLabel) { m_label.setText(labelText, juce::NotificationType::dontSendNotification); - m_label.setFont(juce::Font(juce::FontOptions().withHeight(13.0f))); - m_label.setJustificationType(juce::Justification::horizontallyCentred); - m_label.setBorderSize(juce::BorderSize(0)); addAndMakeVisible(m_label); } - setSize(width, (m_hasLabel ? s_labelH : 0) + s_comboH); + const int autoH = (m_hasLabel && m_leftLabelWidth == 0 ? s_labelH : 0) + s_comboH; + setSize(width, height > 0 ? height : autoH); } Selector::~Selector() @@ -40,8 +40,16 @@ Selector::~Selector() void Selector::resized() { - const int comboY = m_hasLabel ? s_labelH : 0; - if (m_hasLabel) - m_label.setBounds(0, 0, getWidth(), s_labelH); - m_comboBox.setBounds(0, comboY, getWidth(), s_comboH); + if (m_hasLabel && m_leftLabelWidth > 0) + { + m_label.setBounds(0, 0, m_leftLabelWidth, getHeight()); + m_comboBox.setBounds(m_leftLabelWidth, 0, getWidth() - m_leftLabelWidth, getHeight()); + } + else + { + const int comboY = m_hasLabel ? s_labelH : 0; + if (m_hasLabel) + m_label.setBounds(0, 0, getWidth(), s_labelH); + m_comboBox.setBounds(0, comboY, getWidth(), s_comboH); + } } diff --git a/Source/Selector.h b/Source/Selector.h index 1dd969b..07702ac 100644 --- a/Source/Selector.h +++ b/Source/Selector.h @@ -10,7 +10,9 @@ class Selector : public juce::Component const juce::ParameterID& parameterID, const juce::StringArray& items, int width = 60, - float fontSize = 0.0f); + float fontSize = 0.0f, + int leftLabelWidth = 0, + int height = 0); ~Selector() override; void resized() override; @@ -33,6 +35,7 @@ class Selector : public juce::Component juce::ComboBox m_comboBox; std::unique_ptr m_attachment; bool m_hasLabel; + int m_leftLabelWidth = 0; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Selector) }; From 7624f1b4919de70a24415e7bdfade5f8d4a64fb9 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 6 May 2026 08:36:30 +0300 Subject: [PATCH 28/31] update changelog, readme, cmakelists version number --- CHANGELOG.md | 12 +++++++++--- CMakeLists.txt | 3 ++- README.md | 8 +++++++- viive.png | Bin 32184 -> 37335 bytes 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaefbe5..11e5d37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,23 +2,29 @@ All notable changes to this project will be documented in this file. -## \[Unreleased] +## \[0.2.0-beta.1] - 2026-05-06 ### Added * Filter modulation for High and Low cuts with independent LFOs * LFO rate control - * LFO Tempo sync linked song position during playback + * LFO Tempo sync linked to song position during playback * LFO phase offset per filter * LFO Shape selection * Filter modulation depth control -* Square, Ramp Up/Down and Sample&Hold LFO Shapes + * Phase invert switch for high cut lfo (on by default) +* Square, Saw Up/Down and Sample&Hold LFO Shapes +* Indicator lights for both LFOs to get visual idea of its current value ### Changed * Refactored filters to StereoFilter primitive and FilterEngine class * Set maximum resonance for low and high cut filters to 1.5 +### Known limitations + +* Tempo synced LFOs might sync incorrectly to song playback positions in DAW on extreme settings (very high or very low BPMs) + ## \[0.1.0] - 2026-04-17 ### Added diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c2427b..9f74f5b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.22) -project(viive VERSION 0.1.0) +project(viive VERSION 0.2.0) ### Dependency versions ### @@ -41,6 +41,7 @@ set(VST3_DEV_COPY_DIR "" CACHE PATH "If set, copies the Debug VST3 build here af ### Plugin Project Setup ### juce_add_plugin(viive + VERSION 0.2.beta-1 FORMATS VST3 Standalone PRODUCT_NAME "viive" COMPANY_NAME "trsctr" diff --git a/README.md b/README.md index 60ac2d1..7537da0 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Currently developed and tested on Windows (JUCE AudioPluginHost, Ableton Live Su - Mix and feedback controls with feedback stabilization (0-140%) - Stereo width control with mid/side processing - Sculpting low/high-cut filters with adjustable resonance on delayed signal / feedback loop +- Filter modulation section with separate LFOs for high and low cut filters with tempo sync, adjustable rate, modulation depth, stereo phase offset and selectable shapes: Sine, Triangle, Square, Saw Up/Down, Sample & Hold - Dual-voice chorus effect in feedback loop with intensity, modulation rate, and modulation depth controls - Feedback loop protection: HPF, compressor, soft clipping @@ -31,8 +32,11 @@ Download the zip for your platform, extract it, and copy `viive.vst3` to your VS Then rescan plugins in your DAW. +NOTE: MacOS build is untested and unsigned. May be blocked by Gatekeeper on +newer macOS versions and cannot be guaranteed to work without +code signing and notarization. + ## Potential future features -- LFO/modulation for general parameters (such as filter cutoffs) - Additional effects in feedback loop (bitcrush, phaser) - Preset manager - Single/dual delay mode switch @@ -57,6 +61,8 @@ cmake --build Build --config Release JUCE 8.0.11 is downloaded automatically during configuration. +NOTE: Linux builds are reportedly possible but untested and undocumented. + ### Output The VST3 will be produced under `Build/viive_artefacts/Release/VST3/`. diff --git a/viive.png b/viive.png index a3a8a19ab936a55009afa6521f90b6b8621e4002..a811731a67f2cfcdab78b078bc9c8cd3ca3f4927 100644 GIT binary patch literal 37335 zcmd432{e>(-#1PwNzpD@rd1-%=-w)BC37THdWLLl)|ZXfLWn=E5wXWecY z#rN71k7pLSHqj@dL${pU?fGSE-L1O4uCKPdQn7IVq9V1g;hf2xku4feZ@zrZ>=0Vd6pGn2_n-kli>S41>a?{K}d6Ot4(5@@Ta#S&9 z=NfLQCv{UgaXFj#Z1YUHH(eEi<+&GbO2=^7-%rG;^N386c6@9%dzOM4^rz*1gZ_Es zR(^t9vZibQy`ZHActVj&H?%=cH?ZfyKv`$b?V@AQEnkBT`aK*U1K8yZ>@$fAgF#&6 zV<5{C-Gm-brc1`xrvwB;Hza$WSuBHX0cWVXc3u+3`>fD1!j4x%q>`5#VQaM3XpirE zOBNP{N!d=Ii*--r5a;S-Dr+=r0@^Gw!3^y3!eq}^B=Pyf$%)o9od$cdV~vCCX}5RX zYp>RJHDcH8;~@hXTh%bF9_`rQr>FM%lIHrELJyxjIU*A}Aq5XS zxpA0Hf$|P42*Ay%^y9O-o6Ie2I++C79hZFdFVzLgy<)}tW3(kmB?@wTduU|Q{cPyl z=oeU4r$t8c63lP@ZZn1*93nemC;b@MSknt%50JvB@|d%W;-H8wAAY}U-=J=Typ&*T z`q=Cu1q)lYNMY44JcsjY#C}>f^BP8tI9&_XBIjVS^?DDJ-LAf?g%#HaQTI-&ZFn?H zeB`ziE)m*c*&^yi1oa}-Yr{MYSZWRk8!cf(FKuX2FiCpyLs+kk#nq5YZmr4*89EWI z1mQ%wNYZ`?hcw4y18lfM=T9iqXs=n;<-au~=bz*eglb_YS5(AtaVk5XUB-sIsv(AY za&syKkCQ)0)*Z%>qvZswBHr7BjWh;I86g_PZIdfah78*jZzV^G$Ol~3W-3ECZ;sB$ zi9H5(f$I1e3oh1q-M@Ybiw(n86uC2o^bkQiaXJASWwuwEQM>*QeeL)uIl&un}r~WbUW9-?UxTOt^B}$9-ulAzC{nw(j107yzc)h3(EH~b~y|2c>!A4OT zsFU0BW0V|hgCbzYiLC|ihXEjvWAC-zrkP;KiB)6V5@jCpZTLWLMNHGVt;sr*A(8_o zX^iM%x)_sHU>=I+cnG;ozQo9u#rx~@NCIg+o~B~JHBI&;AHQ6+n{@T zU8ZHz^|mI2nBwG0MLSC|o;N2>2$dgKJmAggChzLLdBAWO{>U@!o#H-QQLiiB?+7Rd zORY;=wheuWkQ<-l+@DqDgkrftiusW8UpH+69GayuJeQQtdJMp*gR%P;i?nCaU5nML zzAX3ZElzh_=Z}MkpXet9WyzYmq7Mqg$=fDa4t*+yZUm7^Q0>aa3KZoJOtu~Bm9NC9 ze#A|7gW735Ke}Cj=IEEy-)%mh8P}qO7(rXL*~Yr%QS`nZByacWjh+oM7bE>1Z6qcQ z)N8NVE4r=(^^11`p#%Sq?UzeBIq5PjQkr@Nj%L1M|lV> zW_~SEZXrx}Z%Z7zM_7*5XV$0jO{eOKI8enkzgUt7mk|2>05 zoh4BB9VBc3_n+)EA*L2S2!9-V@;E9)l2+l z2MhNVg!oMquY0s5lb(OdnCDRqD3(&EKYmU$Flotd9j$dKIjj&*QRAq-WSjM=+a zbSL}Z;v{O^(BI%)>u5bvB>jVfq*m525)iYZ69e3SI zc^IyQ@C0VHC}d$zfN9cc`=`Pv1FXnxcy=e$cyZpqXKMhSlU$ti_Nm@RXy1`w9N zg)q{t022NrbD(l*Jh8Rs37)sC`$7#kefqS|jCJ!ZXJsNd(PXX)a-gh;dRAC;>+bH8 z<9E(!=Mx2GON?5{fW8+jN1cH|GKz>R!Cas}8BT%fU+2`HiO)X|wY|VdeMCfa3`*ie zL@Z6$zyxntALJ-LyPrJiTX$j+%s{S=SYpHg@1gG5wL`^)?n7ULkCZeNYyg%Y4s}|< zjW+{j7}<9!JTnD*PDa!)XwsMBu0~_gobJ!^U2$Z#UTBcQ!zY>BIghd05oFrlX|kRz zMHwNWb1^m8K8>wK<>@tmxl7kut1j$7zx(WAPilJ8g*T4`pSX&k8{)}m?)z3BhV{#~ zJgm(dMHPm#rhSAT^xUlEU&t*U9yD&K%cSsNo*G!$iMf$TKLN~`?7?jFTO5L1Iy zLkL(<&`m_=D<3G(VK*{Ky4U&ko63;2HYV_igYh*0-G@TX_LZFWt?4fiXrx|l<=^}k zh83fFSizZ+C9s(Sh#dRmY4pehUBOvBg6@5(z9*lHycx*9hh5eRcdDY-O8_MU z6A4&3!J+AfGqMjaaq34aSzT5-i<2Vj5(i;f1Z4EggnHgE?xBf}5JLjc9P~0Xt=4c2 z_*$BhId$*}*{mPUF{F&7)nfZSpuC2Uu;r6ari2_SBe=esTNb$^G|_!u$uY8P}jk0^?`5`pa~Vb<~>b(EAN8%(>;lh=Vax)71(-$&z#{g7)GPIOJCI zpCZvP7R{X=d>g{4Txc_DrQQiVPXrebJso)XSO;3Oe+axiudrEiOXqi~eB#MH+_^pP zW|`l+CHH4-i*Vsw5v@xHL$>Ci?*V_RxTVN!`jY%w-*Bv7!_8&|F!SC86~F}jOOhse z)|M10%l*#E(K<})QzvrE<}XvLa<)`NLtulaC@p!z9w{tZw}Ge%XMK)^t>5?3wI?K2 z+}_8Ha)7kpX{Pi1CLs5b8lden6Hx=p;{(Hz*dHq{clcaA7W7kD3Emq38BNzy878=A zgMOzy$FDS(3;AnBuK1(tVNn3+4{oT_w+IryuiW+N?_GNyVjW3aKXHm zF}u%;0nXk3A0zJ;=!8lLE?l@_`hFA!&dGdpEit^M8hq~5Q7;~XqmY_q&sp381)b+L zoqvq936MW03Z`;d87*@u;2g3Z4^z%h41q(lc0dJcyzSRIRd@j#6@q0NiQ)%3$Q&*Akx(__fZQ41mLH+S#p&L5PZ^d?Q5OE(g9#30au~}TZyN6} z^>msYtd#5a38W8JYG<`}1E5`b^jt7P)& z2}usI{a&flBm?NOLR|z1&-^P?0~yt#*D&Z8T+V2;168x_2+ycob$DssFG56A;sSkH zk?fo)%x2OFl$oMR?&54TTd#TXz3yTDA|-md;IQUN?yhpsd-Yw zm2L8LBL$U1a+#A1$YKfl(b4;4`t3;{%ils$F%m=m1+~OHVon+@AJS}Vx?CawEWayv zJ=jCra)JqrC<|(`uJR5dTPEs{gX{dKM$I2MZS*M}F^D=vs= zso)hX>w`q~`d(GtAv*Qw+18}nO70VH)?N<95P2zYLQH#uLR}~!aW}uR_xPunCxu*51t7p2B(1&}Fdcp;3}ko>e37UDTGI>y z`Y!k775p(4=0TB`O;3Fb5fMUu$}f28VrKm2xZ zv6m%kvseh7qSgip77EaI-QUw)?79_mi_5Tmv|&`aae!q`zGT!#<4UEHNXKc`7=TkW zGib{ntqHU4JHR)V*m7{%kkW~?(;c^Q|Gx8@%(k9sJpT+69?3w#@;BVrRx(QBHB>*6 z<7^!qLuoT;+-a(Gv_m+Sm)p#vu@0(O?0a$e<-6!TA_B=DM0PRx{eZ4G@qT-{t@=;@ zaY*z^luWMIvwK6N`>Y!w@eat6MJ3AUH(#nmosGcrGuN>%3f?f3?X4d%%V(<+ec}Dm zzI@-ZanQ+4$LGhr{2I+>Xv({gi;=Auw!?Rw6E!eF%Fi>?4#z2Ce@*tM#6FJ2qi1U` z_{|k}>miEb@U_H#1p<92d-6p`-t?X!lvpZQoS-itr^luf`WA!vf!}qClSg8+pO=U+ z>9-%*9Ju0*{McOIX&jEFR6SS>s8nLB&SM*c1XIUO{1y{?t~(5gaXr7LN{6ERuI2q_ z6Hv9XLa}1ZlE|vP!q$u8a&>gJifK)iVl-8H-{Bcu&@5T(KGDPe#H@2QF!quKaoPL^ zOs;rN)7c?hoBOU>Vn}zGlI*r#>*%SzXc;L1ow|nLuCjO7l3)%dB1XQk^nzNLK%YO| z_tXFMmd~@##fO*Yyqf3?<)W89yQb?4B-h@p#PYkbkrL~J=3`P$M`tT@!KTiQe36oZ zldB$=c{d!Zz3C^>J=oyf6>Cm?fqP7l^~5)ZBu7R)=EZKQbM(H6EzkPujoC~)n8snA z++two=JXOfInDR31FF_+SSv5u?PVst^J_(*A}} z_Nx=puS7P=9i|=h;^jMY;;P=&602@pJe&iOV$gj87s7u!mg)~fqK^jeY`9sOf9XlR zE^itEqlu+^_r!`Q$_cI=_mgYh20F3ny^4zsMN_KZQhF8-g<%6M%Ll+E zZt4FV>Aq+l3C?NhV)7CJ(p;q!eU#>;!-VeyQ@3Adm?$_iznMp#Kh11rQ1&3V3g~Jd zx0y_DIjB7D0XeKa@hz<3!@Isu!w@%e9j^Pnn}ozSkc^Ytk@Ipl$M4AN8VQ!ssR&HY=^$MUA@6(kOvzYyO9=eOgZ)Rk{3~2 zubwzex84n~bCRDgsk-IXHA+eSOD?$4VU6@e|$e=1(J*`j&RCpE!R-xGVaKoApn6nQJQ{KlKo{QI!jFEfwz+WqV zuz0=P}cU%b#rw zK0WOi4ANEURgcOh@{@X_70_Lx?(_WzaqIHMG4~(K&F=yGJkUP-bq#$n{Rr&&s{;~i zz{P`RRTF(>zJE#uw3JGd{xer5wnu_q=}2#k+#@>ru2z7|v=Mee*E|x%iDM|mNDQm~ zgDD@-bo$r8$G}vkBUt^X&WA<;j^RLAjc_WTBz*17q#A(!PpOzpjM{JXX`1QU zO#tiv-Xv}%52bbGs)6l96)+cFsXSSNYW0;;`tr`dnP1^~4lI!K*>CKPMJjwfb~IA6 z5zNGzvdkmvd!rV%aS=Uwb@o!gy1`}vAbd0R&>_iWHMc@2KX7H&;(Y3 za{Ne<;L3nSebF`kTDcCjKx%-zni`vbur)Qn`!sFKR&;9191&LwJX7y}dvQ_MbzEmz z)Tt|v7-b6^52TqkJwi4o=CKl!#-3*G0h8>K-{YLQQDbJMHQ1I}?BIvVH1;dlde;QW zDiV0&nx~Ohs~97t3**|2udHTI4s=`3_M2YF1y@FJ;mqD9_BQrobQucqz*nEfXPw=r zY1^*3j7bZu#(um!pc4#<<4^aO#>DZzRrgBkoX)5oJ6pE-SN(`KL=~T`{ktQuYle7I zd5zix?=n@Bg+`UV=&DR$FEd`3-WXHHx)U7XV8g#6wF*-vSA)g(sq)hB67na1S@-yyHO+YZ zsX2qFlX!RBN6_WB>2jWD)^l!xp(*c^+32YTS6&vg2lwF<^!P0)g@1jmGIB6Zv%DZfww4)MWrh_!H)z7upTq z@%;JXy$Pa-1^0c^eht4nXbh_6I&}r_)$2i>(+)GmwJ+eRdOl_6UtIJi5tPHadZpwY zs#Cv{Mtz>8PMw)n=~hA$O#ZLOXX3U0blwzmrMz| z-zaTiWa5?F0P1`MZ%QP7nb&4Dll2=wN3NEA@Ez7fCpbR#?vI3gsNAXN4MZ=~Q=f;R z7pzg4gtURBF0G~)>I5j7r9S%-=2rYkZtNOWk#Vae&kSWyemsjL5@+k;G|j}m44o+J zeT&R_1)6l$Ffl$zWhzP`iK_BEJ{vk~bJw~#n#M+7Rlb~2WfHw7r4pv8YFF*7hA~~1 z#v<8jSo@oK&OCHXZry5)t9&jtja=2fOKW99u{m`0lA#bo|A0R8SZIcH(Xe4gUX@$q z`@uZ#7z4|=TySFDx0$?2kjr_q^24T z#n~C};(VJOHq`f;gR})pxH)h#6WpH5!>~fgrqcQGD{*Ig(&@`C?Lar;E1Azq3;6!* z9)v}6>g}r}O*CWHQ+<%c+}YRboNm|Q1$WqLqQ|i8lvS~%NO^e$ZhkZKLAMS`rg>e3 zY=$ib$%Gp9%sZT6@>K73%!egMpvTZ+EEy#irM>! z#O#$6@G_@44YCkl_IZ*4vnLNGGbp5d$Ob`=Cf~pJ82vV4f!Zu8`$I$ZE=y@M?02Hm zck@nxyY^~#0&AO~p0 zK2W*ef8X13xA*bhWDyf2#<;&K8qLmOzLr*NVw@apLMTh$1AX6gH`exy-4i?xZK03L z&~X15{g(R;lKAF$HaE>1RCMzitS#S;v>iKgCS3P1ob@Kkmw#_O0#?zPo<{Xsaac-G&I1v9Wqe0kG2mRN~7fELrO@tv+#uKG=&6dPpUTklq#BSt#)_eLPq5&`VDGMH^&I8 z-_ngdWck?-F&^Zaz1%IWwpuZqAGVVALYQpBg#7FgGm*J72c82*REFcC`Wgm_nr5z5 zn(fk$y*b{&XK1pB!2`tEuCm}y6v2CG74MBS*>;m%A&=qm{2vLfFhlJ?@vHIZI`zD= z+2sVF<+(|K`8F_iLU~fHk_=XDm1tm$5AUh8%0B)BUv03#b`w$wH)KSfQ5%0e%lr`U z37tXvca%)?OUuc~jF)-&1LQysg@3@73AZoW0B{CxQ}Xj`$d&^qk@p&iZs&JgKEx)ZL}=9JxMrf;!$c!Ww^%;K z@J0&zoEZWL&#M@WNQ7#)S9|sTnRHVE)HAmI)98Y<*C)K|U?A+E#nLe5z{Kzh3-gviKc^7eE|^OqhqYYKR~mObUG zAvbeu9h`I)&m;OTt1Z(c69s99FQStPpUcors~tSe?B;iMDNglPR{oW#oQk5;Wud|+ zP`O#mXW3o)D4cQ`_Zr@<-OTA{?&4ceCme7NT}m}esH{nc^(TmfSGXh!G^C_>A}<`L zk(FtVp0VX#RP$_(PAWSTzc3+r1$aqSev|2el>oJ4r@*RH@>LnW!W z^c9lgHc5{!z?I6plrn~tWBaE7S3ZYpJ0T{xPR&Z@BmT_N0Zigd>bu;O5b+esa~E-p zu5|~PV&PrQzmUc<@8IU7A693(daq}kI(?~~e#0=ER>6A?*YNbI_}yVF6F@a{`dTtb z3A61K$bur@&m)vUzTK~P-%QRTz-!TL$Mt7B{9YK~q&hP}?;lB8PJ=1KZ5yf5H?7e{ z@kETz$f2|Ci#!Xpkv{D^FiJtD%cpkRoqzmQ;z{3cM>0HJ928=GH1w_{Yt>Ko?}=@t zneU`oM;+^u)-#s`jb?BGospZBc^Nl%;%8EZz#(6%;eI)GhfSLX?-$wxp7&>qwhFS_ zBn!P|xfyD#T!_>cUqkt~RXWAT$^^dP() z&C_Wx4QrP^$o?!{Hm|RCZ~jb1m3h9}YEPOdEyb1_@^Y1ldc=Y{-e2I?I97d^euh3_ zu#dbF|TKxfxfSmHG@_Cq6#!LcBr#yH6HhAWzEJBFO64jHU&hnXyNw#m|hh z>r?SxLkvuxkiOB-4Q}xUxaSfLeoNNG1$eqXmc;ddz~dtMO^&+msZh;1^EB{8aitTE z7-PoL9S<02)807tP5(}_z7|e!wWNC7C?YLi)D{oaI8W$yHqXUpx~!-R*#4as{XW0+ z%Ks z@g}`{Y|1UeA=~Y74cERymCNU{Nzc){IKOTVIAw!+b6bw<31*kxoW)7eT&RrFRF9*bm)XQO*}wc|inglf;*mq2Uz$L=_@$l0*x%pk(0P4_ZvJV~ z)tp$g+Wa6c3imt5O#RQ&X4Jb+E4NrGmJc5{NzA?Z9qNLfZU$3?-r*GfyWL=|R@d0m zZOwsyriaBQ@eOe8c?Jf!Y;XPB-04G;5L@L@tDrr4IMFB%8abOZFowy^ll+JHs%{Td zBW-MqlF+}3?^-S|%1G`}ul~EtA`(6sFrr@UYj~ayMVjlG(;j3y{D-ovx+w;F#}eSw&eS8v@eZ_5)@}DjscR{%-ZBgc)LjZB-}O8)>V>HhMM(ZE>yh zk<@n3+m*Wkc(F_HJ|L{zFrmEd9XP9Y+KtG( z>^dm*e7CuaBaR9y>GpeRqP92(IgPv#mY3c23;G<&rmj@=6!ZF^E&O{njcNF*4_ zkYfNo>bX>B{4i1Dn2bQC5~=NDzeL=fsRSRSgC-u-W}W{>NY)5z?kv{W<0otGpoVgKs*M1 zWj^4bI(q9(l16gOio>3lCrP+wkBAnZ>ME5*XEOUyAFwQHPrj>X*QW$%M`;va%ZsP1 z&BH@>ILy(Tu;k{E%@jrY65&0G4g^z^ilnac)z~PR+M^G1Si_txx|V_jqm(M60^$ zWvOXJ|0}#;$c$Qs?NnIy%x=yyUC2};PT`>|L-8*)Ah?@7x{i`t++)C1B&t*{!{lqA zaUtjvqBQFWus)b(GzVQx8tcw)2xMb>BMGPJuN{4jX%Ac$4!|dxb0vUUg;RN+*%VyM zpEz%+zU=31-TtZjs8yfUQx#8FRuJj71FoTZ^`ZK!-UZz2u9dC`-dkOeFm#ln3$}5} zep(W*gxqnqii1ACCck3u5ct?97nelaYUp44OHjiGw^tYG)hhdR9dBs#6Hg-LqLpmM zb1YVD5Zt5Br0;)D$v`)0^~FCjzUa`B0Z~6_^rUC?*M?LmVJafu6end~L3$|YpOC?V zzf1@Tp-s3~(Ko1hA4&dyD?U_R7^WTe`w_6oA6KwRTCH`mzxe&=Z9x20)#@l?b~vWg zs@n?Yzmt{PEPH49N-|#4QHnMvqWNYbi0-Lo{CDjo*_or zsZfh)f9$Heg_V@jc z_e^4Cfx!ush(wnb7IZ4dYhYZxl@(k2FYV6A+cRa&78aW7pV(HM)l~}iyo{79@7jVW zgV`44V{!t_V3`g!*UAqL2}7;h?Y86#hGuhYLs#8jIVTZ8IqzKYQ?jjxj-D*)MB5hU zR6tVC;Y9s9N+sfW8&Zt0x35!MCuj36p!0?f!CEASfk@3km6hflqB&W8rFo~jA=8T! zt{ZZ~6I#OV4HChzp0!QL?6GTnElu(_um8tb`Kx0=dL)0j|Fk1Q`CrGKSW}%O|J!4=Ku0`2i6lo&6@=i>Obzd;O&JdLmY+)+ED2Npl-^2~dknP5 z*9VNiNS$7VPKLKbHKAPiOW`+X#yu&abWN4nu)Q(+o8a^=erEL3nsxec=DOR zT&(@*VQn0g^!3_kQ`@$DLW9hT9+b-Fcm{z!_vT|PILtdz%H$}VS`jk&WD|NO3xnfo zsYjY!!YSO%WWI9l26Y09cLpavzaHjc-@x1?k!R74gBr>ODSp_x`Y2GD0>RdD?k3=C z(AB6MG>Inn&?K8V(TFBdh3u>!LoMbED*wg@`9R9dqc1V(P#QuVQN2Ioyn-|=cNw;I z=J`K#)EaC@PBA2>MaOpkfX3ZfiQzgi5y__yapq+@N?RrUdeIV?3e~^Wt;b47qdqEx zc7|1EIqa`K@M$z6;naX7Vdfw5ss*QJzF~E0x*>d;tUsHGr}cBJVuQ@6pYgzURZvZ< zA%7f@pJi1A*}4P9kDIak+Om~YW@-FZw_&oU@5fWKre{0HFs5FX|MpxlFsm9ni-W;h z@=5RYUZPzC-|g(wQsJ2{;Boj)YnNg>+FHPF?U3s(bC$D>+q>s z#XnqEF9?Y+-v~ScthB{*nFdJ1laQKQko~rB>d*A1u!>nxOA1RkW zsdBq4sc)RDt#O_k1*y2HKPlW_P)GJ25KrRyr}Ol`hral~3JUVSGnoMiRyu9RWr8Vn z5*pS# zXZr4GUhMerM1z+1Bwky?AHBnK0=MtBzDptqdbnT{6 z`;I@e*zbIOWmG5Q6W1KR3_slDSWrYWhkNk|+|om;+T`kpXZo&WQ=V>D@Lxt* zVdN$@Tq!!?UQ^-Ha5yA=X8ROoroZ%FBLnkAFER7Y?EFRN^utXc zwulCz75jg6WSU;0w6@18S@|n1&ecN}`w2d?gB==)<`-@qOEsQb0Xla42I2XiqVXW! z&=y^{o8ZryvTaDx#gtJ6E9fv1-|uMZ;fsZzR)C7vHg|)>gnmQ-h^h~8j)c<2OmR?0 z-KF0#69NJyh-{7SG&hm)JCb(z(W?EU2x(OHJK?c~4@0;Ia~h!AG8gFjAZonF>^gF* z9X&)fn3~;Qi@nWXs6H`uw|gt$8uu_urbin!s&%7D9GI(ca@{-;|#-9 z1D=VO-9P923e@13Q_#=P%1vt&oZlcASIjOL&)-w~6a>ysHGcb|vp;67Y(UNEha%;KPlk9f zjDcs$a%@)h!>%Jd2TV%PoACWL*S8`cNpPWeHQJ7VyTH7$VjX7hc4aC}Aw{{Tm;G8q zMZn=hg3s)<-2UhIU`0qvm&E~hMBLaDDeUMvb*0_G6^s!SW*jbh>P+%La;59dzv}9R zeBh2IoJXd8O~}~q{K_EFkkvkH8yiC|k&NlMelFb=-m?!bQ8lkill1Gj;F8|-0c1a1 z7p-`xzVSX=b;A_2!sQ;tR)5g`Gs@^|T*#VINzA)e_7f-Pwj40S`85nX@k7%;*-;L} z^2hl+o&NOX?8b|sANTk57v4A9afIMH(AQqv(M!;PewYe*x&qKDT0|;8Ulrd&H^E@Q z?8Uh8iMb{>@Yr^8^b;1BQ# zYeGVGgzDD`zi-Z(0%7{oM#p{!`8Dd(GsHd_zS`e+<<$;bVRbw$mG*;?VhtO}Sny~Q z{TQvlr?k)sF(96MdKAp2c?TGyKp0Pn{H*>guD$QU&8_N&pdSS>h6HjeGw=|27>|$h zbuXeCUTw4g>Z5FdHWj z_4u4kHoXH7zepyd%Pq%Nmxksv$2ATBj=hfTHr!}jHbagg}B`=|Dulsh2+PN?2S-b?@t~~N^rzym( z4|8J?Rtn;UU}Aj)ZoUq&w`$X>3L47^MnT|f{!(a7&M@V%NU%Qr`9CKJ@%r2`%Q=-= z3rlw3Ho|HFKWGlE!FS}~I_!`-5V@4J)ZH9n8{W;csLit}Z)^@?<0n|>or$gkP|xo> zEiPb3wJgiMbMdxG#Tx+V+TgYO;+2JuPe!Cma0{yXyM#9b*QQ$%yzN>qT{3VOABIX8IqS+)j~6P$xH4;l+xT=b{WoX90s1xN7g;9sD+V+dK#R>a7`T_EWyg9 z6GY>cOC&BA$SsI0Y%Prpy1OWj>D@B3h_S^V+poB@_3*?CX&;q`Wk2~I(xz?FE-PN) zZ1QSQa)$m0>Cl>N+si*Q`8i*5{rnAm;|l{dupuF#(dW91vdQ$sR3Ep#W4s3O0-c}F z{rh?yE57!bwTb)p&)+l4ccA1;p8IVlY}OU>5Zty)Tak6GF>5bty=`l7!O06)A1Jvs z$oP?|yQc1o3e`In^MnsqZng|tb=>s)`QpB#U#-x{Q9RLLGP=-;a%QTqd$Re-Y>$9kwAeh3O9ZXrbta_sS zer%rR&~?Q?H*U`t2OSWlgH7Vs)7T`OcBPBOb4JQ~?f!{r+s-kY?MG6Z6d6gOkAM6L z<2mL361)LuC{VV#dH<4Nyh3o4pGyE61gBLRw(;I~4O14UJvzrMw_m%EXc=k?y&n4a z3I9ks3|D}?@T670+hwz2o=s)P@rTjkfJYN-(C~Fbp4%%{3b2$CaDvOy*>KO0qY#&8 zc4R;om+_xcq>EyhiZh^`g)4_xcT(1^c^kCp4zA&q4=$!diAVjkumUV*tt3D@L5{-w zA0z)i10)72Y_O;HMvncm`pp_6kxJ?krN=#SBd_bI zem3sWsH{YPhi-ofVkrIvD@_%K2qC{0Q6udVKO_AEXwSf9SpBDgK^8%B7AB-OjYxS-uP+et%(^hTk3B(;pz}{9h2(%$6gs11S zr{Kzogqz)mP5+CT?+Lqp_DH1o`?t~fPJgY?n@L5nYCV#0rnXK)5A#csYkSvJ-|wgY z^JGIzkG#RRLGK0u#n|rcige;!J}(kIHor5VD*C?}D)8JV4WDns$a6;o`j_7Ld35cm zHhYwf(GrjS(rl;M_Fs>4KJDtWEuWhAlZUqrZ=ZI2b?&}G%1fvJa?Ou0{1S}Tv#)2k zS3IvD=|rk;%*@R*PQHcMU+`}zb>K{>fU~3K23tu)KI6Z*;z1cUZAF(n&b?Po zvL|_9N|hgMFE&P@Y`%~#+!gB7Gi4UOs z^!{CEWI*L)Q8+@G53DR1Z+)jtO4f$`X9M{>8qW;zQP>66erCD0n9ik;IYhG0%s)vD zAVVSWlt%9DPQTGtJ{$^rBI+Kaid)6SaY0tG&H&!gA*PT`&*+G9eBo&k<(0{bn2~Uva5!!=K?L;6pKX59*&g z{P?JAxN==?i(1}5Ykb^tPr}AOwC5Yia@yW+!EEhqXLd8zdm~sWaFL$uX-iKY$9<)X zhrq28szA=z&SIDD6HI&xzj1|qptBEP4A-uZ3RTuj#Tp1#%0hw^4AM2aj( zWvJlIH?-<~1$oz%y3Jo#{3}SqaEJ0zK(Y(iDAV1@V;?%}ud_<>0GCHuf32O#=CDDl*sKKN?g1P7ZU@OtB?*K?QPSilIa>MgaN)b)6;*30oz6yJSrQsFY zsYmqc{0~k`_6JkF!D-2@8y=)a=z>g>Yb68aJ_gkT5`7?p;&!?UNKkpL4ARz)fCO4$ z*i0iah#=JP6M9Sku%(pSyN6>!+a+Z|&KFJ>1KlHo{!vVA&=vuUM!Mxph@|e58A^fU zb7o#O2sKO-d9uP5M?`$T8}3&a?QS8l+ZMy>yyw?nhhcjyOte}{Dn7}I0vT(UK^oUg zyYOIgc*A2AdM{p~EOLpB+@EUl;e6#;-}>FpT0rEzyY@^Ytme%4PpE%5OZ1lSsb{-W>g6{6u6d9crMB!IqX@EBS?q1*-ya8PUUWE9=MN&T zKuQ?qJ4;G#>Y_XXq_);Ww@sX{g|Wv-#?xu^4?o;Myn>0ge0FbM8 zAH3fW0r@0QSwB!bocE1dwf*i!fIz{Z@<((&kBWIVER&xcGhBq<)8N&I`GJ^TACUrU zb{Zir^O#C8d7qpEafWCK&X_+*l?^1)>vdg}X+t3;Y-C`IMPG}(?o4q7elOkO3IV0L z{4W3hiCZR-uo>`3hs4ND(NWwl6fuSvaKSwMi)|gIV*Lac1CE1V3T$2>@`ird?vTxo z3g&OtnMcA3Dz20nb5DY1PPZMV1XbvZ3U}E4XI_~CNCcB1xJGYeG0aT07eVz7@MrwQ zQO|R`=p;-P=t}(B5z9{te23;Tf^_n9T3#Z6%39!ncFNO@=z-0er&n)T9${(fJHTv3 z7XIAgz{%LMyIVeBeCghUI~>z}sNSh1&_xb{Da6vy?_Y0li~Ef(bT0I$HD4TU%Q~L# zHRIy5;!$3MorHf$ApZX#TY+%cXn+`+RX;e zC7SMT+MbNiQTLbs>Z9Ent5Ypu`T?B#@W~In0B# zG?;I8evBcAYE2(Fx2Bjl!H8B<^q;qcki!!+Z|@@CHSMA3({_MdzWm!9gU??SCXL#{ z-FD@^Va@C)xgi`Qp$G|N=rw~__3?d2f_QWFmCMk&vXXcA9XQQ-5g;SDAsSHI#ycq) z#~Wfr4FlaOZ)q)^M~;9Du>q$6PH1bzrOzGq1+7dWUGh;;2XA9wvv{9&`=tCmjpJhVHR3 zS_qyES}flL96JBvWw*1~;s&mV+qlf#A7PRYQ#zV^3H5B3k$Cmn>AlCvPAJjz5A2iQJ5SsX zx~2khKWt`8_^-o8pm5uI%BIb5FqJvXuA@c2*W-bEuIw%E}!psFhWNMsF> zhfG;A$ShWPpxG72Ky@Gc1f23W5MHF!zxj$HL>r1;4Zu*A9L3~->}`V4M;Kb*s0rooKB-p?AQRzW2KwLoP+%`#gi36v6I{l*{2WW zQr3MfU(2o*!7Vnx1qJp$3|+>lbfW2d|K^A(cvwE%rBhg0~XDw>d0- zNZz)=5q{|46#E0oy%~Qg+p^93St0e<{lsUfi|u)Fl@M=ZcOsuqxDR6pd+cg~C*xBZ z^qbG$<`4+Ay6>U_6nO&^U*-=2(Sfx@-E!sk#x=0ROV}sx5Xvq_<&teRu&ysa7fr(% zLK}%FOol%OTXV-is*Uq2XGMOUE>k%U_l+edw!~)Ua`!xn2@MPwD5;-1wM{UGU4zzB zx(efg)T%EM5<*wu{dJZPa^iNmX}5xJwn+=>KJqRt_PVyUK~&itTnR&Y(cV(_)`ZWF zsV9Hh#m~hzKtp@%MZouVOfw>u+^e;e4u(C(>KNMLMJgrq*hsD-)b1HMJoTNE`};l2 z=_R%Xxp}kTqTF4}Y14i-ZO^yr4iIIq<5hcClU4tY6@r&-ICe=pS29(u+_tzx<0@Cb zIi%y-Vg4C%v=Y}ImBufLls|6j4oEqIY$T7XducRAH0Fhr>JlKkSQER8wC@y;hWs-8 z{WNl5|DU6#ht%rimXud(w3eUsN0U7)pGh#=G#`MxtYVQn z1Sh|*D@X-f@Z4>eSv=L1V=i$(W9^}iUH1BG%H#%FAJ3|S^PhC_ukk#Fz|+5e;M!(< zsUC(>StIb?z49LKYp0Ae*`7W8i`5)Hn+hS(p? zRT-)a+g0f*d&MynTN!<|lYt69-NJoHj}1||UoVm5epgP1Qbp`#NE#LyCBS#^trfxm zMFeP+Q3BWmYkA*3Xi|zqWv~5E=%(B-!H>D!D{KbKSe_fTv*8EYaHV~#oc z{r~-M^CL&_!n4mAX~L3uh{9g`s8PAkl$+{7T}JFkZRRJGuAT?ppzREpwWcc+f6YV|#9Dw+Fo9?+iXSJNUA;-({JoDg`6!mD4G?_g19UO%j zA7gTFEKQy+SfEjamR&Ssf0jK5ag4}2JVA(=BFmB}FwrrKQO?vNcIpAd# zU>9{f_P}QQ#gLlpwhea-zdRwAM};KF)*??T|Jh2({-zP`@D_}DR{&YRb^X*~Z{)D1 zCN0i~h0_-7nY(ezgK@!7nRz0qtgFfJFP7{10h4j4@2uGXtoP;r-{AcJorvuJOkteG zjnFdUMo&0lV(&-Rj&OcjuPiAhXfLbTj;k}(<;W}8vtVbQ5(k9kwrv)XK*sCp9(`gP z=;n~8fvG219iTx(`2y6+b~$UJ!I1(i3)Rfz!B|Jy+h z@WtWvkJDK;Eys#zMvU0lA&xlauKgIuu1NxNqxC=K*dFDxJzp^wYu109`@cg}wZvho zUmR4ep<o}9MeV~UStvNwlq2cq?3_K1{SUlUyoz=7q}+t8-GkOpVh~o& ziec)#oQfOuJwQ~kdjeruHb&-UeG;t2fC4%$toePQU+hPCuT?pk{ir~6MruxlAMS_h zKo#{8HLRkCf+`9mAw50eFq1UM|CovFh$LiA1Ly@v_pISw!^nXud@N9(&-MW~ z$5u00B9#Yw!PcUA*gth+Bo#-~aNZXJhvxU+mtfJ8?$*@Sk3#~anm-!pJQ&iB9?ZB# zA>N;<|5p(%&LZfkF#Bz;eoLSucJx>QP<#6pXAkbZPHOX;^PY;^62mvbc~ZLivN4Xs3y(rE18FEs+Z7gXwvF@O*=Ap&JIMxiS zjUq!X)3P1XV&G7v*^NB;!fT!%+BHkeQdc-Xmv#+l^L!asmFNr~fwzyaWi^&P2vg-Q zMrRsuHj~XC=-NukMJ3a3rVn}SbEBsfAZFf&tMHqyj5XWI*lAV~bIkpp6d(-ufB{h+ zB0bu7m;)8xeMYJ+E(#qh9LS%bY*2M~-vB3PvfSl)fy>O%Ok77Y$BD#wj6P++@^_}s z{Ul6Y9~~tprsE?Lt-cO0W;&aS-uo$F^HV%IM1Yq98xy3!{00ja3L`=1`aG2E+cjb%fEWn>RlLzAz@XPD4*vs=}9g*@t_)? zLDl%`rt)-@JRpI4@nZ)s?oBhh8x?=qPu)hxykKZY zyjsHwKRo(+R9b?dxRFwSzf`m&+*Rt3(idT_pord1dIc-$MHS^#x3XQTg4$$f*^|HX2@}8r!HuHw2h-p3sHht%`{~CG7ol76+O$ulRSu1 zAP7tiq@Pa;LpWGZ3I9AR1~|OBdEzNF3}9eL7i6MG=^Mo+LOurk9!O~cyS1*8AWAS# zPv=;0dS`B76oqL}$? zs?)U7@VJ|DzlEYULHPq(OYnjRRKur-u@k|&>_)7mRq=lDy?9n@j=9Agw`=DLg*nXBaZCx+BcX*4+`PVsoYV;#-;Ifj%&~^+1h9u{IRZhNce2k zFO@EKOxO=!lEj(cNF-@`4;+;X?$eV)UXOvKV>!1!x93#X)aTo@DQ?n3$q{$JH0yVi zGMR2%_hcN~&)W5q^Lpj1g=jNws}?HFdhr?^5~i#tXhz>8UONh&p31@0wgoGE%EZW01qGtuIP62 zRJkU$>b&ryZBNUKGD=dhNSsPwp0I4dd%61`go!$YeYREmXpivAa$;X!`t?1m_jC43 zPS**wSg=XQfQcKUzU!p==2U;9TggKs71Ba?6e5m>6-Cd8mo z{GHT>c>GQgm$XcxMC08?f4MR!4}(6)!wz(U|o zvFo6tk^c+Pw>voEd8SUbRj+13o(fb7Dp`9_FLQ*w>NF=c9mW&&<~9}9m2CfECQNL+>~>}vp;=I z>AAF~e1scme%hHd_;%$*u}g1^8>oZwS1Quw%b!D02_`|yb^5TWFVHxZPk{Ke2EuDI zg|mMC-Dm(pn!Qzv%{8NkSycGOp;*&)>JK?denU_BHoC+3HqmAWMOp}C2k;*BXXgj~ zgCZDx-1>os-5pxzd2c3eILgPTGR0)o(u1vtXx_U1P{6kxom^eDmGfFAD&b?Q{We8f zCIx*(`S8ijr3fX(Gio1}x}y2M=TP!RFe9BUgvTv-<8_*92gioSZANgR|5ZmllVH0r zv$UFJ(YYdrT1HdyXI;}HY~ZV=RADi5$8f)cGPECNulBhHwM$@UZSDNmfVL41vHhaY zYzIEmA@PHXT@h8h^lbLZJl4J=$_2*uXl#MBz5D#ia3CJi95`2H2Tt9(y*OVZL|XWR z=R+o%HOGq#>3Gky+jZws`kU&;qSBN@{yfO64!10kqVj(_I;l#RwX=W?I_1e&z_ECV zQjV}3sZmqL3q>EDaN_fLO85@vT=;{0(2}I|d^X_A3Uot|f;7#EnEgc!;?HJ$QH+3E;2d zpPXPZ|4>doj`E1AF;{|G40H2wvdJiRiGjoPT>KVAs=| z0D5b{5^OOOX7fU=+Zc%iXRZU*I%DplX2<#O$U?4M+c86Bp7x_+eblAV*N5v8%N@15lmnJa zpN8}{F3;utJf?ZAWS*ZE7OZtkmWdCf2kP@I$}cx+dh|8z$&;k6xL+r<+u+I#}xWNc7u9yD^zz3>hv^rh2?;J>oCm{-t1ZuldT5UDN5;_N$-tn#WS-=JO84r~B|8o0wScb#_A+|K+FF?$gMtw=j81r8vv^Q|6Z z{Jt&apXRvic6}ap4zWy^$nlcdH^};Pdh8V4a`CfB@>%Y$RKAQHFO8`_h?)X*mqiUZ zRx)5?TRDL&_U?I0KHbkh5|jsDm0o#|(>4zs8I_IXxM_!usTqF6wW%ibTmD(K$$mr& zX&>5LQA>sSv%`Acs|ZmK&jA-{;QGeNu|77erEf{O~U)Ew&15d9h+20*?GU18sl7;1U5J+jzNbhqfLsR^0OZb z%OY()G9He5H5$bEbO*7qhD)#aCg6{0W_Sq?!zId=q}J%o*_WEKa+Y)vxyh8d1Q|H8 zc?aCfX&NmpJ!Lvo*al*=Yb69bAN#L^bDS>!0@To4;q^it!Uw`|!^LZM>fuyR54c@Y z{<;SIdV>l;P;J7HsNe_W+NA((v!DiUTQ+w^DnY!`^(r_si~sD0XnMgM5mmRloISvQ z7Nrs+{O9P|avGM;=a6BTisHXfbXPDTLL=Ex>CF^?&M${xWgj69nFeVA`^Q` zafTINo~(l@sDnLtJV}xGf`JK&4Mm!oQm68#QcM=+!?%_Ah(s7bN z`$wscUF`$xo)0)zSgY69NK83;#TylDtG6Aae$K3?=nHs=ZiBujV5WYmpc+(2{{=yM ziMM>sPMON;^gpMEm{BQN<@)%Sj*rx;S#Z!h9QUQNr5QugBjgWB@m~xZ{okm$UfJ}J z{w)#lKh}A@wR-!T6&kbmepO`Ob{Bli6r6+I2LSKzkaQexlHSb2y2|S3^zDC@+MCo} z4&2?NCA@hE)J4Noa@J?uN~=#7U?(QZ9do?AK;YpQ1NRE6Lp)cIQw>Y}engU~wY<$Q z>2&7RBx}rAI)uI@G4`_s2g{01?-(l8zvkz96(DNmc?Zae{1(PwMhJsoI7L8_*wOvE zKEv#Nwtmczt3xil2Pjm-T{`|{b}R^KY_2 zNbDE8_9nHB=X&-H5{*+m_a=Iddn(blMN@OWX?||ZYbPdarhOmV@m=+D_Y`2^>Wqv2 z`}LzETi{D3F)GHaYz=AGc|%mjzi2c$8(?KlIq*M=qrKG8TzsNZqVk5%4fGrOU+62H z{uf3;n;7I>7XqUUQ&Bq`6$0&&GuA}wVq181R|v}C$oKH`h*j5Or8%^YE>ijArZQg^Z) zLBoVBJX_}d$uwq>>lgyD7b9p@JvN$z%k3JLG+Sr*c*y?%#x!Es?{H1gZFozY}jkkKzB+_g*Es#11DJdlV|0nYBtd)z4W@y zslk=|i>Ug9c6=U{xIHkIWA^Vi6@)&~UmtPWJU59dc(g;3ND@mr8#k&>R&%@K^9x<( z_kdh*NH06==D{+Miw1U8&&@-j%e{Q27tc^R%fY-!^Zv1OL_P*d}R3 z$PSk%148z|gl}%3Ol}*el*S3uWFCfo`sBCb5`_0XY+-X7dt&H6k{;Yl*ViubI0pfj zKL74G2L#F?tQ6u}nv=hUG?_rau>Iejygs1)AG12fx`qXG_-y;wpHA^kx-dzbRI^CG zR*q0HAY`6QiHtF(BbjlOnQLwu*onA$>l`l$Fw^-HlmSM@<9~z7%vrQg1;@Z^YqsgKcw8aceM~K zb*~J=IEr9S4rkhi7({8A;JEj#ww!){+@!y7S+oO#U>wNf z%vQ|NBsjEwH`St*F==G4mu9jM0(n4{Y2D9Wf^&+}CF7tJJ2^4OovvP3UkX4}7i_}H z_g&jzoeTWzF7o$Phdc0&8Lgqim(Q+M3vc;L2YDU~IfMvz)nGCv2_m41u1=;01n*XU z_ZAbmnxYe+Tr-c_`KVH~RY0$OmEwf1>K30D`_fy(SK!LzsJWY%#H2;6#%R4oNk z1JXV`qTTBh&rX%n<29?E?p9%lO;!Umhq_AwcQV5jj?qUskrrB;d{yU>kWcks)P~i9UOJ{QNKE7KDc398EF2cIp5}%t zY={zld7#UXh^~{Ka?o^(sC5Znd*>=n#6-YE$Od5CTPBGpyN>OiOK$uCJ8`u;o__NE z)z&+3*baN8Y`10OOzG%I)zlZ~GV!b9A2Z^=tjlc9U&!F�Sr|D&`;jmV*9f-9zr< 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 W`2FC;7cgxdB`>QYlP`Vy@&5urFq!87 From d11b883411641f0a8b59b5eadaaf9ddfc2662804 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 6 May 2026 08:48:16 +0300 Subject: [PATCH 29/31] code cleanup --- Source/FilterEngine.cpp | 204 ++++++++++++++++++++-------------------- Source/FilterEngine.h | 112 +++++++++++----------- Source/LFOIndicator.h | 44 ++++----- Source/LFOScope.cpp | 84 ++++++++--------- Source/LFOScope.h | 18 ++-- Source/LFOScopeBuffer.h | 24 ++--- Source/Selector.cpp | 62 ++++++------ Source/Selector.h | 54 +++++------ Source/StereoFilter.cpp | 30 +++--- Source/StereoFilter.h | 18 ++-- Source/Sync.h | 64 ++++++------- 11 files changed, 356 insertions(+), 358 deletions(-) diff --git a/Source/FilterEngine.cpp b/Source/FilterEngine.cpp index 1514adf..ad29844 100644 --- a/Source/FilterEngine.cpp +++ b/Source/FilterEngine.cpp @@ -1,170 +1,170 @@ #include "FilterEngine.h" #include "Sync.h" + FilterEngine::FilterEngine() { - int lowCutSeed = juce::Random::getSystemRandom().nextInt(); - int highCutSeed = juce::Random::getSystemRandom().nextInt(); - for (int i = 0; i < 2; i++) { - m_lowCutLfos[i].setSeed(lowCutSeed); - m_highCutLfos[i].setSeed(highCutSeed); - } + int lowCutSeed = juce::Random::getSystemRandom().nextInt(); + int highCutSeed = juce::Random::getSystemRandom().nextInt(); + for (int i = 0; i < 2; i++) { + m_lowCutLfos[i].setSeed(lowCutSeed); + m_highCutLfos[i].setSeed(highCutSeed); + } } void FilterEngine::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept { - m_lowCut.prepareToPlay(sampleRate, samplesPerBlock); - m_highCut.prepareToPlay(sampleRate, samplesPerBlock); - for (auto& lfo : m_lowCutLfos) - lfo.prepareToPlay(static_cast(sampleRate)); - for (auto& lfo : m_highCutLfos) - lfo.prepareToPlay(static_cast(sampleRate)); - m_cutoffSmoothCoeff = onePoleLowpassCoeff(5.0f, static_cast(sampleRate)); + m_lowCut.prepareToPlay(sampleRate, samplesPerBlock); + m_highCut.prepareToPlay(sampleRate, samplesPerBlock); + for (auto& lfo : m_lowCutLfos) + lfo.prepareToPlay(static_cast(sampleRate)); + for (auto& lfo : m_highCutLfos) + lfo.prepareToPlay(static_cast(sampleRate)); + m_cutoffSmoothCoeff = onePoleLowpassCoeff(5.0f, static_cast(sampleRate)); } void FilterEngine::reset() noexcept { - m_lowCut.reset(); - m_highCut.reset(); - for (auto& lfo : m_lowCutLfos) - lfo.reset(); - for (auto& lfo : m_highCutLfos) - lfo.reset(); - m_lowCutBase = -1.0f; - m_lowCutQ = -1.0f; - m_highCutBase = -1.0f; - m_highCutQ = -1.0f; - m_lowCutSmoothed[0] = m_lowCutSmoothed[1] = Parameters::minFilterFreq; - m_highCutSmoothed[0] = m_highCutSmoothed[1] = Parameters::maxFilterFreq; + m_lowCut.reset(); + m_highCut.reset(); + for (auto& lfo : m_lowCutLfos) + lfo.reset(); + for (auto& lfo : m_highCutLfos) + lfo.reset(); + m_lowCutBase = -1.0f; + m_lowCutQ = -1.0f; + m_highCutBase = -1.0f; + m_highCutQ = -1.0f; + m_lowCutSmoothed[0] = m_lowCutSmoothed[1] = Parameters::minFilterFreq; + m_highCutSmoothed[0] = m_highCutSmoothed[1] = Parameters::maxFilterFreq; } float FilterEngine::modulatedCutoff(float base, float lfoValue, float modDepth) { - return juce::jlimit(Parameters::minFilterFreq, Parameters::maxFilterFreq, base * std::pow(2.0f, lfoValue * modDepth)); + return juce::jlimit(Parameters::minFilterFreq, Parameters::maxFilterFreq, base * std::pow(2.0f, lfoValue * modDepth)); } void FilterEngine::setFilterFreq(float freq, float& current) { - if (!juce::approximatelyEqual(freq, current)) - current = freq; + if (!juce::approximatelyEqual(freq, current)) + current = freq; } void FilterEngine::setFilterQ(float q, float& current, StereoFilter& filter) { - float newQ = std::min(q, Parameters::maxFilterQ); - if (!juce::approximatelyEqual(newQ, current)) { - current = newQ; - filter.setQ(current); - } + float newQ = std::min(q, Parameters::maxFilterQ); + if (!juce::approximatelyEqual(newQ, current)) { + current = newQ; + filter.setQ(current); + } } void FilterEngine::setFilterModDepth(float depth, float& current) { - if (!juce::approximatelyEqual(depth, current)) - current = depth; + if (!juce::approximatelyEqual(depth, current)) + current = depth; } void FilterEngine::setFilterModRate(float rate, float& current, LFO* lfos) { - if (!juce::approximatelyEqual(rate, current)) { - current = rate; - for (int i = 0; i < 2; i++) - lfos[i].setFrequency(current); - } + if (!juce::approximatelyEqual(rate, current)) { + current = rate; + for (int i = 0; i < 2; i++) + lfos[i].setFrequency(current); + } } void FilterEngine::setFilterModPhase(float phase, float& current, LFO* lfos) { - if (!juce::approximatelyEqual(phase, current)) { - current = phase; - float normalized = std::fmod(std::fmod(phase, 360.0f) + 360.0f, 360.0f) / 360.0f; - lfos[Channel::Right].setOffset(normalized); - } + if (!juce::approximatelyEqual(phase, current)) { + current = phase; + float normalized = std::fmod(std::fmod(phase, 360.0f) + 360.0f, 360.0f) / 360.0f; + lfos[Channel::Right].setOffset(normalized); + } } void FilterEngine::setFilterModShape(int shapeIndex, LFOShape& current, LFO* lfos) { - LFOShape newShape = static_cast(shapeIndex); - if (newShape != current) { - current = newShape; - for (int i = 0; i < 2; i++) - lfos[i].setShape(current); - } + LFOShape newShape = static_cast(shapeIndex); + if (newShape != current) { + current = newShape; + for (int i = 0; i < 2; i++) + lfos[i].setShape(current); + } } void FilterEngine::setLowCutFilter(const Parameters& params) { - setFilterFreq(params.lowCutFreq(), m_lowCutBase); - setFilterQ(params.lowCutQ(), m_lowCutQ, m_lowCut); - setFilterModRate(params.lowCutModRate(), m_lowCutModRate, m_lowCutLfos); - setFilterModDepth(params.lowCutModDepth(), m_lowCutModDepth); - setFilterModPhase(params.lowCutModPhase(), m_lowCutModPhase, m_lowCutLfos ); - setFilterModShape(params.lowCutModShape(), m_lowCutModShape, m_lowCutLfos); - m_lowCutModTempoSync = params.lowCutModTempoSync(); - m_lowCutModNote = params.lowCutModNote(); + setFilterFreq(params.lowCutFreq(), m_lowCutBase); + setFilterQ(params.lowCutQ(), m_lowCutQ, m_lowCut); + setFilterModRate(params.lowCutModRate(), m_lowCutModRate, m_lowCutLfos); + setFilterModDepth(params.lowCutModDepth(), m_lowCutModDepth); + setFilterModPhase(params.lowCutModPhase(), m_lowCutModPhase, m_lowCutLfos); + setFilterModShape(params.lowCutModShape(), m_lowCutModShape, m_lowCutLfos); + m_lowCutModTempoSync = params.lowCutModTempoSync(); + m_lowCutModNote = params.lowCutModNote(); } void FilterEngine::setHighCutFilter(const Parameters& params) { - setFilterFreq(params.highCutFreq(), m_highCutBase); - setFilterQ(params.highCutQ(), m_highCutQ, m_highCut); - setFilterModRate(params.highCutModRate(), m_highCutModRate, m_highCutLfos); - setFilterModDepth(params.highCutModDepth(), m_highCutModDepth); - setFilterModPhase(params.highCutModPhase(), m_highCutModPhase, m_highCutLfos); - setFilterModShape(params.highCutModShape(), m_highCutModShape, m_highCutLfos); - m_highCutModTempoSync = params.highCutModTempoSync(); - m_highCutModNote = params.highCutModNote(); + setFilterFreq(params.highCutFreq(), m_highCutBase); + setFilterQ(params.highCutQ(), m_highCutQ, m_highCut); + setFilterModRate(params.highCutModRate(), m_highCutModRate, m_highCutLfos); + setFilterModDepth(params.highCutModDepth(), m_highCutModDepth); + setFilterModPhase(params.highCutModPhase(), m_highCutModPhase, m_highCutLfos); + setFilterModShape(params.highCutModShape(), m_highCutModShape, m_highCutLfos); + m_highCutModTempoSync = params.highCutModTempoSync(); + m_highCutModNote = params.highCutModNote(); } - void FilterEngine::update(const Parameters& params) { - setLowCutFilter(params); - setHighCutFilter(params); - m_highCutModInvert = params.highCutModInvert(); + setLowCutFilter(params); + setHighCutFilter(params); + m_highCutModInvert = params.highCutModInvert(); } void FilterEngine::updateLFOs(const double ppqPosition) noexcept { - if (m_lowCutModTempoSync) - { - float phase = getSyncedPhase(m_lowCutModNote, ppqPosition); - m_lowCutLfos[Channel::Left].setPhase(phase); - m_lowCutLfos[Channel::Right].setPhase(phase); - } - if (m_highCutModTempoSync) - { - float phase = getSyncedPhase(m_highCutModNote, ppqPosition); - m_highCutLfos[Channel::Left].setPhase(phase); - m_highCutLfos[Channel::Right].setPhase(phase); - } + if (m_lowCutModTempoSync) + { + float phase = getSyncedPhase(m_lowCutModNote, ppqPosition); + m_lowCutLfos[Channel::Left].setPhase(phase); + m_lowCutLfos[Channel::Right].setPhase(phase); + } + if (m_highCutModTempoSync) + { + float phase = getSyncedPhase(m_highCutModNote, ppqPosition); + m_highCutLfos[Channel::Left].setPhase(phase); + m_highCutLfos[Channel::Right].setPhase(phase); + } } void FilterEngine::processSample(const float& inL, const float& inR, float& outL, float& outR) { - float lcL = m_lowCutLfos[Channel::Left].process(); - float lcR = m_lowCutLfos[Channel::Right].process(); - float hcL = m_highCutLfos[Channel::Left].process(); - float hcR = m_highCutLfos[Channel::Right].process(); + float lcL = m_lowCutLfos[Channel::Left].process(); + float lcR = m_lowCutLfos[Channel::Right].process(); + float hcL = m_highCutLfos[Channel::Left].process(); + float hcR = m_highCutLfos[Channel::Right].process(); - hcL = m_highCutModInvert ? -hcL : hcL; - hcR = m_highCutModInvert ? -hcR : hcR; + hcL = m_highCutModInvert ? -hcL : hcL; + hcR = m_highCutModInvert ? -hcR : hcR; - m_lowCutLfoValue.store(lcL, std::memory_order_relaxed); - m_highCutLfoValue.store(hcL, std::memory_order_relaxed); + m_lowCutLfoValue.store(lcL, std::memory_order_relaxed); + m_highCutLfoValue.store(hcL, std::memory_order_relaxed); #if JUCE_DEBUG - if (++m_scopeCounter >= scopeDownsample) { - m_scopeCounter = 0; - m_scopeBuffer.push(lcL, lcR, hcL, hcR); - } + if (++m_scopeCounter >= scopeDownsample) { + m_scopeCounter = 0; + m_scopeBuffer.push(lcL, lcR, hcL, hcR); + } #endif - m_lowCut.setCutoff(onePoleLowpass(modulatedCutoff(m_lowCutBase, lcL, m_lowCutModDepth), m_lowCutSmoothed[Channel::Left], m_cutoffSmoothCoeff), Channel::Left); - m_lowCut.setCutoff(onePoleLowpass(modulatedCutoff(m_lowCutBase, lcR, m_lowCutModDepth), m_lowCutSmoothed[Channel::Right], m_cutoffSmoothCoeff), Channel::Right); - m_highCut.setCutoff(onePoleLowpass(modulatedCutoff(m_highCutBase, hcL, m_highCutModDepth), m_highCutSmoothed[Channel::Left], m_cutoffSmoothCoeff), Channel::Left); - m_highCut.setCutoff(onePoleLowpass(modulatedCutoff(m_highCutBase, hcR, m_highCutModDepth), m_highCutSmoothed[Channel::Right], m_cutoffSmoothCoeff), Channel::Right); + m_lowCut.setCutoff(onePoleLowpass(modulatedCutoff(m_lowCutBase, lcL, m_lowCutModDepth), m_lowCutSmoothed[Channel::Left], m_cutoffSmoothCoeff), Channel::Left); + m_lowCut.setCutoff(onePoleLowpass(modulatedCutoff(m_lowCutBase, lcR, m_lowCutModDepth), m_lowCutSmoothed[Channel::Right], m_cutoffSmoothCoeff), Channel::Right); + m_highCut.setCutoff(onePoleLowpass(modulatedCutoff(m_highCutBase, hcL, m_highCutModDepth), m_highCutSmoothed[Channel::Left], m_cutoffSmoothCoeff), Channel::Left); + m_highCut.setCutoff(onePoleLowpass(modulatedCutoff(m_highCutBase, hcR, m_highCutModDepth), m_highCutSmoothed[Channel::Right], m_cutoffSmoothCoeff), Channel::Right); - float lowL, lowR; - m_lowCut.processSample(inL, inR, lowL, lowR); - m_highCut.processSample(lowL, lowR, outL, outR); + float lowL, lowR; + m_lowCut.processSample(inL, inR, lowL, lowR); + m_highCut.processSample(lowL, lowR, outL, outR); } diff --git a/Source/FilterEngine.h b/Source/FilterEngine.h index a298813..7597fcc 100644 --- a/Source/FilterEngine.h +++ b/Source/FilterEngine.h @@ -9,79 +9,77 @@ #include "LFOScopeBuffer.h" #endif -using FilterType = juce::dsp::StateVariableTPTFilterType; - class FilterEngine { public: - FilterEngine(); - ~FilterEngine() = default; + FilterEngine(); + ~FilterEngine() = default; - void prepareToPlay(double sampleRate, int samplesPerBlock) noexcept; - void reset() noexcept; + void prepareToPlay(double sampleRate, int samplesPerBlock) noexcept; + void reset() noexcept; - void update(const Parameters& params); - void processSample(const float& inL, const float& inR, float& outL, float& outR); + void update(const Parameters& params); + void processSample(const float& inL, const float& inR, float& outL, float& outR); - void updateLFOs(const double ppqPosition) noexcept; + void updateLFOs(const double ppqPosition) noexcept; - std::atomic& getLowCutLfoValue() { return m_lowCutLfoValue; } - std::atomic& getHighCutLfoValue() { return m_highCutLfoValue; } + std::atomic& getLowCutLfoValue() { return m_lowCutLfoValue; } + std::atomic& getHighCutLfoValue() { return m_highCutLfoValue; } #if JUCE_DEBUG - LFOScopeBuffer m_scopeBuffer; + LFOScopeBuffer m_scopeBuffer; #endif private: - StereoFilter m_lowCut{ FilterType::highpass }; - StereoFilter m_highCut{ FilterType::lowpass }; - juce::Random m_random; - - LFO m_lowCutLfos[2]; - LFO m_highCutLfos[2]; - - float m_lowCutBase = -1.0f; - float m_lowCutQ = -1.0f; - float m_lowCutModDepth = 0.0f; - float m_lowCutModRate = 0.0f; - float m_lowCutModPhase = 0.0f; - LFOShape m_lowCutModShape = LFOShape::Sine; - bool m_lowCutModTempoSync = false; - int m_lowCutModNote = 0; - - float m_highCutBase = -1.0f; - float m_highCutQ = -1.0f; - float m_highCutModDepth = 0.0f; - float m_highCutModRate = 0.0f; - float m_highCutModPhase = 0.0f; - LFOShape m_highCutModShape = LFOShape::Sine; - bool m_highCutModTempoSync = false; - int m_highCutModNote = 0; - - bool m_highCutModInvert = false; - - std::atomic m_lowCutLfoValue { 0.0f }; - std::atomic m_highCutLfoValue { 0.0f }; - - float m_lowCutSmoothed[2] = { Parameters::minFilterFreq, Parameters::minFilterFreq }; - float m_highCutSmoothed[2] = { Parameters::maxFilterFreq, Parameters::maxFilterFreq }; - float m_cutoffSmoothCoeff = 0.0f; + StereoFilter m_lowCut{ FilterType::highpass }; + StereoFilter m_highCut{ FilterType::lowpass }; + juce::Random m_random; + + LFO m_lowCutLfos[2]; + LFO m_highCutLfos[2]; + + float m_lowCutBase = -1.0f; + float m_lowCutQ = -1.0f; + float m_lowCutModDepth = 0.0f; + float m_lowCutModRate = 0.0f; + float m_lowCutModPhase = 0.0f; + LFOShape m_lowCutModShape = LFOShape::Sine; + bool m_lowCutModTempoSync = false; + int m_lowCutModNote = 0; + + float m_highCutBase = -1.0f; + float m_highCutQ = -1.0f; + float m_highCutModDepth = 0.0f; + float m_highCutModRate = 0.0f; + float m_highCutModPhase = 0.0f; + LFOShape m_highCutModShape = LFOShape::Sine; + bool m_highCutModTempoSync = false; + int m_highCutModNote = 0; + + bool m_highCutModInvert = false; + + std::atomic m_lowCutLfoValue { 0.0f }; + std::atomic m_highCutLfoValue { 0.0f }; + + float m_lowCutSmoothed[2] = { Parameters::minFilterFreq, Parameters::minFilterFreq }; + float m_highCutSmoothed[2] = { Parameters::maxFilterFreq, Parameters::maxFilterFreq }; + float m_cutoffSmoothCoeff = 0.0f; #if JUCE_DEBUG - static constexpr int scopeDownsample = 64; - int m_scopeCounter = 0; + static constexpr int scopeDownsample = 64; + int m_scopeCounter = 0; #endif - void setLowCutFilter(const Parameters& params); - void setHighCutFilter(const Parameters& params); + void setLowCutFilter(const Parameters& params); + void setHighCutFilter(const Parameters& params); - float modulatedCutoff(float base, float lfoValue, float modDepth); - void setFilterFreq(float freq, float& current); - void setFilterQ(float q, float& current, StereoFilter& filter); - void setFilterModDepth(float depth, float& current); - void setFilterModRate(float rate, float& current, LFO* lfos); - void setFilterModPhase(float phase, float& current, LFO* lfos); - void setFilterModShape(int shapeIndex, LFOShape& current, LFO* lfos); + float modulatedCutoff(float base, float lfoValue, float modDepth); + void setFilterFreq(float freq, float& current); + void setFilterQ(float q, float& current, StereoFilter& filter); + void setFilterModDepth(float depth, float& current); + void setFilterModRate(float rate, float& current, LFO* lfos); + void setFilterModPhase(float phase, float& current, LFO* lfos); + void setFilterModShape(int shapeIndex, LFOShape& current, LFO* lfos); - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FilterEngine) + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FilterEngine) }; diff --git a/Source/LFOIndicator.h b/Source/LFOIndicator.h index ede16fb..c84a6fd 100644 --- a/Source/LFOIndicator.h +++ b/Source/LFOIndicator.h @@ -4,32 +4,32 @@ class LFOIndicator : public juce::Component, private juce::Timer { public: - LFOIndicator(std::atomic& value) : m_value(value) - { - startTimerHz(60); - } + LFOIndicator(std::atomic& value) : m_value(value) + { + startTimerHz(60); + } - ~LFOIndicator() override { stopTimer(); } + ~LFOIndicator() override { stopTimer(); } - void paint(juce::Graphics& g) override - { - auto bounds = getLocalBounds().toFloat().reduced(1.0f); - float v = m_value.load(); - juce::Colour fill; - if (v >= 0.0f) - fill = juce::Colours::black.interpolatedWith(juce::Colours::green, v); - else - fill = juce::Colours::black.interpolatedWith(juce::Colours::red, -v); + void paint(juce::Graphics& g) override + { + auto bounds = getLocalBounds().toFloat().reduced(1.0f); + float v = m_value.load(); + juce::Colour fill; + if (v >= 0.0f) + fill = juce::Colours::black.interpolatedWith(juce::Colours::green, v); + else + fill = juce::Colours::black.interpolatedWith(juce::Colours::red, -v); - g.setColour(juce::Colours::darkgrey.darker(0.5f)); - g.drawEllipse(bounds, 1.0f); - g.setColour(fill); - g.fillEllipse(bounds.reduced(1.0f)); - } + g.setColour(juce::Colours::darkgrey.darker(0.5f)); + g.drawEllipse(bounds, 1.0f); + g.setColour(fill); + g.fillEllipse(bounds.reduced(1.0f)); + } private: - std::atomic& m_value; - void timerCallback() override { repaint(); } + std::atomic& m_value; + void timerCallback() override { repaint(); } - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LFOIndicator) + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LFOIndicator) }; diff --git a/Source/LFOScope.cpp b/Source/LFOScope.cpp index 077a4b8..24c472e 100644 --- a/Source/LFOScope.cpp +++ b/Source/LFOScope.cpp @@ -2,70 +2,70 @@ LFOScope::LFOScope(LFOScopeBuffer& buffer) : m_buffer(buffer) { - startTimerHz(refreshRate); + startTimerHz(s_refreshRate); } LFOScope::~LFOScope() { - stopTimer(); + stopTimer(); } void LFOScope::resized() {} void LFOScope::paint(juce::Graphics& g) { - auto bounds = getLocalBounds(); - int w = bounds.getWidth(); - int h = bounds.getHeight(); + auto bounds = getLocalBounds(); + int w = bounds.getWidth(); + int h = bounds.getHeight(); - g.fillAll(juce::Colour(0xff1a1a1a)); + g.fillAll(juce::Colour(0xff1a1a1a)); - g.setColour(juce::Colour(0xff333333)); - g.drawHorizontalLine(h / 2, 0.0f, (float)w); - g.drawHorizontalLine(h / 4, 0.0f, (float)w); - g.drawHorizontalLine(3 * h / 4, 0.0f, (float)w); - g.drawRect(bounds); + g.setColour(juce::Colour(0xff333333)); + g.drawHorizontalLine(h / 2, 0.0f, (float)w); + g.drawHorizontalLine(h / 4, 0.0f, (float)w); + g.drawHorizontalLine(3 * h / 4, 0.0f, (float)w); + g.drawRect(bounds); - int numSamples = std::min(w, LFOScopeBuffer::size); - int writeIdx = m_buffer.writeIndex.load(std::memory_order_acquire); + int numSamples = std::min(w, LFOScopeBuffer::size); + int writeIdx = m_buffer.writeIndex.load(std::memory_order_acquire); - const juce::Colour colours[LFOScopeBuffer::numChannels] = { - juce::Colour(0xff4fc3f7), // lowcut L — light blue - juce::Colour(0xff0288d1), // lowcut R — dark blue - juce::Colour(0xffffb74d), // highcut L — amber - juce::Colour(0xfff57c00), // highcut R — dark amber - }; + const juce::Colour colours[LFOScopeBuffer::numChannels] = { + juce::Colour(0xff4fc3f7), // lowcut L — light blue + juce::Colour(0xff0288d1), // lowcut R — dark blue + juce::Colour(0xffffb74d), // highcut L — amber + juce::Colour(0xfff57c00), // highcut R — dark amber + }; - const char* labels[LFOScopeBuffer::numChannels] = { - "LC L", "LC R", "HC L", "HC R" - }; + const char* labels[LFOScopeBuffer::numChannels] = { + "LC L", "LC R", "HC L", "HC R" + }; - for (int ch = 0; ch < LFOScopeBuffer::numChannels; ++ch) - drawChannel(g, ch, colours[ch], writeIdx, numSamples); + for (int ch = 0; ch < LFOScopeBuffer::numChannels; ++ch) + drawChannel(g, ch, colours[ch], writeIdx, numSamples); - g.setFont(11.0f); - for (int ch = 0; ch < LFOScopeBuffer::numChannels; ++ch) { - g.setColour(colours[ch]); - g.drawText(labels[ch], 4 + ch * 48, 4, 44, 14, juce::Justification::left); - } + g.setFont(11.0f); + for (int ch = 0; ch < LFOScopeBuffer::numChannels; ++ch) { + g.setColour(colours[ch]); + g.drawText(labels[ch], 4 + ch * 48, 4, 44, 14, juce::Justification::left); + } } void LFOScope::drawChannel(juce::Graphics& g, int channel, juce::Colour colour, int writeIdx, int numSamples) { - int h = getHeight(); - juce::Path path; + int h = getHeight(); + juce::Path path; - for (int x = 0; x < numSamples; ++x) { - int bufIdx = (writeIdx - numSamples + x + LFOScopeBuffer::size) & (LFOScopeBuffer::size - 1); - float value = m_buffer.data[channel][bufIdx]; - float y = juce::jmap(value, -1.0f, 1.0f, (float)(h - 1), 0.0f); + for (int x = 0; x < numSamples; ++x) { + int bufIdx = (writeIdx - numSamples + x + LFOScopeBuffer::size) & (LFOScopeBuffer::size - 1); + float value = m_buffer.data[channel][bufIdx]; + float y = juce::jmap(value, -1.0f, 1.0f, (float)(h - 1), 0.0f); - if (x == 0) - path.startNewSubPath(0.0f, y); - else - path.lineTo((float)x, y); - } + if (x == 0) + path.startNewSubPath(0.0f, y); + else + path.lineTo((float)x, y); + } - g.setColour(colour); - g.strokePath(path, juce::PathStrokeType(1.5f)); + g.setColour(colour); + g.strokePath(path, juce::PathStrokeType(1.5f)); } diff --git a/Source/LFOScope.h b/Source/LFOScope.h index e556f5b..bc239e7 100644 --- a/Source/LFOScope.h +++ b/Source/LFOScope.h @@ -5,19 +5,19 @@ class LFOScope : public juce::Component, private juce::Timer { public: - LFOScope(LFOScopeBuffer& buffer); - ~LFOScope() override; + LFOScope(LFOScopeBuffer& buffer); + ~LFOScope() override; - void paint(juce::Graphics&) override; - void resized() override; + void paint(juce::Graphics&) override; + void resized() override; private: - LFOScopeBuffer& m_buffer; + LFOScopeBuffer& m_buffer; - static constexpr int refreshRate = 30; + static constexpr int s_refreshRate = 30; - void timerCallback() override { repaint(); } - void drawChannel(juce::Graphics& g, int channel, juce::Colour colour, int writeIdx, int numSamples); + void timerCallback() override { repaint(); } + void drawChannel(juce::Graphics& g, int channel, juce::Colour colour, int writeIdx, int numSamples); - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LFOScope) + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LFOScope) }; diff --git a/Source/LFOScopeBuffer.h b/Source/LFOScopeBuffer.h index bd537e3..3002e03 100644 --- a/Source/LFOScopeBuffer.h +++ b/Source/LFOScopeBuffer.h @@ -2,18 +2,18 @@ #include struct LFOScopeBuffer { - static constexpr int numChannels = 4; - static constexpr int size = 4096; + static constexpr int numChannels = 4; + static constexpr int size = 4096; - float data[numChannels][size] = {}; - std::atomic writeIndex { 0 }; + float data[numChannels][size] = {}; + std::atomic writeIndex { 0 }; - void push(float lcL, float lcR, float hcL, float hcR) noexcept { - int idx = writeIndex.load(std::memory_order_relaxed); - data[0][idx] = lcL; - data[1][idx] = lcR; - data[2][idx] = hcL; - data[3][idx] = hcR; - writeIndex.store((idx + 1) & (size - 1), std::memory_order_release); - } + void push(float lcL, float lcR, float hcL, float hcR) noexcept { + int idx = writeIndex.load(std::memory_order_relaxed); + data[0][idx] = lcL; + data[1][idx] = lcR; + data[2][idx] = hcL; + data[3][idx] = hcR; + writeIndex.store((idx + 1) & (size - 1), std::memory_order_release); + } }; diff --git a/Source/Selector.cpp b/Source/Selector.cpp index b9aa86c..0abccf4 100644 --- a/Source/Selector.cpp +++ b/Source/Selector.cpp @@ -8,48 +8,48 @@ Selector::Selector(const juce::String& labelText, float fontSize, int leftLabelWidth, int height) - : m_hasLabel(labelText.isNotEmpty()), m_leftLabelWidth(leftLabelWidth) + : m_hasLabel(labelText.isNotEmpty()), m_leftLabelWidth(leftLabelWidth) { - if (fontSize > 0.0f) - { - m_laf.fontSize = fontSize; - m_comboBox.setLookAndFeel(&m_laf); - } + if (fontSize > 0.0f) + { + m_laf.fontSize = fontSize; + m_comboBox.setLookAndFeel(&m_laf); + } - m_comboBox.addItemList(items, 1); - m_comboBox.setEditableText(false); - addAndMakeVisible(m_comboBox); + m_comboBox.addItemList(items, 1); + m_comboBox.setEditableText(false); + addAndMakeVisible(m_comboBox); - m_attachment = std::make_unique( - apvts, parameterID.getParamID(), m_comboBox); + m_attachment = std::make_unique( + apvts, parameterID.getParamID(), m_comboBox); - if (m_hasLabel) - { - m_label.setText(labelText, juce::NotificationType::dontSendNotification); - addAndMakeVisible(m_label); - } + if (m_hasLabel) + { + m_label.setText(labelText, juce::NotificationType::dontSendNotification); + addAndMakeVisible(m_label); + } - const int autoH = (m_hasLabel && m_leftLabelWidth == 0 ? s_labelH : 0) + s_comboH; - setSize(width, height > 0 ? height : autoH); + const int autoH = (m_hasLabel && m_leftLabelWidth == 0 ? s_labelH : 0) + s_comboH; + setSize(width, height > 0 ? height : autoH); } Selector::~Selector() { - m_comboBox.setLookAndFeel(nullptr); + m_comboBox.setLookAndFeel(nullptr); } void Selector::resized() { - if (m_hasLabel && m_leftLabelWidth > 0) - { - m_label.setBounds(0, 0, m_leftLabelWidth, getHeight()); - m_comboBox.setBounds(m_leftLabelWidth, 0, getWidth() - m_leftLabelWidth, getHeight()); - } - else - { - const int comboY = m_hasLabel ? s_labelH : 0; - if (m_hasLabel) - m_label.setBounds(0, 0, getWidth(), s_labelH); - m_comboBox.setBounds(0, comboY, getWidth(), s_comboH); - } + if (m_hasLabel && m_leftLabelWidth > 0) + { + m_label.setBounds(0, 0, m_leftLabelWidth, getHeight()); + m_comboBox.setBounds(m_leftLabelWidth, 0, getWidth() - m_leftLabelWidth, getHeight()); + } + else + { + const int comboY = m_hasLabel ? s_labelH : 0; + if (m_hasLabel) + m_label.setBounds(0, 0, getWidth(), s_labelH); + m_comboBox.setBounds(0, comboY, getWidth(), s_comboH); + } } diff --git a/Source/Selector.h b/Source/Selector.h index 07702ac..2116ce6 100644 --- a/Source/Selector.h +++ b/Source/Selector.h @@ -5,37 +5,37 @@ class Selector : public juce::Component { public: - Selector(const juce::String& labelText, - juce::AudioProcessorValueTreeState& apvts, - const juce::ParameterID& parameterID, - const juce::StringArray& items, - int width = 60, - float fontSize = 0.0f, - int leftLabelWidth = 0, - int height = 0); - ~Selector() override; + Selector(const juce::String& labelText, + juce::AudioProcessorValueTreeState& apvts, + const juce::ParameterID& parameterID, + const juce::StringArray& items, + int width = 60, + float fontSize = 0.0f, + int leftLabelWidth = 0, + int height = 0); + ~Selector() override; - void resized() override; + void resized() override; private: - static constexpr int s_labelH = 14; - static constexpr int s_comboH = 20; + static constexpr int s_labelH = 14; + static constexpr int s_comboH = 20; - struct SelectorLookAndFeel : public juce::LookAndFeel_V4 - { - float fontSize = 0.0f; - juce::Font getComboBoxFont(juce::ComboBox&) override - { - return juce::Font(juce::FontOptions().withHeight(fontSize)); - } - }; + struct SelectorLookAndFeel : public juce::LookAndFeel_V4 + { + float fontSize = 0.0f; + juce::Font getComboBoxFont(juce::ComboBox&) override + { + return juce::Font(juce::FontOptions().withHeight(fontSize)); + } + }; - SelectorLookAndFeel m_laf; - juce::Label m_label; - juce::ComboBox m_comboBox; - std::unique_ptr m_attachment; - bool m_hasLabel; - int m_leftLabelWidth = 0; + SelectorLookAndFeel m_laf; + juce::Label m_label; + juce::ComboBox m_comboBox; + std::unique_ptr m_attachment; + bool m_hasLabel; + int m_leftLabelWidth = 0; - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Selector) + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Selector) }; diff --git a/Source/StereoFilter.cpp b/Source/StereoFilter.cpp index cb4c959..f889824 100644 --- a/Source/StereoFilter.cpp +++ b/Source/StereoFilter.cpp @@ -2,39 +2,39 @@ StereoFilter::StereoFilter(FilterType type) { - for (auto& f : m_filters) - f.setType(type); + for (auto& f : m_filters) + f.setType(type); } void StereoFilter::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept { - juce::dsp::ProcessSpec spec; - spec.sampleRate = sampleRate; - spec.maximumBlockSize = juce::uint32(samplesPerBlock); - spec.numChannels = 1; - for (auto& f : m_filters) - f.prepare(spec); + juce::dsp::ProcessSpec spec; + spec.sampleRate = sampleRate; + spec.maximumBlockSize = juce::uint32(samplesPerBlock); + spec.numChannels = 1; + for (auto& f : m_filters) + f.prepare(spec); } void StereoFilter::reset() noexcept { - for (auto& f : m_filters) - f.reset(); + for (auto& f : m_filters) + f.reset(); } void StereoFilter::setCutoff(float freq, Channel channel) { - m_filters[channel].setCutoffFrequency(freq); + m_filters[channel].setCutoffFrequency(freq); } void StereoFilter::setQ(float q) { - for (auto& f : m_filters) - f.setResonance(q); + for (auto& f : m_filters) + f.setResonance(q); } void StereoFilter::processSample(const float& inL, const float& inR, float& outL, float& outR) { - outL = m_filters[Channel::Left].processSample(0, inL); - outR = m_filters[Channel::Right].processSample(0, inR); + outL = m_filters[Channel::Left].processSample(0, inL); + outR = m_filters[Channel::Right].processSample(0, inR); } diff --git a/Source/StereoFilter.h b/Source/StereoFilter.h index 2fa2d5a..f38ee7e 100644 --- a/Source/StereoFilter.h +++ b/Source/StereoFilter.h @@ -8,18 +8,18 @@ using FilterType = juce::dsp::StateVariableTPTFilterType; class StereoFilter { public: - StereoFilter(FilterType type = FilterType::lowpass); - ~StereoFilter() = default; + StereoFilter(FilterType type = FilterType::lowpass); + ~StereoFilter() = default; - void prepareToPlay(double sampleRate, int samplesPerBlock) noexcept; - void reset() noexcept; + void prepareToPlay(double sampleRate, int samplesPerBlock) noexcept; + void reset() noexcept; - void processSample(const float& inL, const float& inR, float& outL, float& outR); - void setCutoff(float freq, Channel channel); - void setQ(float q); + void processSample(const float& inL, const float& inR, float& outL, float& outR); + void setCutoff(float freq, Channel channel); + void setQ(float q); private: - Filter m_filters[2]; + Filter m_filters[2]; - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(StereoFilter) + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(StereoFilter) }; diff --git a/Source/Sync.h b/Source/Sync.h index 7b0adf4..2c46334 100644 --- a/Source/Sync.h +++ b/Source/Sync.h @@ -3,45 +3,45 @@ inline static std::array delayNoteLengthMultipliers = { - 0.125, // 0 = 1/32 - 0.5 / 3.0, // 1 = 1/16 triplet - 0.1875, // 2 = 1/32 dotted - 0.25, // 3 = 1/16 - 1.0 / 3.0, // 4 = 1/8 triplet - 0.375, // 5 = 1/16 dotted - 0.5, // 6 = 1/8 - 2.0 / 3.0, // 7 = 1/4 triplet - 0.75, // 8 = 1/8 dotted - 1.0, // 9 = 1/4 - 4.0 / 3.0, // 10 = 1/2 triplet - 1.5, // 11 = 1/4 dotted - 2.0, // 12 = 1/2 - 8.0 / 3.0, // 13 = 1/1 triplet - 3.0, // 14 = 1/2 dotted - 4.0, // 15 = 1/1 + 0.125, // 0 = 1/32 + 0.5 / 3.0, // 1 = 1/16 triplet + 0.1875, // 2 = 1/32 dotted + 0.25, // 3 = 1/16 + 1.0 / 3.0, // 4 = 1/8 triplet + 0.375, // 5 = 1/16 dotted + 0.5, // 6 = 1/8 + 2.0 / 3.0, // 7 = 1/4 triplet + 0.75, // 8 = 1/8 dotted + 1.0, // 9 = 1/4 + 4.0 / 3.0, // 10 = 1/2 triplet + 1.5, // 11 = 1/4 dotted + 2.0, // 12 = 1/2 + 8.0 / 3.0, // 13 = 1/1 triplet + 3.0, // 14 = 1/2 dotted + 4.0, // 15 = 1/1 }; inline static std::array modNoteLengthMultipliers = { - 0.5, // 0 = 1/8 - 0.75, // 1 = 1/8 dotted - 1.0, // 2 = 1/4 - 1.5, // 3 = 1/4 dotted - 2.0, // 4 = 1/2 - 3.0, // 5 = 1/2 dotted - 4.0, // 6 = 1/1 - 6.0, // 7 = 1/1 dotted - 8.0, // 8 = 2/1 - 12.0, // 9 = 3/1 - 16.0, // 10 = 4/1 - 20.0, // 11 = 5/1 - 24.0, // 12 = 6/1 - 28.0, // 13 = 7/1 - 32.0, // 14 = 8/1 + 0.5, // 0 = 1/8 + 0.75, // 1 = 1/8 dotted + 1.0, // 2 = 1/4 + 1.5, // 3 = 1/4 dotted + 2.0, // 4 = 1/2 + 3.0, // 5 = 1/2 dotted + 4.0, // 6 = 1/1 + 6.0, // 7 = 1/1 dotted + 8.0, // 8 = 2/1 + 12.0, // 9 = 3/1 + 16.0, // 10 = 4/1 + 20.0, // 11 = 5/1 + 24.0, // 12 = 6/1 + 28.0, // 13 = 7/1 + 32.0, // 14 = 8/1 }; inline float getSyncedPhase(int index, double ppqPosition) { double cycleLengthInBeats = modNoteLengthMultipliers[size_t(index)]; return static_cast(std::fmod(ppqPosition, cycleLengthInBeats) / cycleLengthInBeats); -} \ No newline at end of file +} From 853c4c9167d5afba871967a89bfff1b243113b23 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 6 May 2026 09:08:00 +0300 Subject: [PATCH 30/31] fix version number string in cmakelists, hardcode beta version number to editor --- CMakeLists.txt | 2 +- Source/PluginEditor.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f74f5b..0aa96b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,7 +41,7 @@ set(VST3_DEV_COPY_DIR "" CACHE PATH "If set, copies the Debug VST3 build here af ### Plugin Project Setup ### juce_add_plugin(viive - VERSION 0.2.beta-1 + VERSION 0.2.0 FORMATS VST3 Standalone PRODUCT_NAME "viive" COMPANY_NAME "trsctr" diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index e03d99d..3991b74 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -102,7 +102,9 @@ void ViiveAudioProcessorEditor::paint (juce::Graphics& g) g.fillAll(juce::Colours::darkgrey); g.setColour(juce::Colours::white); g.setFont(12.0f); - g.drawText("viive " JucePlugin_VersionString, getLocalBounds().reduced(5), juce::Justification::topRight); + g.drawText("viive 0.2.beta-1", getLocalBounds().reduced(5), juce::Justification::topRight); + +// g.drawText("viive " JucePlugin_VersionString, getLocalBounds().reduced(5), juce::Justification::topRight); } void ViiveAudioProcessorEditor::resized() From 1c2ef85f0d822e58197746a01128c5026b1cf14e Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 6 May 2026 09:08:37 +0300 Subject: [PATCH 31/31] remove unused variables from Parameters.h --- Source/Parameters.h | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Source/Parameters.h b/Source/Parameters.h index d292683..f9dfbc4 100644 --- a/Source/Parameters.h +++ b/Source/Parameters.h @@ -147,11 +147,6 @@ class Parameters { juce::LinearSmoothedValue m_chorusModRateSmoother; juce::LinearSmoothedValue m_chorusModDepthSmoother; - float m_targetDelayTimeL = 0.0f; - float m_targetDelayTimeR = 0.0f; - - float m_coeff = 0.0f; - float m_gain = 0.0f; float m_delayTimeL = 0.0f; float m_delayTimeR = 0.0f;