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().