diff --git a/complete/tremolo_plugin/include/Tremolo/Tremolo.h b/complete/tremolo_plugin/include/Tremolo/Tremolo.h index 327d44d..550678b 100644 --- a/complete/tremolo_plugin/include/Tremolo/Tremolo.h +++ b/complete/tremolo_plugin/include/Tremolo/Tremolo.h @@ -1,6 +1,8 @@ #pragma once namespace tremolo { +enum class ApplySmoothing { no, yes }; + class Tremolo { public: enum class LfoWaveform : size_t { @@ -8,11 +10,7 @@ class Tremolo { triangle = 1, }; - Tremolo() { - for (auto& lfo : lfos) { - lfo.setFrequency(5.f /* Hz */, true); - } - } + Tremolo() { setModulationRateHz(5.f, ApplySmoothing::no); } void prepare(double sampleRate, int expectedMaxFramesPerBlock) { const juce::dsp::ProcessSpec processSpec{ @@ -31,16 +29,24 @@ class Tremolo { lfoSamples.resize(4u * static_cast(expectedMaxFramesPerBlock)); } - void setModulationRate(float rateHz) noexcept { + void setModulationRateHz( + float rateHz, + ApplySmoothing applySmoothing = ApplySmoothing::yes) noexcept { + const auto force = applySmoothing == ApplySmoothing::no; for (auto& lfo : lfos) { - lfo.setFrequency(rateHz); + lfo.setFrequency(rateHz, force); } } - void setLfoWaveform(LfoWaveform waveform) { + void setLfoWaveform(LfoWaveform waveform, + ApplySmoothing applySmoothing = ApplySmoothing::yes) { jassert(waveform == LfoWaveform::sine || waveform == LfoWaveform::triangle); lfoToSet = waveform; + + if (applySmoothing == ApplySmoothing::no) { + currentLfo = waveform; + } } void process(juce::AudioBuffer& buffer) noexcept { @@ -118,9 +124,14 @@ class Tremolo { static constexpr auto modulationDepth = 0.4f; static float triangle(float phase) { + // offset the phase by pi/2 to return 0 if phase equals 0 + // and match the sine waveform + // (otherwise, the waveform starts at 1) + const auto offsetPhase = phase - juce::MathConstants::halfPi; + // Source: // https://thewolfsound.com/sine-saw-square-triangle-pulse-basic-waveforms-in-synthesis/#triangle - const auto ft = phase / juce::MathConstants::twoPi; + const auto ft = offsetPhase / juce::MathConstants::twoPi; return 4.f * std::abs(ft - std::floor(ft + 0.5f)) - 1.f; } @@ -146,7 +157,10 @@ class Tremolo { } std::array, 2u> lfos{ - juce::dsp::Oscillator{[](auto phase) { return std::sin(phase); }}, + juce::dsp::Oscillator{[](auto phase) { + // start phase is -pi -> change it to 0 to match the mathematical sine + return std::sin(phase + juce::MathConstants::pi); + }}, juce::dsp::Oscillator{triangle}}; LfoWaveform currentLfo = LfoWaveform::sine; diff --git a/complete/tremolo_plugin/source/PluginProcessor.cpp b/complete/tremolo_plugin/source/PluginProcessor.cpp index e5d6886..b553193 100644 --- a/complete/tremolo_plugin/source/PluginProcessor.cpp +++ b/complete/tremolo_plugin/source/PluginProcessor.cpp @@ -1,4 +1,5 @@ +#include "tremolo_plugin/include/Tremolo/Tremolo.h" namespace tremolo { PluginProcessor::PluginProcessor() : AudioProcessor( @@ -107,16 +108,26 @@ void PluginProcessor::processBlock(juce::AudioBuffer& buffer, buffer.clear(channelToClear, 0, buffer.getNumSamples()); } + const auto bypassedAndNotTransitioning = + parameters.bypassed.get() && !bypassTransitionSmoother.isTransitioning(); + const auto applySmoothing = + bypassedAndNotTransitioning ? ApplySmoothing::no : ApplySmoothing::yes; + // update the parameters - tremolo.setModulationRate(parameters.rate); + // Skip smoothing if fully bypassed to avoid LFO waveform morphing + // when parameters change under bypass ON. + // For example, if the LFO waveform is the sine, and the user selects + // the triangle under bypass ON, they will see a curved triangle slope + // on toggling bypass OFF, which is unexpected. + tremolo.setModulationRateHz(parameters.rate, applySmoothing); tremolo.setLfoWaveform( - static_cast(parameters.waveform.getIndex())); + static_cast(parameters.waveform.getIndex()), + applySmoothing); bypassTransitionSmoother.setBypass(parameters.bypassed); - if (parameters.bypassed.get() && - !bypassTransitionSmoother.isTransitioning()) { - // avoid processing if the plugin is bypassed + if (bypassedAndNotTransitioning) { + // avoid processing if the plugin is fully bypassed return; } @@ -153,7 +164,16 @@ void PluginProcessor::setStateInformation(const void* data, int sizeInBytes) { DBG(result.getErrorMessage()); } + // Skip smoothing to avoid LFO waveform morphing + // when loading a project or a preset. + // For example, the default LFO waveform is the sine. If the project or preset + // has the triangle selected, the user will see a curved triangle slope + // on load, which is unexpected. bypassTransitionSmoother.setBypassForced(parameters.bypassed); + tremolo.setLfoWaveform( + static_cast(parameters.waveform.getIndex()), + ApplySmoothing::no); + tremolo.setModulationRateHz(parameters.rate, ApplySmoothing::no); } Parameters& PluginProcessor::getParameterRefs() noexcept {