#include "PluginProcessor.h" #include "PluginEditor.h" #include //============================================================== // 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& getFactoryPatches() { static const std::vector 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 (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 (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 (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& 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::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::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 (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& 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 (synth.getVoice (i))) voice->setParameters (apvts); // --- Render voices --- synth.renderNextBlock (buffer, midiMessages, 0, buffer.getNumSamples()); // --- Optional high-pass (normally bypassed) --- // juce::dsp::AudioBlock block (buffer); // hpFilter.process (juce::dsp::ProcessContextReplacing (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 xml (state.createXml()); copyXmlToBinary (*xml, destData); } void TwoOscAudioProcessor::setStateInformation (const void* data, int sizeInBytes) { std::unique_ptr 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 TwoOscAudioProcessor::getPresetIndicesForCategory (const juce::String& category) const { juce::Array 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 (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> p; p.push_back (std::make_unique( "presetIndex","Preset", getFlatPresetLabels(), 0)); p.push_back (std::make_unique( "oscA","Osc A", juce::StringArray{ "Sine","Saw","Square","Tri" }, 1)); p.push_back (std::make_unique( "oscB","Osc B", juce::StringArray{ "Sine","Saw","Square","Tri" }, 2)); p.push_back (std::make_unique("mix","A/B Mix",0.0f,1.0f,0.35f)); p.push_back (std::make_unique("detune","Detune (c)",-24.0f,24.0f,-5.0f)); juce::NormalisableRange cutoffRange (20.0f, 20000.0f); cutoffRange.setSkewForCentre (500.0f); p.push_back (std::make_unique("cutoff","Cutoff",cutoffRange,180.0f)); p.push_back (std::make_unique("reso","Resonance",0.1f,1.0f,0.18f)); p.push_back (std::make_unique("envAmt","Filter Env",-1.0f,1.0f,0.25f)); p.push_back (std::make_unique("aA","Amp A",0.001f,2.0f,0.01f)); p.push_back (std::make_unique("dA","Amp D",0.001f,2.0f,0.10f)); p.push_back (std::make_unique("sA","Amp S",0.0f,1.0f,0.70f)); p.push_back (std::make_unique("rA","Amp R",0.001f,4.0f,0.12f)); p.push_back (std::make_unique("aF","Filt A",0.001f,2.0f,0.015f)); p.push_back (std::make_unique("dF","Filt D",0.001f,2.0f,0.18f)); p.push_back (std::make_unique("sF","Filt S",0.0f,1.0f,0.10f)); p.push_back (std::make_unique("rF","Filt R",0.001f,4.0f,0.18f)); p.push_back (std::make_unique("filterBypass","Deactivate Filter", false)); // ------- NEW: LFO parameters ------- // Rate: 0..20 Hz (0 = off) p.push_back (std::make_unique("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("lfoToCut","LFO → Cutoff (oct)", -4.0f, 4.0f, 0.0f)); // Depth to pitch in cents p.push_back (std::make_unique("lfoToPitch","LFO → Pitch (c)", 0.0f, 100.0f, 0.0f)); p.push_back (std::make_unique("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(); }