diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7d67b9c..83ae0e2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -103,7 +103,7 @@ include(SharedCodeDefaults)
# Just ensure you employ CONFIGURE_DEPENDS so the build system picks up changes
# If you want to appease the CMake gods and avoid globs, manually add files like so:
# set(SourceFiles Source/PluginEditor.h Source/PluginProcessor.h Source/PluginEditor.cpp Source/PluginProcessor.cpp)
-file(GLOB_RECURSE SourceFiles CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/source/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/source/*.h")
+file(GLOB_RECURSE SourceFiles CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/source/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/source/*.h" "${CMAKE_CURRENT_SOURCE_DIR}/source/*.hpp")
target_sources(SharedCode INTERFACE ${SourceFiles})
# Adds a BinaryData target for embedding assets into the binary
diff --git a/source/DSP/PhatEffectsProcessor.cpp b/source/DSP/PhatEffectsProcessor.cpp
new file mode 100644
index 0000000..e69de29
diff --git a/source/DSP/PhatEffectsProcessor.hpp b/source/DSP/PhatEffectsProcessor.hpp
new file mode 100644
index 0000000..c02352a
--- /dev/null
+++ b/source/DSP/PhatEffectsProcessor.hpp
@@ -0,0 +1,240 @@
+/*
+ ==============================================================================
+
+ ProPhat is a virtual synthesizer inspired by the Prophet REV2.
+ Copyright (C) 2024 Vincent Berthiaume
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+ ==============================================================================
+*/
+
+#pragma once
+
+#include "../Utility/Helpers.h"
+#include "PhatVerb.h"
+#include "ProPhatVoice.h"
+
+enum class EffectType
+{
+ verb = 0,
+ chorus,
+ transitioning
+};
+
+template
+class EffectsCrossfadeProcessor
+{
+public:
+ EffectsCrossfadeProcessor() = default;
+
+ void prepare (const juce::dsp::ProcessSpec& spec)
+ {
+ smoothedGain.reset (spec.sampleRate, .1);
+ }
+
+ void changeEffect()
+ {
+ if (curEffect == EffectType::verb)
+ curEffect = EffectType::chorus;
+ else if (curEffect == EffectType::chorus)
+ curEffect = EffectType::verb;
+ else
+ jassertfalse;
+
+ if (curEffect == EffectType::verb)
+ smoothedGain.setTargetValue (1.);
+ else
+ smoothedGain.setTargetValue (0.);
+
+ //this does not resolve the click we get on the first transition
+ //if (curEffect == EffectType::verb)
+ // smoothedGain.setTargetValue (0.);
+ //else
+ // smoothedGain.setTargetValue (1.);
+ };
+
+ EffectType getCurrentEffectType() const
+ {
+ //TODO: this isn't atomic. Try lock?
+ if (smoothedGain.isSmoothing())
+ return EffectType::transitioning;
+
+ return curEffect;
+ }
+
+ /** so this needs to be a previous and next buffer, and the smoothing needs to always be 1 -> 0.
+ * I don' think this class should know anything about the processors... actually it should just get
+ * references to the processors, or even just their processed outputs?
+ */
+ void process (const juce::AudioBuffer& leftBuffer,
+ const juce::AudioBuffer& rightBuffer,
+ juce::AudioBuffer& outputBuffer)
+ {
+ jassert (leftBuffer.getNumChannels() == rightBuffer.getNumChannels() && rightBuffer.getNumChannels() == outputBuffer.getNumChannels());
+ jassert (leftBuffer.getNumSamples() == rightBuffer.getNumSamples() && rightBuffer.getNumSamples() == outputBuffer.getNumSamples());
+
+ const auto channels = outputBuffer.getNumChannels();
+ const auto samples = outputBuffer.getNumSamples();
+
+ for (int channel = 0; channel < channels; ++channel)
+ {
+ for (int sample = 0; sample < samples; ++sample)
+ {
+ // obtain the input samples from their respective buffers
+ const auto left = leftBuffer.getSample (channel, sample);
+ const auto right = rightBuffer.getSample (channel, sample);
+
+ // get the next gain value in the smoothed ramp towards target
+ const auto gain = smoothedGain.getNextValue();
+ DBG(gain);
+
+ // calculate the output sample as a mix of left and right
+ auto output = left * gain + right * (1.0 - gain);
+
+ // store the output sample value
+ outputBuffer.setSample (channel, sample, static_cast (output));
+ }
+ }
+ }
+
+private:
+ juce::SmoothedValue smoothedGain;
+
+ //TODO: this needs to be stored and retreived from the state
+ EffectType curEffect = EffectType::verb;
+
+ //changing the default curEffect to chorus DOES fix the click on first transition. Weird man
+ //EffectType curEffect = EffectType::chorus;
+};
+
+//==================================================
+
+template
+class EffectsProcessor
+{
+public:
+ EffectsProcessor()
+ {
+ verbWrapper = std::make_unique, T>>();
+ verbWrapper->processor.setParameters (reverbParams);
+
+ chorusWrapper = std::make_unique, T>>();
+ }
+
+ void prepare (const juce::dsp::ProcessSpec& spec)
+ {
+ // pre-allocate!
+ fade_buffer1.setSize (spec.numChannels, spec.maximumBlockSize);
+ fade_buffer2.setSize (spec.numChannels, spec.maximumBlockSize);
+
+ verbWrapper->prepare (spec);
+ chorusWrapper->prepare (spec);
+ effectCrossFader.prepare (spec);
+ }
+
+ template
+ void setEffectParam (juce::StringRef parameterID, T newValue)
+ {
+ const auto curEffect { effectCrossFader.getCurrentEffectType() };
+ if (parameterID == ProPhatParameterIds::effectParam1ID.getParamID())
+ {
+ if (curEffect == EffectType::verb)
+ {
+ reverbParams.roomSize = newValue;
+ verbWrapper->processor.setParameters (reverbParams);
+ }
+ else if (curEffect == EffectType::chorus)
+ {
+ chorusWrapper->processor.setRate (static_cast (99.9) * newValue);
+ }
+ }
+ else if (parameterID == ProPhatParameterIds::effectParam2ID.getParamID())
+ {
+ if (curEffect == EffectType::verb)
+ {
+ reverbParams.wetLevel = newValue;
+ verbWrapper->processor.setParameters (reverbParams);
+ }
+ else if (curEffect == EffectType::chorus)
+ {
+ chorusWrapper->processor.setDepth (newValue);
+ chorusWrapper->processor.setMix (newValue);
+ }
+ }
+ else
+ jassertfalse; //unknown effect parameter!
+ }
+
+ void changeEffect()
+ {
+ effectCrossFader.changeEffect();
+ };
+
+ void process (juce::AudioBuffer& buffer, int startSample, int numSamples)
+ {
+ //TODO: surround with trylock or something
+ const auto currentEffectType { effectCrossFader.getCurrentEffectType() };
+
+ if (currentEffectType == EffectType::transitioning)
+ {
+ //copy the OG buffer into the individual processor ones
+ fade_buffer1 = buffer;
+ fade_buffer2 = buffer;
+
+ //make the individual blocks and process
+ auto audioBlock1 { juce::dsp::AudioBlock (fade_buffer1).getSubBlock ((size_t) startSample, (size_t) numSamples) };
+ auto context1 { juce::dsp::ProcessContextReplacing (audioBlock1) };
+ verbWrapper->process (context1);
+
+ auto audioBlock2 { juce::dsp::AudioBlock (fade_buffer2).getSubBlock ((size_t) startSample, (size_t) numSamples) };
+ auto context2 { juce::dsp::ProcessContextReplacing (audioBlock2) };
+ chorusWrapper->process (context2);
+
+ //crossfade the 2 effects
+ effectCrossFader.process (fade_buffer1, fade_buffer2, buffer);
+
+ return;
+ }
+
+ auto audioBlock { juce::dsp::AudioBlock (buffer).getSubBlock ((size_t) startSample, (size_t) numSamples) };
+ auto context { juce::dsp::ProcessContextReplacing (audioBlock) };
+
+ if (currentEffectType == EffectType::verb)
+ verbWrapper->process (context);
+ else if (currentEffectType == EffectType::chorus)
+ chorusWrapper->process (context);
+ else
+ jassertfalse; //unknown effect!!
+ }
+
+private:
+ std::unique_ptr, T>> chorusWrapper;
+
+ std::unique_ptr, T>> verbWrapper;
+ PhatVerbParameters reverbParams {
+ //manually setting all these because we need to set the default room size and wet level to 0 if we want to be able to retrieve
+ //these values from a saved state. If they are saved as 0 in the state, the event callback will not be propagated because
+ //the change isn't forced-pushed
+ 0.0f, //< Room size, 0 to 1.0, where 1.0 is big, 0 is small.
+ 0.5f, //< Damping, 0 to 1.0, where 0 is not damped, 1.0 is fully damped.
+ 0.0f, //< Wet level, 0 to 1.0
+ 0.4f, //< Dry level, 0 to 1.0
+ 1.0f, //< Reverb width, 0 to 1.0, where 1.0 is very wide.
+ 0.0f //< Freeze mode - values < 0.5 are "normal" mode, values > 0.5 put the reverb into a continuous feedback loop.
+ };
+
+ juce::AudioBuffer fade_buffer1, fade_buffer2;
+ EffectsCrossfadeProcessor effectCrossFader;
+};
\ No newline at end of file
diff --git a/source/DSP/ProPhatSynthesiser.h b/source/DSP/ProPhatSynthesiser.h
index 5f7a39b..47520d8 100644
--- a/source/DSP/ProPhatSynthesiser.h
+++ b/source/DSP/ProPhatSynthesiser.h
@@ -22,251 +22,11 @@
#pragma once
+#include "PhatEffectsProcessor.hpp"
#include "ProPhatVoice.h"
#include "PhatVerb.h"
#include "../Utility/Helpers.h"
-enum class EffectType
-{
- verb = 0,
- chorus,
- transitioning
-};
-
-template
-class EffectsCrossfadeProcessor
-{
-public:
-
-
- EffectsCrossfadeProcessor() = default;
-
- void prepare (const juce::dsp::ProcessSpec& spec)
- {
- smoothedGain.reset (spec.sampleRate, .1);
- }
-
- void changeEffect()
- {
- if (curEffect == EffectType::verb)
- curEffect = EffectType::chorus;
- else if (curEffect == EffectType::chorus)
- curEffect = EffectType::verb;
- else
- jassertfalse;
-
- if (curEffect == EffectType::verb)
- smoothedGain.setTargetValue (1.);
- else
- smoothedGain.setTargetValue (0.);
-
- //this does not resolve the click we get on the first transition
- //if (curEffect == EffectType::verb)
- // smoothedGain.setTargetValue (0.);
- //else
- // smoothedGain.setTargetValue (1.);
- };
-
- EffectType getCurrentEffectType() const
- {
- //TODO: this isn't atomic. Try lock?
- if (smoothedGain.isSmoothing())
- return EffectType::transitioning;
-
- return curEffect;
- }
-
- /**
- Applies the crossfade.
-
- Output buffer can be the same buffer as either of the inputs.
-
- All buffers should have the same number of channels and samples as each
- other, but if not, then the minimum number of channels/samples will be
- used.
-
- @param leftBuffer The left input buffer to read from.
- @param rightBuffer The right input buffer to read from.
- @param outputBuffer The buffer in which to store the result of the crossfade.
- */
- void process (const juce::AudioBuffer& leftBuffer,
- const juce::AudioBuffer& rightBuffer,
- juce::AudioBuffer& outputBuffer)
- {
-#if 1
- jassert (leftBuffer.getNumChannels() == rightBuffer.getNumChannels()
- && rightBuffer.getNumChannels() == outputBuffer.getNumChannels());
- jassert (leftBuffer.getNumSamples() == rightBuffer.getNumSamples()
- && rightBuffer.getNumSamples() == outputBuffer.getNumSamples());
-
- const auto channels = outputBuffer.getNumChannels();
- const auto samples = outputBuffer.getNumSamples();
-
-#else
- // find the lowest number of channels available across all buffers
- const auto channels = std::min ({ leftBuffer.getNumChannels(),
- rightBuffer.getNumChannels(),
- outputBuffer.getNumChannels() });
-
- // find the lowest number of samples available across all buffers
- const auto samples = std::min ({ leftBuffer.getNumSamples(),
- rightBuffer.getNumSamples(),
- outputBuffer.getNumSamples() });
-#endif
-
- for (int channel = 0; channel < channels; ++channel)
- {
- for (int sample = 0; sample < samples; ++sample)
- {
- // obtain the input samples from their respective buffers
- const auto left = leftBuffer.getSample (channel, sample);
- const auto right = rightBuffer.getSample (channel, sample);
-
- // get the next gain value in the smoothed ramp towards target
- const auto gain = smoothedGain.getNextValue();
-
- // calculate the output sample as a mix of left and right
- auto output = left * gain + right * (1.0 - gain);
-
- // store the output sample value
- outputBuffer.setSample (channel, sample, static_cast (output));
- }
- }
- }
-
-private:
- juce::SmoothedValue smoothedGain;
-
- //TODO: this needs to be stored and retreived from the state
- EffectType curEffect = EffectType::verb;
-
- //changing the default curEffect to chorus DOES fix the click on first transition. Weird man
- //EffectType curEffect = EffectType::chorus;
-};
-
-//==================================================
-
-template
-class EffectsProcessor
-{
-public:
- EffectsProcessor()
- {
- verbWrapper = std::make_unique, T>>();
- verbWrapper->processor.setParameters (reverbParams);
-
- chorusWrapper = std::make_unique, T>>();
- }
-
- void prepare (const juce::dsp::ProcessSpec& spec)
- {
- // pre-allocate!
- fade_buffer1.setSize (spec.numChannels, spec.maximumBlockSize);
- fade_buffer2.setSize (spec.numChannels, spec.maximumBlockSize);
-
- verbWrapper->prepare(spec);
- chorusWrapper->prepare (spec);
- effectCrossFader.prepare (spec);
- }
-
- template
- void setEffectParam (juce::StringRef parameterID, T newValue)
- {
- const auto curEffect { effectCrossFader.getCurrentEffectType() };
- if (parameterID == ProPhatParameterIds::effectParam1ID.getParamID ())
- {
- if (curEffect == EffectType::verb)
- {
- reverbParams.roomSize = newValue;
- verbWrapper->processor.setParameters (reverbParams);
- }
- else if (curEffect == EffectType::chorus)
- {
- chorusWrapper->processor.setRate (static_cast (99.9) * newValue);
- }
- }
- else if (parameterID == ProPhatParameterIds::effectParam2ID.getParamID())
- {
- if (curEffect == EffectType::verb)
- {
- reverbParams.wetLevel = newValue;
- verbWrapper->processor.setParameters (reverbParams);
- }
- else if (curEffect == EffectType::chorus)
- {
- chorusWrapper->processor.setDepth (newValue);
- chorusWrapper->processor.setMix (newValue);
- }
- }
- else
- jassertfalse; //unknown effect parameter!
- }
-
- void changeEffect()
- {
- effectCrossFader.changeEffect();
- };
-
- void process (juce::AudioBuffer& buffer, int startSample, int numSamples)
- {
- //TODO: surround with trylock or something
- const auto currentEffectType { effectCrossFader.getCurrentEffectType() };
-
- if (currentEffectType == EffectType::transitioning)
- {
- //copy the OG buffer into the individual processor ones
- fade_buffer1 = buffer;
- fade_buffer2 = buffer;
-
- //make the individual blocks and process
- auto audioBlock1 { juce::dsp::AudioBlock (fade_buffer1).getSubBlock ((size_t) startSample, (size_t) numSamples) };
- auto context1 { juce::dsp::ProcessContextReplacing (audioBlock1) };
- verbWrapper->process (context1);
-
- auto audioBlock2 { juce::dsp::AudioBlock (fade_buffer2).getSubBlock ((size_t) startSample, (size_t) numSamples) };
- auto context2 { juce::dsp::ProcessContextReplacing (audioBlock2) };
- chorusWrapper->process (context2);
-
- //crossfade the 2 effects
- effectCrossFader.process (fade_buffer1, fade_buffer2, buffer);
-
- return;
- }
-
- auto audioBlock { juce::dsp::AudioBlock (buffer).getSubBlock ((size_t) startSample, (size_t) numSamples) };
- auto context { juce::dsp::ProcessContextReplacing (audioBlock) };
-
- if (currentEffectType == EffectType::verb)
- verbWrapper->process (context);
- else if (currentEffectType == EffectType::chorus)
- chorusWrapper->process (context);
- else
- jassertfalse; //unknown effect!!
- }
-
-private:
- std::unique_ptr, T>> chorusWrapper;
-
- std::unique_ptr, T>> verbWrapper;
- PhatVerbParameters reverbParams
- {
- //manually setting all these because we need to set the default room size and wet level to 0 if we want to be able to retrieve
- //these values from a saved state. If they are saved as 0 in the state, the event callback will not be propagated because
- //the change isn't forced-pushed
- 0.0f, //< Room size, 0 to 1.0, where 1.0 is big, 0 is small.
- 0.5f, //< Damping, 0 to 1.0, where 0 is not damped, 1.0 is fully damped.
- 0.0f, //< Wet level, 0 to 1.0
- 0.4f, //< Dry level, 0 to 1.0
- 1.0f, //< Reverb width, 0 to 1.0, where 1.0 is very wide.
- 0.0f //< Freeze mode - values < 0.5 are "normal" mode, values > 0.5 put the reverb into a continuous feedback loop.
- };
-
- juce::AudioBuffer fade_buffer1, fade_buffer2;
- EffectsCrossfadeProcessor effectCrossFader;
-};
-
-//========================================================
-
/** The main Synthesiser for the plugin. It uses Constants::numVoices voices (of type ProPhatVoice),
* and one ProPhatSound, which applies to all midi notes. It responds to paramater changes in the
* state via juce::AudioProcessorValueTreeState::Listener().