Skip to content
34 changes: 24 additions & 10 deletions complete/tremolo_plugin/include/Tremolo/Tremolo.h
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
#pragma once

namespace tremolo {
enum class ApplySmoothing { no, yes };

class Tremolo {
public:
enum class LfoWaveform : size_t {
sine = 0,
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{
Expand All @@ -31,16 +29,24 @@ class Tremolo {
lfoSamples.resize(4u * static_cast<size_t>(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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this again, maybe this should be named setModulationRateHz so that the units are slightly more obvious

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

For more clarity, I'd use the ClosedRangeValue<float> or Frequency classes from my wolfsound_dsp_utils library. Then, you could write

tremolo.setModulationRate(Frequency{p.rate});

or even

tremolo.setModulationRate(10_Hz);

I don't like using floats as parameters in general!

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<float>& buffer) noexcept {
Expand Down Expand Up @@ -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<float>::halfPi;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider instead introducing a new variable with a different name (maybe just offsetPhase), so that phase always refers to the initial parameter value. I expect this will be marginally clearer

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// Source:
// https://thewolfsound.com/sine-saw-square-triangle-pulse-basic-waveforms-in-synthesis/#triangle
const auto ft = phase / juce::MathConstants<float>::twoPi;
const auto ft = offsetPhase / juce::MathConstants<float>::twoPi;
return 4.f * std::abs(ft - std::floor(ft + 0.5f)) - 1.f;
}

Expand All @@ -146,7 +157,10 @@ class Tremolo {
}

std::array<juce::dsp::Oscillator<float>, 2u> lfos{
juce::dsp::Oscillator<float>{[](auto phase) { return std::sin(phase); }},
juce::dsp::Oscillator<float>{[](auto phase) {
// start phase is -pi -> change it to 0 to match the mathematical sine
return std::sin(phase + juce::MathConstants<float>::pi);
}},
juce::dsp::Oscillator<float>{triangle}};

LfoWaveform currentLfo = LfoWaveform::sine;
Expand Down
30 changes: 25 additions & 5 deletions complete/tremolo_plugin/source/PluginProcessor.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

#include "tremolo_plugin/include/Tremolo/Tremolo.h"
namespace tremolo {
PluginProcessor::PluginProcessor()
: AudioProcessor(
Expand Down Expand Up @@ -107,16 +108,26 @@ void PluginProcessor::processBlock(juce::AudioBuffer<float>& 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<Tremolo::LfoWaveform>(parameters.waveform.getIndex()));
static_cast<Tremolo::LfoWaveform>(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;
}

Expand Down Expand Up @@ -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<Tremolo::LfoWaveform>(parameters.waveform.getIndex()),
ApplySmoothing::no);
tremolo.setModulationRateHz(parameters.rate, ApplySmoothing::no);
}

Parameters& PluginProcessor::getParameterRefs() noexcept {
Expand Down