Files
NeuralSynthEd/PluginProcessor.cpp
2025-10-21 17:22:44 +01:00

1079 lines
37 KiB
C++

#include "PluginProcessor.h"
#include "PluginEditor.h"
#include <cmath>
//==============================================================
// Factory presets (Category + Name + full parameter snapshot)
//==============================================================
namespace
{
struct Patch
{
juce::String category;
juce::String name;
// Core
int oscA, oscB;
float mix, detune, cutoff, reso, envAmt;
// Amp ADSR
float aA, dA, sA, rA;
// Filter ADSR
float aF, dF, sF, rF;
float master; // dB
// NEW: LFO (sine) stored in presets
float lfoRate; // Hz
float lfoToCut; // octaves
float lfoToPitch; // cents
};
// Factory helper with defaulted LFO so existing calls remain valid
Patch P (juce::String cat, juce::String nm,
int oscA, int oscB, float mix, float detune,
float cutoff, float reso, float envAmt,
float aA, float dA, float sA, float rA,
float aF, float dF, float sF, float rF,
float master,
float lfoRate = 0.0f, float lfoToCut = 0.0f, float lfoToPitch = 0.0f)
{
return Patch { std::move (cat), std::move (nm),
oscA, oscB, mix, detune, cutoff, reso, envAmt,
aA, dA, sA, rA, aF, dF, sF, rF, master,
lfoRate, lfoToCut, lfoToPitch };
}
const std::vector<Patch>& getFactoryPatches()
{
static const std::vector<Patch> patches = {
// ===================== Bass =====================
P("Bass","Deep Bass",
1,2, 0.35f,-5.f,
180.f,0.18f,0.25f,
0.01f,0.10f,0.70f,0.12f,
0.015f,0.18f,0.10f,0.18f, -18.f,
0.7f, 0.20f, 0.0f),
P("Bass","Rubber Bass",
1,3, 0.45f,+2.f,
600.f,0.24f,0.22f,
0.005f,0.12f,0.60f,0.12f,
0.008f,0.15f,0.10f,0.12f, -17.f,
1.2f, 0.15f, 0.0f),
P("Bass","Analog Growl",
2,1, 0.50f,+4.f,
1200.f,0.40f,0.35f,
0.008f,0.18f,0.60f,0.14f,
0.010f,0.20f,0.20f,0.25f, -16.f,
0.8f, 0.25f, 3.0f),
P("Bass","Punch Bass",
1,1, 0.50f,+4.f,
1000.f,0.22f,0.40f,
0.003f,0.10f,0.55f,0.12f,
0.010f,0.12f,0.20f,0.18f, -16.f,
1.5f, 0.10f, 0.0f),
P("Bass","Acid Bite",
1,2, 0.50f,+3.f,
800.f,0.70f,0.45f,
0.002f,0.10f,0.55f,0.10f,
0.008f,0.14f,0.15f,0.18f, -16.f,
6.5f, 0.35f, 0.0f),
P("Bass","Wobble Bass",
2,1, 0.48f, 0.f,
220.f,0.35f,0.18f,
0.004f,0.12f,0.60f,0.12f,
0.010f,0.18f,0.12f,0.18f, -16.f,
1.6f, 0.80f, 2.0f),
P("Bass","Pluck Bass",
1,3, 0.42f,-3.f,
420.f,0.22f,0.25f,
0.001f,0.08f,0.30f,0.10f,
0.006f,0.12f,0.10f,0.12f, -17.f,
5.5f, 0.10f, 5.0f),
// ===================== Pad ======================
P("Pad","Dream Pad",
3,1, 0.50f,+7.f,
1200.f,0.22f,0.35f,
0.20f,0.60f,0.85f,0.90f,
0.18f,0.70f,0.60f,0.80f, -18.f,
0.35f, 0.45f, 0.0f),
P("Pad","Analog Pad",
1,1, 0.50f,+4.f,
1500.f,0.26f,0.28f,
0.12f,0.45f,0.80f,0.70f,
0.10f,0.50f,0.55f,0.65f, -18.f,
0.30f, 0.35f, 0.0f),
P("Pad","Glass Sweep",
3,2, 0.45f,+2.f,
1800.f,0.18f,0.42f,
0.30f,0.70f,0.80f,0.85f,
0.20f,0.65f,0.55f,0.75f, -18.f,
0.25f, 0.40f, 0.0f),
P("Pad","Glass Pad",
0,1, 0.35f,+3.f,
2000.f,0.18f,0.28f,
0.25f,0.65f,0.85f,0.95f,
0.18f,0.65f,0.50f,0.75f, -18.f,
0.28f, 0.30f, 0.0f),
P("Pad","Dreamscape",
1,1, 0.50f,+9.f,
4000.f,0.22f,0.40f,
0.30f,0.80f,0.85f,1.00f,
0.20f,0.75f,0.60f,0.85f, -18.f,
0.20f, 0.50f, 0.0f),
P("Pad","Slow Motion",
3,3, 0.50f, 0.f,
2000.f,0.30f,0.20f,
0.50f,0.90f,0.90f,1.20f,
0.30f,0.90f,0.70f,1.10f, -18.f,
0.18f, 0.55f, 0.0f),
P("Pad","Lunar Pad",
1,2, 0.55f,+5.f,
2500.f,0.24f,0.35f,
0.22f,0.70f,0.85f,1.00f,
0.20f,0.70f,0.60f,0.90f, -18.f,
0.32f, 0.35f, 0.0f),
// ===================== Lead =====================
P("Lead","Bright Lead",
2,1, 0.40f,+6.f,
2400.f,0.30f,0.45f,
0.01f,0.20f,0.60f,0.20f,
0.02f,0.28f,0.20f,0.30f, -18.f,
5.8f, 0.05f, 12.0f),
P("Lead","Sync Pulse",
2,2, 0.50f,+5.f,
2200.f,0.24f,0.30f,
0.01f,0.15f,0.55f,0.18f,
0.02f,0.20f,0.20f,0.25f, -16.f,
5.5f, 0.04f, 9.0f),
P("Lead","Solo Glide",
1,1, 0.50f,+6.f,
3500.f,0.22f,0.25f,
0.005f,0.18f,0.60f,0.20f,
0.010f,0.20f,0.20f,0.22f, -18.f,
5.2f, 0.03f, 10.0f),
P("Lead","Vintage Lead",
2,3, 0.50f,+3.f,
3200.f,0.28f,0.20f,
0.010f,0.22f,0.65f,0.25f,
0.020f,0.24f,0.22f,0.28f, -18.f,
5.0f, 0.04f, 8.0f),
P("Lead","PWM Lead",
2,1, 0.55f,+5.f,
3000.f,0.26f,0.30f,
0.010f,0.20f,0.60f,0.22f,
0.020f,0.22f,0.22f,0.26f, -18.f,
5.6f, 0.03f, 10.0f),
P("Lead","Expressive Sync",
1,3, 0.50f,+2.f,
2800.f,0.30f,0.35f,
0.008f,0.20f,0.60f,0.22f,
0.020f,0.26f,0.25f,0.30f, -18.f,
5.4f, 0.06f, 12.0f),
// ===================== Strings ==================
P("Strings","Warm Ensemble",
1,1, 0.50f,+6.f,
1800.f,0.22f,0.25f,
0.18f,0.60f,0.85f,0.90f,
0.12f,0.65f,0.55f,0.75f, -18.f,
5.0f, 0.00f, 6.0f),
P("Strings","Silky Strings",
3,1, 0.45f,+4.f,
1500.f,0.20f,0.30f,
0.20f,0.70f,0.85f,0.95f,
0.14f,0.70f,0.60f,0.80f, -18.f,
5.3f, 0.00f, 5.0f),
P("Strings","Octa Strings",
1,1, 0.50f,+12.f,
2200.f,0.26f,0.22f,
0.12f,0.50f,0.80f,0.80f,
0.10f,0.50f,0.50f,0.65f, -18.f,
5.0f, 0.00f, 7.0f),
P("Strings","Mellow Chamber",
3,2, 0.40f,+2.f,
1200.f,0.18f,0.18f,
0.15f,0.55f,0.75f,0.85f,
0.10f,0.45f,0.45f,0.60f, -18.f,
4.8f, 0.00f, 5.0f),
P("Strings","Glass Ensemble",
3,1, 0.48f,+6.f,
1700.f,0.22f,0.28f,
0.20f,0.65f,0.85f,0.95f,
0.14f,0.60f,0.60f,0.85f, -18.f,
5.2f, 0.00f, 6.0f),
P("Strings","Warm Section",
1,1, 0.52f,+5.f,
1400.f,0.20f,0.25f,
0.18f,0.60f,0.85f,0.90f,
0.12f,0.58f,0.55f,0.78f, -18.f,
5.0f, 0.00f, 5.0f),
P("Strings","Silk Legato",
0,3, 0.55f,+4.f,
1300.f,0.18f,0.22f,
0.18f,0.65f,0.90f,1.10f,
0.12f,0.60f,0.60f,0.85f, -18.f,
5.4f, 0.00f, 4.0f),
P("Strings","Octave Ensemble 2",
1,3, 0.50f,+12.f,
2100.f,0.24f,0.20f,
0.16f,0.55f,0.80f,0.90f,
0.10f,0.52f,0.50f,0.70f, -18.f,
5.0f, 0.00f, 7.0f),
// ===================== Polysynth ================
P("Polysynth","Poly Warm",
1,3, 0.50f,+5.f,
1000.f,0.24f,0.30f,
0.04f,0.25f,0.85f,0.40f,
0.05f,0.30f,0.40f,0.40f, -18.f,
0.30f, 0.25f, 0.0f),
P("Polysynth","Stab Classic",
2,1, 0.45f,+3.f,
1600.f,0.20f,0.25f,
0.01f,0.10f,0.60f,0.15f,
0.02f,0.12f,0.20f,0.18f, -16.f),
P("Polysynth","PolyStack",
1,1, 0.50f,+7.f,
1200.f,0.22f,0.30f,
0.03f,0.22f,0.80f,0.35f,
0.05f,0.28f,0.40f,0.42f, -18.f,
0.35f, 0.30f, 0.0f),
P("Polysynth","Soft Poly Pluck",
2,1, 0.45f,+3.f,
1600.f,0.24f,0.35f,
0.001f,0.10f,0.25f,0.16f,
0.02f,0.14f,0.20f,0.18f, -16.f,
4.5f, 0.00f, 0.0f),
P("Polysynth","Warm Chords",
3,1, 0.50f,+5.f,
1800.f,0.22f,0.28f,
0.05f,0.30f,0.85f,0.45f,
0.06f,0.32f,0.42f,0.45f, -18.f,
0.30f, 0.25f, 0.0f),
P("Polysynth","Airy Poly",
3,3, 0.50f, 0.f,
2400.f,0.26f,0.20f,
0.20f,0.70f,0.88f,0.95f,
0.16f,0.68f,0.60f,0.90f, -18.f,
0.22f, 0.35f, 0.0f),
// ===================== Organ ====================
P("Organ","Soft Organ",
2,2, 0.50f, 0.f,
1800.f,0.12f,0.00f,
0.01f,0.05f,0.75f,0.20f,
0.01f,0.05f,0.00f,0.05f, -18.f,
5.8f, 0.00f, 7.0f),
P("Organ","Full Drawbars",
2,1, 0.55f, 0.f,
2200.f,0.12f,0.00f,
0.01f,0.06f,0.80f,0.22f,
0.01f,0.05f,0.00f,0.05f, -16.f,
5.8f, 0.00f, 6.0f),
P("Organ","Jazz 888000000",
2,1, 0.55f, 0.f,
20000.f,0.12f,0.00f,
0.005f,0.06f,0.85f,0.18f,
0.005f,0.05f,0.00f,0.06f, -16.f,
5.8f, 0.00f, 7.0f),
P("Organ","Gospel Perc",
2,2, 0.52f, 0.f,
5000.f,0.14f,0.00f,
0.001f,0.20f,0.60f,0.25f,
0.006f,0.05f,0.00f,0.06f, -16.f,
6.0f, 0.00f, 5.0f),
P("Organ","Rock Drawbars",
1,2, 0.58f,+1.f,
8000.f,0.16f,0.00f,
0.004f,0.08f,0.80f,0.22f,
0.006f,0.06f,0.00f,0.06f, -15.f,
6.2f, 0.00f, 6.0f),
P("Organ","Chorus Organ",
3,1, 0.50f, 0.f,
6000.f,0.15f,0.00f,
0.004f,0.10f,0.80f,0.24f,
0.006f,0.06f,0.00f,0.06f, -17.f,
0.6f, 0.15f, 4.0f),
// ===================== Keys =====================
P("Keys","Clav Bite",
2,2, 0.40f,-2.f,
1800.f,0.22f,0.22f,
0.01f,0.08f,0.55f,0.18f,
0.02f,0.18f,0.15f,0.20f, -16.f),
// (no LFO to keep it percussive)
P("Keys","Poly Warm",
3,1, 0.50f,+5.f,
3000.f,0.24f,0.30f,
0.04f,0.25f,0.85f,0.40f,
0.05f,0.30f,0.40f,0.40f, -18.f,
0.30f, 0.22f, 0.0f),
P("Keys","Bell Keys",
0,3, 0.40f,+2.f,
4500.f,0.20f,0.15f,
0.005f,0.20f,0.30f,0.35f,
0.010f,0.22f,0.10f,0.22f, -16.f,
5.2f, 0.00f, 4.0f),
P("Keys","MK1 Soft",
3,0, 0.52f, 0.f,
1500.f,0.18f,0.12f,
0.006f,0.22f,0.75f,0.28f,
0.010f,0.22f,0.10f,0.20f, -18.f,
5.0f, 0.00f, 3.0f),
P("Keys","MK1 Bark",
2,1, 0.50f,+2.f,
2500.f,0.22f,0.18f,
0.003f,0.22f,0.50f,0.25f,
0.012f,0.20f,0.12f,0.20f, -16.f,
5.2f, 0.00f, 4.0f),
P("Keys","Wurly Bite",
2,2, 0.45f, 0.f,
1800.f,0.24f,0.20f,
0.004f,0.18f,0.55f,0.22f,
0.012f,0.18f,0.12f,0.18f, -16.f,
5.6f, 0.10f, 3.0f),
P("Keys","Trem EP",
3,1, 0.50f, 0.f,
2200.f,0.20f,0.16f,
0.004f,0.24f,0.70f,0.30f,
0.012f,0.22f,0.10f,0.22f, -18.f,
4.8f, 0.25f, 0.0f),
// ===================== Brass ====================
P("Brass","Soft Brass",
1,2, 0.60f,+4.f,
1800.f,0.22f,0.35f,
0.01f,0.20f,0.55f,0.30f,
0.02f,0.30f,0.25f,0.40f, -18.f,
5.0f, 0.06f, 6.0f),
P("Brass","Stacked Brass",
1,1, 0.55f,+5.f,
1600.f,0.24f,0.30f,
0.01f,0.18f,0.60f,0.25f,
0.02f,0.28f,0.22f,0.35f, -16.f,
5.2f, 0.05f, 8.0f),
P("Brass","Soft Trumpet",
1,2, 0.55f,+6.f,
1800.f,0.32f,0.40f,
0.010f,0.25f,0.60f,0.35f,
0.020f,0.30f,0.28f,0.42f, -18.f,
5.3f, 0.06f, 6.0f),
P("Brass","Brass Stabs",
2,1, 0.52f,+4.f,
1600.f,0.24f,0.45f,
0.004f,0.12f,0.40f,0.18f,
0.012f,0.20f,0.20f,0.28f, -16.f,
5.0f, 0.00f, 4.0f),
P("Brass","Warm Horns",
1,3, 0.50f,+3.f,
1400.f,0.22f,0.32f,
0.020f,0.40f,0.75f,0.50f,
0.020f,0.35f,0.30f,0.45f, -18.f,
4.8f, 0.05f, 5.0f),
P("Brass","Sync Brass",
2,2, 0.58f,+7.f,
2200.f,0.34f,0.35f,
0.006f,0.18f,0.55f,0.22f,
0.016f,0.26f,0.24f,0.32f, -16.f,
5.6f, 0.05f, 7.0f),
// ===================== Choir ====================
P("Choir","Ooh Pad",
3,3, 0.60f,+3.f,
1200.f,0.16f,0.20f,
0.12f,0.60f,0.85f,0.90f,
0.10f,0.60f,0.60f,0.70f, -18.f,
0.28f, 0.40f, 0.0f),
P("Choir","Ahh Warm",
3,1, 0.58f,+2.f,
1100.f,0.18f,0.22f,
0.14f,0.58f,0.80f,0.88f,
0.10f,0.55f,0.55f,0.68f, -18.f,
0.26f, 0.35f, 0.0f),
// ===================== Drums ====================
P("Drums","Click Kick",
2,2, 0.50f, 0.f,
220.f,0.15f,0.00f,
0.00f,0.06f,0.35f,0.18f,
0.00f,0.05f,0.00f,0.06f, -16.f),
P("Drums","Deep Kick",
0,0, 0.35f, 0.f,
180.f,0.16f,0.00f,
0.001f,0.12f,0.20f,0.18f,
0.001f,0.08f,0.00f,0.08f, -12.f,
0.0f, 0.00f, 0.0f),
P("Drums","Snappy Snare",
2,1, 0.50f, 0.f,
2200.f,0.30f,0.20f,
0.001f,0.10f,0.00f,0.15f,
0.004f,0.10f,0.05f,0.10f, -14.f,
0.0f, 0.00f, 0.0f),
P("Drums","Closed Hat",
2,2, 0.50f, 0.f,
8000.f,0.18f,0.00f,
0.001f,0.05f,0.00f,0.05f,
0.002f,0.05f,0.00f,0.05f, -18.f,
0.0f, 0.00f, 0.0f),
P("Drums","Low Tom",
0,0, 0.30f, 0.f,
220.f,0.16f,0.00f,
0.001f,0.14f,0.25f,0.18f,
0.002f,0.08f,0.00f,0.08f, -16.f,
0.0f, 0.00f, 0.0f),
// ===================== SFX ======================
P("SFX","Meteor Rise",
3,1, 0.50f, +4.f,
900.f,0.28f,0.85f,
0.05f,0.70f,0.10f,0.80f,
0.08f,0.75f,0.10f,0.85f, -18.f,
0.15f, 0.80f, 0.0f),
P("SFX","Noise Drone",
1,1, 0.50f,+12.f,
1200.f,0.30f,0.50f,
0.30f,0.90f,0.90f,1.20f,
0.30f,0.90f,0.70f,1.10f, -14.f,
0.22f, 0.65f, 0.0f),
P("SFX","Reso Sweep",
3,1, 0.50f,+2.f,
700.f,0.60f,0.80f,
0.02f,0.50f,0.30f,0.60f,
0.05f,0.70f,0.20f,0.70f, -18.f,
0.20f, 0.70f, 0.0f),
P("SFX","Dark Pad Sweep",
1,1, 0.50f,+5.f,
500.f,0.35f,0.85f,
0.40f,0.90f,0.90f,1.20f,
0.80f,1.20f,0.60f,1.00f, -18.f,
0.18f, 0.90f, 0.0f),
};
return patches;
}
// Flattened labels for the stored preset index
juce::StringArray getFlatPresetLabels()
{
juce::StringArray a;
for (auto& p : getFactoryPatches())
a.add (p.category + " - " + p.name);
return a;
}
inline void setFloatParam (juce::AudioProcessorValueTreeState& apvts,
const juce::String& id, float value)
{
if (auto* param = apvts.getParameter (id))
if (auto* rp = dynamic_cast<juce::RangedAudioParameter*> (param))
{
const float norm = rp->convertTo0to1 (value);
param->beginChangeGesture();
param->setValueNotifyingHost (juce::jlimit (0.0f, 1.0f, norm));
param->endChangeGesture();
}
}
inline void setChoiceParam (juce::AudioProcessorValueTreeState& apvts,
const juce::String& id, int index)
{
if (auto* param = apvts.getParameter (id))
if (auto* cp = dynamic_cast<juce::AudioParameterChoice*> (param))
{
const int n = cp->choices.size();
index = juce::jlimit (0, juce::jmax (0, n - 1), index);
const float norm = (n > 1 ? (float) index / (float) (n - 1) : 0.0f);
param->beginChangeGesture();
param->setValueNotifyingHost (juce::jlimit (0.0f, 1.0f, norm));
param->endChangeGesture();
}
}
} // namespace
//==============================================================
// SimpleSound / SimpleVoice
//==============================================================
bool SimpleSound::appliesToNote (int) { return true; }
bool SimpleSound::appliesToChannel (int) { return true; }
bool SimpleVoice::canPlaySound (juce::SynthesiserSound* s)
{
return dynamic_cast<SimpleSound*> (s) != nullptr;
}
void SimpleVoice::setParameters (juce::AudioProcessorValueTreeState& apvts)
{
oscAChoice = (int) apvts.getRawParameterValue ("oscA")->load();
oscBChoice = (int) apvts.getRawParameterValue ("oscB")->load();
mix = (float) apvts.getRawParameterValue ("mix")->load();
detune = (float) apvts.getRawParameterValue ("detune")->load();
cutoff = (float) apvts.getRawParameterValue ("cutoff")->load();
reso = (float) apvts.getRawParameterValue ("reso")->load();
envAmt = (float) apvts.getRawParameterValue ("envAmt")->load();
filterBypass = apvts.getRawParameterValue ("filterBypass")->load() > 0.5f;
// LFO pulls
lfoRateHz = (float) apvts.getRawParameterValue ("lfoRate")->load();
lfoToCutOct = (float) apvts.getRawParameterValue ("lfoToCut")->load();
lfoToPitchCt = (float) apvts.getRawParameterValue ("lfoToPitch")->load();
ampParams.attack = (float) apvts.getRawParameterValue ("aA")->load();
ampParams.decay = (float) apvts.getRawParameterValue ("dA")->load();
ampParams.sustain = (float) apvts.getRawParameterValue ("sA")->load();
ampParams.release = (float) apvts.getRawParameterValue ("rA")->load();
filterParams.attack = (float) apvts.getRawParameterValue ("aF")->load();
filterParams.decay = (float) apvts.getRawParameterValue ("dF")->load();
filterParams.sustain = (float) apvts.getRawParameterValue ("sF")->load();
filterParams.release = (float) apvts.getRawParameterValue ("rF")->load();
adsrAmp.setParameters (ampParams);
adsrFilter.setParameters (filterParams);
// Smooth only targets (no skip)
detuneSmoothed.setTargetValue (detune);
cutoffSmoothed.setTargetValue (cutoff);
resoSmoothed.setTargetValue (reso);
envAmtSmoothed.setTargetValue (envAmt);
lfoCutSmoothed.setTargetValue (lfoToCutOct);
lfoPitchSmoothed.setTargetValue (lfoToPitchCt);
}
void SimpleVoice::startNote (int midiNoteNumber, float velocity, juce::SynthesiserSound*, int)
{
currentNoteHz = (float) juce::MidiMessage::getMidiNoteInHertz (midiNoteNumber);
velocityGain = juce::jlimit (0.0f, 1.0f, velocity);
// Ensure minimum attack so starting ramps are never instant
const float minA = 0.003f;
auto p = adsrAmp.getParameters();
p.attack = juce::jmax (p.attack, minA);
adsrAmp.setParameters (p);
// Reset per-note
triState = 0.0f;
forcedOffActive = false;
forcedOffCounter = forcedOffSamples = 0;
// Reset LFO phase on key-on
lfoPhase = 0.0f;
// Per-voice SVF
const double sr = getSampleRate();
juce::dsp::ProcessSpec spec { sr, 1u, 1u };
lpFilter.reset(); lpFilter.prepare (spec);
lpFilter.setType (juce::dsp::StateVariableTPTFilterType::lowpass);
// SR-aware triangle leak: tau ≈ 40 ms
const float Tc = 0.040f;
triLeakCoeff = 1.0f - std::exp (-1.0f / (float) (sr * Tc));
// Smoothers
detuneSmoothed.reset (sr, 0.020);
cutoffSmoothed.reset (sr, 0.020);
resoSmoothed.reset (sr, 0.020);
envAmtSmoothed.reset (sr, 0.020);
cutoffModSmooth.reset (sr, 0.005);
lfoCutSmoothed.reset (sr, 0.020);
lfoPitchSmoothed.reset (sr, 0.020);
detuneSmoothed.setCurrentAndTargetValue (detune);
cutoffSmoothed.setCurrentAndTargetValue (cutoff);
resoSmoothed.setCurrentAndTargetValue (reso);
envAmtSmoothed.setCurrentAndTargetValue (envAmt);
cutoffModSmooth.setCurrentAndTargetValue (cutoff);
lfoCutSmoothed.setCurrentAndTargetValue (lfoToCutOct);
lfoPitchSmoothed.setCurrentAndTargetValue (lfoToPitchCt);
adsrAmp.noteOn();
adsrFilter.noteOn();
// ~2.5 ms fade-in
rampSamples = (int) std::ceil (sr * 0.0025);
rampCounter = 0;
}
void SimpleVoice::stopNote (float /*velocity*/, bool allowTailOff)
{
adsrAmp.noteOff();
adsrFilter.noteOff();
if (! allowTailOff)
{
const double sr = getSampleRate();
forcedOffSamples = (int) std::ceil (sr * 0.003);
forcedOffCounter = forcedOffSamples;
forcedOffActive = true;
return;
}
if (! adsrAmp.isActive())
clearCurrentNote();
}
void SimpleVoice::renderNextBlock (juce::AudioBuffer<float>& out, int start, int num)
{
const float sr = (float) getSampleRate();
if (sr <= 0.0f) return;
for (int i = 0; i < num; ++i)
{
// --- LFO (sine) ---
const float lfoDt = juce::jmax (0.0f, lfoRateHz) / sr;
const float lfoVal = (lfoRateHz > 0.0f ? std::sin (juce::MathConstants<float>::twoPi * lfoPhase) : 0.0f);
lfoPhase = wrap01 (lfoPhase + lfoDt);
const float vibCents = lfoPitchSmoothed.getNextValue() * lfoVal;
// Freq increments with vibrato applied to both oscillators
const float hzBaseA = currentNoteHz * std::pow (2.0f, (vibCents) / 1200.0f);
const float hzBaseB = currentNoteHz * std::pow (2.0f, (detuneSmoothed.getNextValue() + vibCents) / 1200.0f);
const float dtA = juce::jmin (0.5f, hzBaseA / sr); // clamp dt to be safe
const float dtB = juce::jmin (0.5f, hzBaseB / sr);
// Band-limited oscillators (free-running phase)
const float sA = oscBLEP (oscAChoice, phaseA, dtA);
const float sB = oscBLEP (oscBChoice, phaseB, dtB);
phaseA = wrap01 (phaseA + dtA);
phaseB = wrap01 (phaseB + dtB);
// Voice mix + headroom
float raw = (1.0f - mix) * sA + mix * sB;
raw *= 0.65f; // voice-level headroom
// Filter env + LFO cutoff modulation if active
float y = raw;
if (! filterBypass)
{
const float fEnv = juce::jlimit (0.0f, 1.0f, adsrFilter.getNextSample());
// Exponent adds: envAmt*env + lfoCut*LFO (both in octaves)
const float expo = envAmtSmoothed.getNextValue() * fEnv
+ lfoCutSmoothed.getNextValue() * lfoVal;
float modCut = cutoffSmoothed.getNextValue() * std::pow (2.0f, expo);
modCut = juce::jlimit (20.0f, sr * 0.45f, modCut);
cutoffModSmooth.setTargetValue (modCut);
lpFilter.setCutoffFrequency (cutoffModSmooth.getNextValue());
lpFilter.setResonance (resoSmoothed.getNextValue());
y = lpFilter.processSample (0, raw);
}
// Amp env
float amp = adsrAmp.getNextSample() * velocityGain;
// Start ramp
if (rampCounter < rampSamples)
{
const float t = (float) rampCounter / (float) rampSamples;
amp *= t * t;
++rampCounter;
}
// Forced stop ramp
if (forcedOffActive && forcedOffSamples > 0)
{
const float t = (float) forcedOffCounter / (float) forcedOffSamples;
amp *= juce::jlimit (0.0f, 1.0f, t);
if (--forcedOffCounter <= 0) { forcedOffActive = false; clearCurrentNote(); }
}
const float s = y * amp;
for (int ch = 0; ch < out.getNumChannels(); ++ch)
out.addSample (ch, start + i, s);
}
if (! adsrAmp.isActive() && ! forcedOffActive)
clearCurrentNote();
}
void SimpleVoice::reset()
{
lpFilter.reset();
triState = 0.0f;
lfoPhase = 0.0f;
forcedOffActive = false;
forcedOffSamples = forcedOffCounter = 0;
}
//==============================================================
// Processor
//==============================================================
TwoOscAudioProcessor::TwoOscAudioProcessor()
: juce::AudioProcessor (BusesProperties()
#if ! JucePlugin_IsMidiEffect
#if ! JucePlugin_IsSynth
.withInput ("Input", juce::AudioChannelSet::stereo(), true)
#endif
.withOutput ("Output", juce::AudioChannelSet::stereo(), true)
#endif
),
apvts (*this, nullptr, "PARAMS", createParameterLayout())
{
apvts.addParameterListener ("presetIndex", this);
const int preset = (int) apvts.getRawParameterValue ("presetIndex")->load();
applyPresetByIndex (preset, false);
}
TwoOscAudioProcessor::~TwoOscAudioProcessor()
{
apvts.removeParameterListener ("presetIndex", this);
}
// Basic info overrides (linker was missing these)
const juce::String TwoOscAudioProcessor::getName() const { return JucePlugin_Name; }
bool TwoOscAudioProcessor::acceptsMidi() const { return true; }
bool TwoOscAudioProcessor::producesMidi() const { return false; }
bool TwoOscAudioProcessor::isMidiEffect() const { return false; }
double TwoOscAudioProcessor::getTailLengthSeconds() const { return 0.0; }
// Prepare / process
void TwoOscAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
lastSampleRate = sampleRate;
// Kill denormals globally (prevents random CPU spikes / noise)
juce::FloatVectorOperations::disableDenormalisedNumberSupport();
// Synth setup
synth.clearVoices();
synth.clearSounds();
// Choose the polyphony here
for (int i = 0; i < 32; ++i)
synth.addVoice (new SimpleVoice());
synth.addSound (new SimpleSound());
synth.setCurrentPlaybackSampleRate (sampleRate);
synth.setNoteStealingEnabled (true);
// Optional hidden HPF (prepared even if bypassed)
juce::dsp::ProcessSpec spec { sampleRate, (juce::uint32) samplesPerBlock, 2 };
hpFilter.reset();
hpFilter.prepare (spec);
*hpFilter.state = *juce::dsp::IIR::Coefficients<float>::makeHighPass ((float) sampleRate, 40.0f);
// Global smoothers (if you keep them)
smoothedCutoff.reset (sampleRate, 0.01);
smoothedReso.reset (sampleRate, 0.01);
smoothedEnvAmt.reset (sampleRate, 0.01);
// Make sure all voices start in a clean state
for (int i = 0; i < synth.getNumVoices(); ++i)
if (auto* v = dynamic_cast<SimpleVoice*> (synth.getVoice (i)))
v->reset();
}
#ifndef JucePlugin_PreferredChannelConfigurations
bool TwoOscAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
#if JucePlugin_IsSynth
juce::ignoreUnused (layouts);
return true;
#else
if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono()
&& layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
return false;
#if ! JucePlugin_IsSynth
if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
return false;
#endif
return true;
#endif
}
#endif
void TwoOscAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer,
juce::MidiBuffer& midiMessages)
{
juce::ScopedNoDenormals noDenormals;
buffer.clear();
// --- Push current parameter values into each voice ---
for (int i = 0; i < synth.getNumVoices(); ++i)
if (auto* voice = dynamic_cast<SimpleVoice*> (synth.getVoice (i)))
voice->setParameters (apvts);
// --- Render voices ---
synth.renderNextBlock (buffer, midiMessages, 0, buffer.getNumSamples());
// --- Optional high-pass (normally bypassed) ---
// juce::dsp::AudioBlock<float> block (buffer);
// hpFilter.process (juce::dsp::ProcessContextReplacing<float> (block));
// --- Stage 1: inter-voice safety headroom ---
buffer.applyGain (0.80f);
// --- Stage 2: ultra-soft output saturation (very gentle) ---
constexpr float k = 0.12f; // smaller = softer
for (int ch = 0; ch < buffer.getNumChannels(); ++ch)
{
float* d = buffer.getWritePointer (ch);
const int n = buffer.getNumSamples();
for (int i = 0; i < n; ++i)
{
const float x = d[i];
d[i] = x - k * x * x * x;
}
}
// --- Stage 3: master output gain (dB) ---
const float masterDb = (float) apvts.getRawParameterValue ("masterGain")->load();
buffer.applyGain (juce::Decibels::decibelsToGain (masterDb));
// --- Stage 4: anti-denormal / NaN cleanup ---
for (int ch = 0; ch < buffer.getNumChannels(); ++ch)
{
float* d = buffer.getWritePointer (ch);
for (int i = 0; i < buffer.getNumSamples(); ++i)
if (! std::isfinite (d[i]))
d[i] = 0.0f;
}
}
//==============================================================================
// Editor
bool TwoOscAudioProcessor::hasEditor() const { return true; }
juce::AudioProcessorEditor* TwoOscAudioProcessor::createEditor()
{
return new TwoOscAudioProcessorEditor (*this);
}
//==============================================================================
// State
void TwoOscAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
{
auto state = apvts.copyState();
std::unique_ptr<juce::XmlElement> xml (state.createXml());
copyXmlToBinary (*xml, destData);
}
void TwoOscAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
std::unique_ptr<juce::XmlElement> xml (getXmlFromBinary (data, sizeInBytes));
if (xml != nullptr && xml->hasTagName (apvts.state.getType()))
apvts.replaceState (juce::ValueTree::fromXml (*xml));
}
//==============================================================================
// APVTS listener (for preset changes)
void TwoOscAudioProcessor::parameterChanged (const juce::String& paramID, float newValue)
{
if (paramID == "presetIndex")
applyPresetByIndex ((int) newValue, false);
}
//==============================================================================
// Preset helpers (for the editor)
juce::StringArray TwoOscAudioProcessor::getPresetCategories() const
{
juce::StringArray cats;
for (auto& p : getFactoryPatches())
if (! cats.contains (p.category))
cats.add (p.category);
return cats;
}
juce::Array<int> TwoOscAudioProcessor::getPresetIndicesForCategory (const juce::String& category) const
{
juce::Array<int> idx;
const auto& bank = getFactoryPatches();
for (int i = 0; i < (int) bank.size(); ++i)
if (bank[(size_t) i].category == category)
idx.add (i);
return idx;
}
juce::String TwoOscAudioProcessor::getPresetLabel (int index) const
{
const auto& bank = getFactoryPatches();
if ((size_t) index < bank.size())
return bank[(size_t) index].name;
return {};
}
juce::String TwoOscAudioProcessor::getCurrentPresetLabel() const
{
const int idx = (int) apvts.getRawParameterValue ("presetIndex")->load();
auto labels = getFlatPresetLabels();
if (idx >= 0 && idx < labels.size())
return labels[idx];
return {};
}
//==============================================================================
// Apply preset (writes into APVTS)
void TwoOscAudioProcessor::applyPresetByIndex (int index, bool setParamToo)
{
const auto& bank = getFactoryPatches();
if (bank.empty()) return;
index = juce::jlimit (0, (int) bank.size() - 1, index);
const auto& p = bank[(size_t) index];
if (isApplyingPreset.exchange (true))
return;
setChoiceParam (apvts, "oscA", p.oscA);
setChoiceParam (apvts, "oscB", p.oscB);
setFloatParam (apvts, "mix", p.mix);
setFloatParam (apvts, "detune", p.detune);
setFloatParam (apvts, "cutoff", p.cutoff);
setFloatParam (apvts, "reso", p.reso);
setFloatParam (apvts, "envAmt", p.envAmt);
setFloatParam (apvts, "aA", p.aA); setFloatParam (apvts, "dA", p.dA);
setFloatParam (apvts, "sA", p.sA); setFloatParam (apvts, "rA", p.rA);
setFloatParam (apvts, "aF", p.aF); setFloatParam (apvts, "dF", p.dF);
setFloatParam (apvts, "sF", p.sF); setFloatParam (apvts, "rF", p.rF);
setFloatParam (apvts, "masterGain", p.master);
// NEW: LFO params from preset
setFloatParam (apvts, "lfoRate", p.lfoRate);
setFloatParam (apvts, "lfoToCut", p.lfoToCut);
setFloatParam (apvts, "lfoToPitch", p.lfoToPitch);
if (setParamToo)
if (auto* prm = dynamic_cast<juce::AudioParameterChoice*> (apvts.getParameter ("presetIndex")))
{
const int n = prm->choices.size();
const float norm = (n > 1 ? (float) index / (float) (n - 1) : 0.0f);
prm->beginChangeGesture();
prm->setValueNotifyingHost (juce::jlimit (0.0f, 1.0f, norm));
prm->endChangeGesture();
}
isApplyingPreset = false;
}
//==============================================================================
// Parameter layout
juce::AudioProcessorValueTreeState::ParameterLayout TwoOscAudioProcessor::createParameterLayout()
{
std::vector<std::unique_ptr<juce::RangedAudioParameter>> p;
p.push_back (std::make_unique<juce::AudioParameterChoice>(
"presetIndex","Preset", getFlatPresetLabels(), 0));
p.push_back (std::make_unique<juce::AudioParameterChoice>(
"oscA","Osc A", juce::StringArray{ "Sine","Saw","Square","Tri" }, 1));
p.push_back (std::make_unique<juce::AudioParameterChoice>(
"oscB","Osc B", juce::StringArray{ "Sine","Saw","Square","Tri" }, 2));
p.push_back (std::make_unique<juce::AudioParameterFloat>("mix","A/B Mix",0.0f,1.0f,0.35f));
p.push_back (std::make_unique<juce::AudioParameterFloat>("detune","Detune (c)",-24.0f,24.0f,-5.0f));
juce::NormalisableRange<float> cutoffRange (20.0f, 20000.0f);
cutoffRange.setSkewForCentre (500.0f);
p.push_back (std::make_unique<juce::AudioParameterFloat>("cutoff","Cutoff",cutoffRange,180.0f));
p.push_back (std::make_unique<juce::AudioParameterFloat>("reso","Resonance",0.1f,1.0f,0.18f));
p.push_back (std::make_unique<juce::AudioParameterFloat>("envAmt","Filter Env",-1.0f,1.0f,0.25f));
p.push_back (std::make_unique<juce::AudioParameterFloat>("aA","Amp A",0.001f,2.0f,0.01f));
p.push_back (std::make_unique<juce::AudioParameterFloat>("dA","Amp D",0.001f,2.0f,0.10f));
p.push_back (std::make_unique<juce::AudioParameterFloat>("sA","Amp S",0.0f,1.0f,0.70f));
p.push_back (std::make_unique<juce::AudioParameterFloat>("rA","Amp R",0.001f,4.0f,0.12f));
p.push_back (std::make_unique<juce::AudioParameterFloat>("aF","Filt A",0.001f,2.0f,0.015f));
p.push_back (std::make_unique<juce::AudioParameterFloat>("dF","Filt D",0.001f,2.0f,0.18f));
p.push_back (std::make_unique<juce::AudioParameterFloat>("sF","Filt S",0.0f,1.0f,0.10f));
p.push_back (std::make_unique<juce::AudioParameterFloat>("rF","Filt R",0.001f,4.0f,0.18f));
p.push_back (std::make_unique<juce::AudioParameterBool>("filterBypass","Deactivate Filter", false));
// ------- NEW: LFO parameters -------
// Rate: 0..20 Hz (0 = off)
p.push_back (std::make_unique<juce::AudioParameterFloat>("lfoRate","LFO Rate (Hz)", 0.0f, 20.0f, 0.0f));
// Depth to cutoff in octaves (-4..+4 for full range if you wish; keep 0..4 positive here)
p.push_back (std::make_unique<juce::AudioParameterFloat>("lfoToCut","LFO → Cutoff (oct)", -4.0f, 4.0f, 0.0f));
// Depth to pitch in cents
p.push_back (std::make_unique<juce::AudioParameterFloat>("lfoToPitch","LFO → Pitch (c)", 0.0f, 100.0f, 0.0f));
p.push_back (std::make_unique<juce::AudioParameterFloat>("masterGain","Master",-36.0f,12.0f,-18.0f));
return { p.begin(), p.end() };
}
//==============================================================================
// Misc helpers (kept for completeness)
void TwoOscAudioProcessor::updateFilter()
{
smoothedCutoff.setTargetValue ((float) apvts.getRawParameterValue ("cutoff")->load());
smoothedReso.setTargetValue ((float) apvts.getRawParameterValue ("reso")->load());
smoothedEnvAmt.setTargetValue ((float) apvts.getRawParameterValue ("envAmt")->load());
}
//==============================================================================
// Factory
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
return new TwoOscAudioProcessor();
}