diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index b14276c..e13745f 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -325,6 +325,8 @@ void NeuralSynthAudioProcessorEditor::showPresetMenu() menu.addSubMenu(category, sub); } + menu.addItem(categories.size() + 1, "Custom ...", true, false); + menu.showMenuAsync(juce::PopupMenu::Options().withParentComponent(this), [this, baseId](int result) { diff --git a/Source/SynthVoice.cpp b/Source/SynthVoice.cpp index b68e2f2..95cd903 100644 --- a/Source/SynthVoice.cpp +++ b/Source/SynthVoice.cpp @@ -1,5 +1,13 @@ #include "SynthVoice.h" + #include +#include "SynthVoice/ADSR.h" +#include "SynthVoice/Chorus.h" +#include "SynthVoice/Distortion.h" +#include "SynthVoice/EQ.h" +#include "SynthVoice/Flanger.h" +#include "SynthVoice/Reverb.h" +#include "SynthVoice/SimpleDelay.h" //============================================================================== @@ -201,216 +209,13 @@ void NeuralSynthVoice::renderNextBlock (juce::AudioBuffer& outputBuffer, auto block = tempBlock.getSubBlock (0, (size_t) numSamples); - // ================================================================ - // Flanger (pre-filter) – manual per-sample to set varying delay - // ================================================================ - { - auto& flanger = chain.get(); + renderFlanger(numSamples, numCh); + renderADSR(numSamples, numCh); + renderChorus(block); + renderSimpleDelay(block); + renderDistortion(numSamples, numCh, block); + renderEQ(block); - const bool enabled = shared.flangerOn && shared.flangerOn->load() > 0.5f; - if (enabled) - { - const float rate = shared.flangerRate ? shared.flangerRate->load() : 0.0f; - float lfoPhase = shared.flangerPhase ? shared.flangerPhase->load() : 0.0f; - const float flangerDepth = shared.flangerDepth ? shared.flangerDepth->load() : 0.0f; // ms - const float mix = shared.flangerDryMix ? shared.flangerDryMix->load() : 0.0f; - const float feedback = shared.flangerFeedback ? shared.flangerFeedback->load() : 0.0f; - const float baseDelayMs = shared.flangerDelay ? shared.flangerDelay->load() : 0.25f; - - for (int i = 0; i < numSamples; ++i) - { - const float in = tempBuffer.getReadPointer (0)[i]; - - const float lfo = std::sin (lfoPhase); - const float delayMs = baseDelayMs + 0.5f * (1.0f + lfo) * flangerDepth; - const float delaySamples = juce::jmax (0.0f, delayMs * 0.001f * (float) spec.sampleRate); - - flanger.setDelay (delaySamples); - - const float delayed = flanger.popSample (0); - flanger.pushSample (0, in + delayed * feedback); - - const float out = in * (1.0f - mix) + delayed * mix; - for (int ch = 0; ch < numCh; ++ch) - tempBuffer.getWritePointer (ch)[i] = out; - - lfoPhase += juce::MathConstants::twoPi * rate / (float) spec.sampleRate; - if (lfoPhase > juce::MathConstants::twoPi) - lfoPhase -= juce::MathConstants::twoPi; - } - } - } - - // ================================================================ - // Filter with per-sample ADSR modulation (poly) - // ================================================================ - { - const bool enabled = shared.filterOn && shared.filterOn->load() > 0.5f; - - // Update filter type every block (cheap) - const int ftype = (int) std::lround (juce::jlimit (0.0f, 2.0f, - shared.filterType ? shared.filterType->load() : 0.0f)); - switch (ftype) - { - case 0: svf.setType (juce::dsp::StateVariableTPTFilterType::lowpass); break; - case 1: svf.setType (juce::dsp::StateVariableTPTFilterType::highpass); break; - case 2: svf.setType (juce::dsp::StateVariableTPTFilterType::bandpass); break; - default: break; - } - - const float qOrRes = juce::jlimit (0.1f, 10.0f, - shared.filterResonance ? shared.filterResonance->load() : 0.7f); - svf.setResonance (qOrRes); - - const float baseCutoff = juce::jlimit (20.0f, 20000.0f, - shared.filterCutoff ? shared.filterCutoff->load() : 1000.0f); - const float envAmt = shared.fenvAmount ? shared.fenvAmount->load() : 0.0f; - - for (int i = 0; i < numSamples; ++i) - { - const float envVal = filterAdsr.getNextSample(); - const float cutoff = juce::jlimit (20.0f, 20000.0f, - baseCutoff * std::pow (2.0f, envAmt * envVal)); - svf.setCutoffFrequency (cutoff); - - if (enabled) - { - for (int ch = 0; ch < numCh; ++ch) - { - float x = tempBuffer.getSample (ch, i); - x = svf.processSample (ch, x); - tempBuffer.setSample (ch, i, x); - } - } - } - } - - // ================================================================ - // Chorus - // ================================================================ - if (shared.chorusOn && shared.chorusOn->load() > 0.5f) - { - auto& chorus = chain.get(); - if (shared.chorusCentre) chorus.setCentreDelay (shared.chorusCentre->load()); - if (shared.chorusDepth) chorus.setDepth (shared.chorusDepth->load()); - if (shared.chorusFeedback) chorus.setFeedback (shared.chorusFeedback->load()); - if (shared.chorusMix) chorus.setMix (shared.chorusMix->load()); - if (shared.chorusRate) chorus.setRate (shared.chorusRate->load()); - - chain.get().process (juce::dsp::ProcessContextReplacing (block)); - } - - // ================================================================ - // Simple Delay (per-voice) - // ================================================================ - if (shared.delayOn && shared.delayOn->load() > 0.5f) - { - auto& delay = chain.get(); - const float time = shared.delayTime ? shared.delayTime->load() : 0.1f; - delay.setDelay (juce::jmax (0.0f, time * (float) spec.sampleRate)); - delay.process (juce::dsp::ProcessContextReplacing (block)); - } - - // ================================================================ - // Reverb - // ================================================================ - if (shared.reverbOn && shared.reverbOn->load() > 0.5f) - { - juce::Reverb::Parameters rp; - rp.damping = shared.reverbDamping ? shared.reverbDamping->load() : 0.0f; - rp.dryLevel = shared.reverbDryLevel ? shared.reverbDryLevel->load() : 0.0f; - rp.freezeMode = shared.reverbFreezeMode ? shared.reverbFreezeMode->load() : 0.0f; - rp.roomSize = shared.reverbRoomSize ? shared.reverbRoomSize->load() : 0.0f; - rp.wetLevel = shared.reverbWetLevel ? shared.reverbWetLevel->load() : 0.0f; - rp.width = shared.reverbWidth ? shared.reverbWidth->load() : 0.0f; - - chain.get().setParameters (rp); - chain.get().process (juce::dsp::ProcessContextReplacing (block)); - } - - // ================================================================ - // Distortion + tone (post LPF/Peak) - // ================================================================ - { - const float driveDb = shared.distortionDrive ? shared.distortionDrive->load() : 0.0f; - const float bias = juce::jlimit (-1.0f, 1.0f, shared.distortionBias ? shared.distortionBias->load() : 0.0f); - const float toneHz = juce::jlimit (100.0f, 8000.0f, shared.distortionTone ? shared.distortionTone->load() : 3000.0f); - const int shape = (int) std::lround (juce::jlimit (0.0f, 2.0f, - shared.distortionShape ? shared.distortionShape->load() : 0.0f)); - const float mix = shared.distortionMix ? shared.distortionMix->load() : 0.0f; - - auto& pre = chain.get(); - auto& sh = chain.get(); - auto& tone = chain.get(); - - pre.setGainDecibels (driveDb); - - // Explicit std::function target (works on MSVC) - if (shape == 0) sh.functionToUse = std::function{ [bias](float x) noexcept { return std::tanh (x + bias); } }; - else if (shape == 1) sh.functionToUse = std::function{ [bias](float x) noexcept { return juce::jlimit (-1.0f, 1.0f, x + bias); } }; - else sh.functionToUse = std::function{ [bias](float x) noexcept { return std::atan (x + bias) * (2.0f / juce::MathConstants::pi); } }; - - tone.coefficients = juce::dsp::IIR::Coefficients::makePeakFilter ( - spec.sampleRate, toneHz, 0.707f, - juce::Decibels::decibelsToGain (shared.highGainDbls ? shared.highGainDbls->load() : 0.0f)); - - if (shared.distortionOn && shared.distortionOn->load() > 0.5f) - { - // Wet/dry blend around the shaper - juce::AudioBuffer dryCopy (tempBuffer.getNumChannels(), numSamples); - for (int ch = 0; ch < numCh; ++ch) - dryCopy.copyFrom (ch, 0, tempBuffer, ch, 0, numSamples); - - // pre -> shaper -> tone - pre.process (juce::dsp::ProcessContextReplacing (block)); - sh.process (juce::dsp::ProcessContextReplacing (block)); - tone.process (juce::dsp::ProcessContextReplacing (block)); - - const float wet = mix, dry = 1.0f - mix; - for (int ch = 0; ch < numCh; ++ch) - { - auto* d = dryCopy.getReadPointer (ch); - auto* w = tempBuffer.getWritePointer (ch); - for (int i = 0; i < numSamples; ++i) - w[i] = dry * d[i] + wet * w[i]; - } - } - } - - // ================================================================ - // EQ + Master + Limiter (EQ guarded by eqOn) - // ================================================================ - { - const bool eqEnabled = shared.eqOn && shared.eqOn->load() > 0.5f; - - auto& eqL = chain.get(); - auto& eqM = chain.get(); - auto& eqH = chain.get(); - - if (eqEnabled) - { - eqL.coefficients = juce::dsp::IIR::Coefficients::makeLowShelf ( - spec.sampleRate, 100.0f, 0.707f, - juce::Decibels::decibelsToGain (shared.lowGainDbls ? shared.lowGainDbls->load() : 0.0f)); - - eqM.coefficients = juce::dsp::IIR::Coefficients::makePeakFilter ( - spec.sampleRate, 1000.0f, 1.0f, - juce::Decibels::decibelsToGain (shared.midGainDbls ? shared.midGainDbls->load() : 0.0f)); - - eqH.coefficients = juce::dsp::IIR::Coefficients::makePeakFilter ( - spec.sampleRate, 10000.0f, 0.707f, - juce::Decibels::decibelsToGain (shared.highGainDbls ? shared.highGainDbls->load() : 0.0f)); - - eqL.process (juce::dsp::ProcessContextReplacing (block)); - eqM.process (juce::dsp::ProcessContextReplacing (block)); - eqH.process (juce::dsp::ProcessContextReplacing (block)); - } - - chain.get().setGainDecibels (shared.masterDbls ? shared.masterDbls->load() : 0.0f); - chain.get().process (juce::dsp::ProcessContextReplacing (block)); - - chain.get().process (juce::dsp::ProcessContextReplacing (block)); - } // ================================================================ // Apply AMP ADSR envelope diff --git a/Source/SynthVoice.h b/Source/SynthVoice.h index a50f043..a1e8f49 100644 --- a/Source/SynthVoice.h +++ b/Source/SynthVoice.h @@ -78,6 +78,15 @@ private: using Reverb = juce::dsp::Reverb; using Limiter = juce::dsp::Limiter; + // Separate functions for different parts + void renderReverb(juce::dsp::AudioBlock &block); + void renderSimpleDelay(juce::dsp::AudioBlock &block); + void renderADSR(int numSamples, int numCh); + void renderChorus(juce::dsp::AudioBlock &block); + void renderFlanger(int numSamples, int numCh); + void renderDistortion(int numSamples, int numCh, juce::dsp::AudioBlock &block); + void renderEQ(juce::dsp::AudioBlock &block); + enum ChainIndex { flangerIndex = 0, diff --git a/Source/SynthVoice/ADSR.h b/Source/SynthVoice/ADSR.h new file mode 100644 index 0000000..ee4bd82 --- /dev/null +++ b/Source/SynthVoice/ADSR.h @@ -0,0 +1,46 @@ +#pragma once +#include "../SynthVoice.h" + +void NeuralSynthVoice::renderADSR(int numSamples, int numCh) { + // ================================================================ + // Filter with per-sample ADSR modulation (poly) + // ================================================================ + const bool enabled = shared.filterOn && shared.filterOn->load() > 0.5f; + + // Update filter type every block (cheap) + const int ftype = (int) std::lround (juce::jlimit (0.0f, 2.0f, + shared.filterType ? shared.filterType->load() : 0.0f)); + switch (ftype) + { + case 0: svf.setType (juce::dsp::StateVariableTPTFilterType::lowpass); break; + case 1: svf.setType (juce::dsp::StateVariableTPTFilterType::highpass); break; + case 2: svf.setType (juce::dsp::StateVariableTPTFilterType::bandpass); break; + default: break; + } + + const float qOrRes = juce::jlimit (0.1f, 10.0f, + shared.filterResonance ? shared.filterResonance->load() : 0.7f); + svf.setResonance (qOrRes); + + const float baseCutoff = juce::jlimit (20.0f, 20000.0f, + shared.filterCutoff ? shared.filterCutoff->load() : 1000.0f); + const float envAmt = shared.fenvAmount ? shared.fenvAmount->load() : 0.0f; + + for (int i = 0; i < numSamples; ++i) + { + const float envVal = filterAdsr.getNextSample(); + const float cutoff = juce::jlimit (20.0f, 20000.0f, + baseCutoff * std::pow (2.0f, envAmt * envVal)); + svf.setCutoffFrequency (cutoff); + + if (enabled) + { + for (int ch = 0; ch < numCh; ++ch) + { + float x = tempBuffer.getSample (ch, i); + x = svf.processSample (ch, x); + tempBuffer.setSample (ch, i, x); + } + } + } +} diff --git a/Source/SynthVoice/Chorus.h b/Source/SynthVoice/Chorus.h new file mode 100644 index 0000000..aeb1cbc --- /dev/null +++ b/Source/SynthVoice/Chorus.h @@ -0,0 +1,19 @@ +#pragma once +#include "../SynthVoice.h" + +void NeuralSynthVoice::renderChorus(juce::dsp::AudioBlock &block) { + // ================================================================ + // Chorus + // ================================================================ + if (shared.chorusOn && shared.chorusOn->load() > 0.5f) + { + auto& chorus = chain.get(); + if (shared.chorusCentre) chorus.setCentreDelay (shared.chorusCentre->load()); + if (shared.chorusDepth) chorus.setDepth (shared.chorusDepth->load()); + if (shared.chorusFeedback) chorus.setFeedback (shared.chorusFeedback->load()); + if (shared.chorusMix) chorus.setMix (shared.chorusMix->load()); + if (shared.chorusRate) chorus.setRate (shared.chorusRate->load()); + + chain.get().process (juce::dsp::ProcessContextReplacing (block)); + } +} diff --git a/Source/SynthVoice/Distortion.h b/Source/SynthVoice/Distortion.h new file mode 100644 index 0000000..e232e4a --- /dev/null +++ b/Source/SynthVoice/Distortion.h @@ -0,0 +1,54 @@ +#pragma once +#include "../SynthVoice.h" + +void NeuralSynthVoice::renderDistortion( + int numSamples, + int numCh, + juce::dsp::AudioBlock &block) { +// ================================================================ +// Distortion + tone (post LPF/Peak) +// ================================================================ + const float driveDb = shared.distortionDrive ? shared.distortionDrive->load() : 0.0f; + const float bias = juce::jlimit (-1.0f, 1.0f, shared.distortionBias ? shared.distortionBias->load() : 0.0f); + const float toneHz = juce::jlimit (100.0f, 8000.0f, shared.distortionTone ? shared.distortionTone->load() : 3000.0f); + const int shape = (int) std::lround (juce::jlimit (0.0f, 2.0f, + shared.distortionShape ? shared.distortionShape->load() : 0.0f)); + const float mix = shared.distortionMix ? shared.distortionMix->load() : 0.0f; + + auto& pre = chain.get(); + auto& sh = chain.get(); + auto& tone = chain.get(); + + pre.setGainDecibels (driveDb); + + // Explicit std::function target (works on MSVC) + if (shape == 0) sh.functionToUse = std::function{ [bias](float x) noexcept { return std::tanh (x + bias); } }; + else if (shape == 1) sh.functionToUse = std::function{ [bias](float x) noexcept { return juce::jlimit (-1.0f, 1.0f, x + bias); } }; + else sh.functionToUse = std::function{ [bias](float x) noexcept { return std::atan (x + bias) * (2.0f / juce::MathConstants::pi); } }; + + tone.coefficients = juce::dsp::IIR::Coefficients::makePeakFilter ( + spec.sampleRate, toneHz, 0.707f, + juce::Decibels::decibelsToGain (shared.highGainDbls ? shared.highGainDbls->load() : 0.0f)); + + if (shared.distortionOn && shared.distortionOn->load() > 0.5f) + { + // Wet/dry blend around the shaper + juce::AudioBuffer dryCopy (tempBuffer.getNumChannels(), numSamples); + for (int ch = 0; ch < numCh; ++ch) + dryCopy.copyFrom (ch, 0, tempBuffer, ch, 0, numSamples); + + // pre -> shaper -> tone + pre.process (juce::dsp::ProcessContextReplacing (block)); + sh.process (juce::dsp::ProcessContextReplacing (block)); + tone.process (juce::dsp::ProcessContextReplacing (block)); + + const float wet = mix, dry = 1.0f - mix; + for (int ch = 0; ch < numCh; ++ch) + { + auto* d = dryCopy.getReadPointer (ch); + auto* w = tempBuffer.getWritePointer (ch); + for (int i = 0; i < numSamples; ++i) + w[i] = dry * d[i] + wet * w[i]; + } + } +} diff --git a/Source/SynthVoice/EQ.h b/Source/SynthVoice/EQ.h new file mode 100644 index 0000000..07e49d1 --- /dev/null +++ b/Source/SynthVoice/EQ.h @@ -0,0 +1,38 @@ +#pragma once +#include "../SynthVoice.h" + +void NeuralSynthVoice::renderEQ(juce::dsp::AudioBlock &block) +{ + // ================================================================ + // EQ + Master + Limiter (EQ guarded by eqOn) + // ================================================================ + const bool eqEnabled = shared.eqOn && shared.eqOn->load() > 0.5f; + + auto& eqL = chain.get(); + auto& eqM = chain.get(); + auto& eqH = chain.get(); + + if (eqEnabled) + { + eqL.coefficients = juce::dsp::IIR::Coefficients::makeLowShelf ( + spec.sampleRate, 100.0f, 0.707f, + juce::Decibels::decibelsToGain (shared.lowGainDbls ? shared.lowGainDbls->load() : 0.0f)); + + eqM.coefficients = juce::dsp::IIR::Coefficients::makePeakFilter ( + spec.sampleRate, 1000.0f, 1.0f, + juce::Decibels::decibelsToGain (shared.midGainDbls ? shared.midGainDbls->load() : 0.0f)); + + eqH.coefficients = juce::dsp::IIR::Coefficients::makePeakFilter ( + spec.sampleRate, 10000.0f, 0.707f, + juce::Decibels::decibelsToGain (shared.highGainDbls ? shared.highGainDbls->load() : 0.0f)); + + eqL.process (juce::dsp::ProcessContextReplacing (block)); + eqM.process (juce::dsp::ProcessContextReplacing (block)); + eqH.process (juce::dsp::ProcessContextReplacing (block)); + } + + chain.get().setGainDecibels (shared.masterDbls ? shared.masterDbls->load() : 0.0f); + chain.get().process (juce::dsp::ProcessContextReplacing (block)); + + chain.get().process (juce::dsp::ProcessContextReplacing (block)); +} diff --git a/Source/SynthVoice/Flanger.h b/Source/SynthVoice/Flanger.h new file mode 100644 index 0000000..94abd7b --- /dev/null +++ b/Source/SynthVoice/Flanger.h @@ -0,0 +1,43 @@ +#pragma once +#include "../SynthVoice.h" + +void NeuralSynthVoice::renderFlanger(int numSamples, int numCh) +{ + // ================================================================ + // Flanger (pre-filter) – manual per-sample to set varying delay + // ================================================================ + auto& flanger = chain.get(); + + const bool enabled = shared.flangerOn && shared.flangerOn->load() > 0.5f; + if (enabled) + { + const float rate = shared.flangerRate ? shared.flangerRate->load() : 0.0f; + float lfoPhase = shared.flangerPhase ? shared.flangerPhase->load() : 0.0f; + const float flangerDepth = shared.flangerDepth ? shared.flangerDepth->load() : 0.0f; // ms + const float mix = shared.flangerDryMix ? shared.flangerDryMix->load() : 0.0f; + const float feedback = shared.flangerFeedback ? shared.flangerFeedback->load() : 0.0f; + const float baseDelayMs = shared.flangerDelay ? shared.flangerDelay->load() : 0.25f; + + for (int i = 0; i < numSamples; ++i) + { + const float in = tempBuffer.getReadPointer (0)[i]; + + const float lfo = std::sin (lfoPhase); + const float delayMs = baseDelayMs + 0.5f * (1.0f + lfo) * flangerDepth; + const float delaySamples = juce::jmax (0.0f, delayMs * 0.001f * (float) spec.sampleRate); + + flanger.setDelay (delaySamples); + + const float delayed = flanger.popSample (0); + flanger.pushSample (0, in + delayed * feedback); + + const float out = in * (1.0f - mix) + delayed * mix; + for (int ch = 0; ch < numCh; ++ch) + tempBuffer.getWritePointer (ch)[i] = out; + + lfoPhase += juce::MathConstants::twoPi * rate / (float) spec.sampleRate; + if (lfoPhase > juce::MathConstants::twoPi) + lfoPhase -= juce::MathConstants::twoPi; + } + } +} diff --git a/Source/SynthVoice/Reverb.h b/Source/SynthVoice/Reverb.h new file mode 100644 index 0000000..d7e20a6 --- /dev/null +++ b/Source/SynthVoice/Reverb.h @@ -0,0 +1,22 @@ +#pragma once +#include "../SynthVoice.h" + +void NeuralSynthVoice::renderReverb(juce::dsp::AudioBlock &block) { + // ================================================================ + // Reverb + // ================================================================ + if (shared.reverbOn && shared.reverbOn->load() > 0.5f) + { + juce::Reverb::Parameters rp; + rp.damping = shared.reverbDamping ? shared.reverbDamping->load() : 0.0f; + rp.dryLevel = shared.reverbDryLevel ? shared.reverbDryLevel->load() : 0.0f; + rp.freezeMode = shared.reverbFreezeMode ? shared.reverbFreezeMode->load() : 0.0f; + rp.roomSize = shared.reverbRoomSize ? shared.reverbRoomSize->load() : 0.0f; + rp.wetLevel = shared.reverbWetLevel ? shared.reverbWetLevel->load() : 0.0f; + rp.width = shared.reverbWidth ? shared.reverbWidth->load() : 0.0f; + + chain.get().setParameters (rp); + chain.get().process (juce::dsp::ProcessContextReplacing (block)); + } + +} diff --git a/Source/SynthVoice/SimpleDelay.h b/Source/SynthVoice/SimpleDelay.h new file mode 100644 index 0000000..cba6698 --- /dev/null +++ b/Source/SynthVoice/SimpleDelay.h @@ -0,0 +1,16 @@ +#pragma once +#include "../SynthVoice.h" + +void NeuralSynthVoice::renderSimpleDelay(juce::dsp::AudioBlock &block) +{ + // ================================================================ + // Simple Delay (per-voice) + // ================================================================ + if (shared.delayOn && shared.delayOn->load() > 0.5f) + { + auto& delay = chain.get(); + const float time = shared.delayTime ? shared.delayTime->load() : 0.1f; + delay.setDelay (juce::jmax (0.0f, time * (float) spec.sampleRate)); + delay.process (juce::dsp::ProcessContextReplacing (block)); + } +}