From 45183c21a4b9b3e2526b2668ea4785736cecea2f Mon Sep 17 00:00:00 2001 From: Roboboffin Date: Tue, 21 Oct 2025 17:22:44 +0100 Subject: [PATCH] Initial commit --- AGENTS.md | 33 ++ PluginEditor.cpp | 431 +++++++++++++++++ PluginEditor.h | 139 ++++++ PluginProcessor.cpp | 1078 +++++++++++++++++++++++++++++++++++++++++++ PluginProcessor.h | 241 ++++++++++ 5 files changed, 1922 insertions(+) create mode 100644 AGENTS.md create mode 100644 PluginEditor.cpp create mode 100644 PluginEditor.h create mode 100644 PluginProcessor.cpp create mode 100644 PluginProcessor.h diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b7eaf58 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,33 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `PluginProcessor.h/cpp` contain `TwoOscAudioProcessor`, `SimpleVoice`, the APVTS layout, and factory presets—centralize DSP, parameter definitions, and state handling here. +- `PluginEditor.h/cpp` manage the UI components, parameter attachments, and layout helpers; register new parameter IDs in the processor and wire their controls in these files. +- Factory presets sit in `getFactoryPatches()`; keep categories organised so the preset popup remains easy to scan. + +## Build, Test, and Development Commands +- Make JUCE modules available (export `JUCE_MODULES_PATH` or keep a JUCE checkout beside this repository). +- Configure and build out-of-tree with CMake: + ```bash + cmake -S . -B build -DJUCE_MODULES_PATH=$JUCE_MODULES_PATH + cmake --build build --target NeuralSynthEd_Standalone + cmake --build build --target NeuralSynthEd_VST3 + ``` +- Leave `build/` untracked and use the standalone target for smoke tests before validating in a DAW. + +## Coding Style & Naming Conventions +- Follow JUCE style: 4 spaces, same-line braces, and `//==============================================================================` separators for major sections. +- Types use PascalCase, members camelCase, parameter IDs lowerCamel (e.g., `lfoRate`, `oscA`); reserve `auto` for self-evident types. +- Keep realtime code allocation-free and tuck UI-only helpers inside the editor files. + +## Testing Guidelines +- Manual passes in the standalone build and JUCE `AudioPluginHost` cover current needs; exercise preset recall, automation, MIDI, and state persistence whenever parameters move. +- Describe expected sonic or UX changes in PR notes so reviewers can mirror the checks. + +## Commit & Pull Request Guidelines +- Use Conventional Commits with imperative subjects; add a brief body when behaviour changes or migrations occur. +- PRs should summarise functional impact, link issues, attach audio clips or screenshots for DSP/UI tweaks, and avoid force-pushes post-review without noting the delta. + +## Preset & State Tips +- Gain-match new presets (master around -18 dBFS) and provide explicit LFO values to keep legacy sounds stable. +- Update APVTS defaults and editor control ranges together, refreshing browser labels if new preset categories appear. diff --git a/PluginEditor.cpp b/PluginEditor.cpp new file mode 100644 index 0000000..5ced797 --- /dev/null +++ b/PluginEditor.cpp @@ -0,0 +1,431 @@ +#include "PluginEditor.h" +#include "PluginProcessor.h" + +// Small helpers to set APVTS params safely (gesture + normalized) +namespace +{ + inline void setFloatParam (juce::AudioProcessorValueTreeState& apvts, + const juce::String& id, float value) + { + if (auto* p = apvts.getParameter (id)) + if (auto* rp = dynamic_cast (p)) + { + auto norm = rp->getNormalisableRange().convertTo0to1 (value); + p->beginChangeGesture(); + p->setValueNotifyingHost (juce::jlimit (0.0f, 1.0f, norm)); + p->endChangeGesture(); + } + } + + inline void setChoiceParam (juce::AudioProcessorValueTreeState& apvts, + const juce::String& id, int index) + { + if (auto* p = apvts.getParameter (id)) + if (auto* cp = dynamic_cast (p)) + { + const int n = cp->choices.size(); + index = juce::jlimit (0, juce::jmax (0, n - 1), index); + float norm = (n > 1 ? (float) index / (float) (n - 1) : 0.0f); + p->beginChangeGesture(); + p->setValueNotifyingHost (juce::jlimit (0.0f, 1.0f, norm)); + p->endChangeGesture(); + } + } +} + +//============================================================================== +// Editor + +TwoOscAudioProcessorEditor::TwoOscAudioProcessorEditor (TwoOscAudioProcessor& p) +: AudioProcessorEditor (&p), processor (p) +{ + // Smaller overall size + setSize (860, 460); + + // Header title + title.setText ("Samedi Dimanche AV-60", juce::dontSendNotification); + title.setJustificationType (juce::Justification::centredLeft); + title.setFont (juce::Font (juce::FontOptions (20.0f, juce::Font::bold))); // slightly smaller + addAndMakeVisible (title); + + // Preset display + Browse button inside a framed "preset screen" + presetDisplay.setJustificationType (juce::Justification::centred); + presetDisplay.setFont (juce::Font (juce::FontOptions (13.0f))); + presetDisplay.setColour (juce::Label::textColourId, juce::Colours::white.withAlpha (0.85f)); + addAndMakeVisible (presetDisplay); + + browseBtn.setButtonText ("Browse..."); + browseBtn.onClick = [this]{ showPresetPopup(); }; + addAndMakeVisible (browseBtn); + + // Controls + oscA.addItemList ({"Sine","Saw","Square","Tri"}, 1); + oscB.addItemList ({"Sine","Saw","Square","Tri"}, 1); + + mix.setRange (0,1); + detune.setRange (-24,24,0.01); + cutoff.setRange (20,20000,1); cutoff.setSkewFactorFromMidPoint (500.0); + reso.setRange (0.1,1.0,0.0); + envAmt.setRange (-1,1,0.0); + aA.setRange (0.001,2.0,0.0); dA.setRange (0.001,2.0,0.0); + sA.setRange (0,1); rA.setRange (0.001,4.0,0.0); + aF.setRange (0.001,2.0,0.0); dF.setRange (0.001,2.0,0.0); + sF.setRange (0,1); rF.setRange (0.001,4.0,0.0); + masterGain.setRange (-36.0, 12.0, 0.1); // dB + + // LFO GUI ranges (matching APVTS) + lfoRate.setRange (0.0, 20.0, 0.01); // Hz + lfoDepth.setRange (-4.0, 4.0, 0.0); // octaves (mapped to lfoToCut) + + for (auto* s : { &mix,&detune,&cutoff,&reso,&envAmt, + &aA,&dA,&sA,&rA,&aF,&dF,&sF,&rF,&masterGain, + &lfoRate,&lfoDepth }) + styleKnob (*s); + + for (auto* l : { &lOscA,&lOscB,&lMix,&lDetune,&lCutoff,&lReso,&lEnv, + &lAA,&lDA,&lSA,&lRA,&lAF,&lDF,&lSF,&lRF,&lMaster, + &lLfoRate,&lLfoDepth }) + styleLabel (*l); + + // Add with captions + addLabeled (oscA, lOscA, "Osc A"); + addLabeled (oscB, lOscB, "Osc B"); + addLabeled (lfoRate, lLfoRate, "LFO Rate (Hz)"); + addLabeled (lfoDepth, lLfoDepth, "LFO Depth (oct)"); + addLabeled (mix, lMix, "A/B Mix"); + addLabeled (detune, lDetune, "Detune (c)"); + addLabeled (cutoff, lCutoff, "Cutoff"); + addLabeled (reso, lReso, "Resonance"); + addLabeled (aF, lAF, "Filt A"); + addLabeled (dF, lDF, "Filt D"); + addLabeled (sF, lSF, "Filt S"); + addLabeled (rF, lRF, "Filt R"); + addLabeled (envAmt, lEnv, "Filter Env"); + addLabeled (aA, lAA, "Amp A"); + addLabeled (dA, lDA, "Amp D"); + addLabeled (sA, lSA, "Amp S"); + addLabeled (rA, lRA, "Amp R"); + addLabeled (masterGain, lMaster, "Master"); + + // Attach to APVTS + auto& apvts = processor.apvts; + aOscA.reset (new CA (apvts, "oscA", oscA)); + aOscB.reset (new CA (apvts, "oscB", oscB)); + aMix.reset (new SA (apvts, "mix", mix)); + aDetune.reset (new SA (apvts, "detune", detune)); + aCutoff.reset (new SA (apvts, "cutoff", cutoff)); + aReso.reset (new SA (apvts, "reso", reso)); + aEnv.reset (new SA (apvts, "envAmt", envAmt)); + aAA.reset (new SA (apvts, "aA", aA)); aDA.reset (new SA (apvts, "dA", dA)); + aSA.reset (new SA (apvts, "sA", sA)); aRA.reset (new SA (apvts, "rA", rA)); + aAF.reset (new SA (apvts, "aF", aF)); aDF.reset (new SA (apvts, "dF", dF)); + aSF.reset (new SA (apvts, "sF", sF)); aRF.reset (new SA (apvts, "rF", rF)); + aMaster.reset (new SA (apvts, "masterGain", masterGain)); + + // LFO attachments + aLfoRate .reset (new SA (apvts, "lfoRate", lfoRate)); + aLfoDepth.reset (new SA (apvts, "lfoToCut", lfoDepth)); + + updatePresetDisplay(); // show the current preset name on load +} + +TwoOscAudioProcessorEditor::~TwoOscAudioProcessorEditor() +{ + for (auto* s : { &mix,&detune,&cutoff,&reso,&envAmt, + &aA,&dA,&sA,&rA,&aF,&dF,&sF,&rF,&masterGain, + &lfoRate,&lfoDepth }) + s->setLookAndFeel (nullptr); +} + +//------------------------------------------------------------------------------ +// Styling +void TwoOscAudioProcessorEditor::styleKnob (juce::Slider& s) +{ + s.setLookAndFeel (&retroLNF); + s.setSliderStyle (juce::Slider::RotaryVerticalDrag); + s.setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0); + + // SW -> SE 270° sweep + const float start = juce::MathConstants::pi * 0.75f; // 135° + const float sweep = juce::MathConstants::pi * 1.5f; // 270° + s.setRotaryParameters (start, start + sweep, true); +} + +void TwoOscAudioProcessorEditor::styleLabel (juce::Label& l) +{ + l.setJustificationType (juce::Justification::centred); + l.setColour (juce::Label::textColourId, juce::Colours::white.withAlpha (0.85f)); + l.setFont (juce::Font (juce::FontOptions (11.0f, juce::Font::plain))); // a touch smaller + addAndMakeVisible (l); +} + +void TwoOscAudioProcessorEditor::addLabeled (juce::Component& c, juce::Label& label, const juce::String& text) +{ + addAndMakeVisible (c); + label.setText (text, juce::dontSendNotification); +} + +//------------------------------------------------------------------------------ +// Layout & painting +void TwoOscAudioProcessorEditor::paint (juce::Graphics& g) +{ + g.fillAll (juce::Colours::black); + + const int corner = 10; + auto chrome = getLocalBounds().reduced (8); + + // Header band + auto header = chrome.removeFromTop (headerH); + g.setColour (juce::Colours::white.withAlpha (0.06f)); + g.fillRoundedRectangle (header.toFloat(), (float) corner); + g.setColour (juce::Colours::white.withAlpha (0.10f)); + g.drawRoundedRectangle (header.toFloat(), (float) corner, 1.0f); + + // Body panel + g.setColour (juce::Colours::white.withAlpha (0.08f)); + g.fillRoundedRectangle (chrome.toFloat(), (float) corner); + g.setColour (juce::Colours::white.withAlpha (0.12f)); + g.drawRoundedRectangle (chrome.toFloat(), (float) corner, 1.0f); + + // Preset "screen" + if (! presetBarBounds.isEmpty()) + { + auto r = presetBarBounds.toFloat(); + const float innerCorner = 6.0f; + g.setColour (juce::Colours::white.withAlpha (0.06f)); + g.fillRoundedRectangle (r, innerCorner); + g.setColour (juce::Colours::grey.withAlpha (0.45f)); + g.drawRoundedRectangle (r, innerCorner, 1.0f); + } + + // --- Group backgrounds --- + auto drawGroup = [&g](juce::Rectangle bounds, juce::Colour fill, juce::Colour stroke) + { + if (bounds.isEmpty()) return; + auto rf = bounds.toFloat(); + const float r = 8.0f; + g.setColour (fill); + g.fillRoundedRectangle (rf, r); + g.setColour (stroke); + g.drawRoundedRectangle (rf, r, 1.0f); + }; + + // Neutral soft grey + const auto softFill = juce::Colours::white.withAlpha (0.10f); + const auto softEdge = juce::Colours::white.withAlpha (0.16f); + + // Amp ADSR: darker + teal + const auto ampFill = juce::Colour::fromRGB (34, 50, 54).withAlpha (0.40f); + const auto ampEdge = juce::Colour::fromRGB (70, 110, 118).withAlpha (0.50f); + + drawGroup (groupCutResBounds, softFill, softEdge); + drawGroup (groupMixDetBounds, softFill, softEdge); + drawGroup (groupLfoBounds, softFill, softEdge); // LFO block + drawGroup (groupAmpBounds, ampFill, ampEdge); +} + +void TwoOscAudioProcessorEditor::resized() +{ + auto r = getLocalBounds().reduced (10); + + // ---------- Header ---------- + auto header = r.removeFromTop (headerH); + { + auto titleArea = header.removeFromLeft (260).reduced (12, 8); + title.setBounds (titleArea); + + auto screen = header.reduced (12, 8); + presetBarBounds = screen; + + const int lineH = 30; // slightly smaller + const int btnW = 98; + auto inner = screen.withHeight (lineH) + .withY (screen.getY() + (screen.getHeight() - lineH) / 2) + .reduced (8, 0); + + auto lbl = inner.removeFromLeft (inner.getWidth() - btnW - 8); + presetDisplay.setBounds (lbl); + inner.removeFromLeft (8); + browseBtn.setBounds (inner.removeFromLeft (btnW)); + } + + r.removeFromTop (6); + const auto bodyClamp = r; + + // ---------- helpers (tight captions) ---------- + auto putMenu = [&](juce::ComboBox& box, juce::Label& lab, juce::Rectangle cell, int menuH = 30) + { + const int labelH = 14; + lab.setBounds (cell.removeFromTop (labelH)); + cell.removeFromTop (2); + auto b = cell.withHeight (menuH); + b.setY (cell.getY() + (cell.getHeight() - menuH) / 2); + box.setBounds (b); + }; + + auto putKnob = [&](juce::Slider& s, juce::Label& lab, juce::Rectangle cell) + { + const int labelH = 14; + lab.setBounds (cell.removeFromTop (labelH)); + cell.removeFromTop (2); + auto k = juce::Rectangle (knobSize, knobSize) + .withCentre ({ cell.getCentreX(), cell.getY() + knobSize / 2 }); + s.setBounds (k); + }; + + auto rowCells = [&](juce::Rectangle row, int cols) + { + juce::Array> cells; + const int totalGap = gap * (cols - 1); + const int colW = (row.getWidth() - totalGap) / cols; + for (int c = 0; c < cols; ++c) + { + auto cell = row.removeFromLeft (colW); + if (c < cols - 1) row.removeFromLeft (gap); + cells.add (cell); + } + return cells; + }; + + const int rowH = knobSize + 14 + 6 + 12; // a little tighter + + // ---------- Top row ---------- + auto topRowArea = r.removeFromTop (rowH); + auto top = rowCells (topRowArea, 6); + { + putMenu (oscA, lOscA, top[0].reduced (6)); + putMenu (oscB, lOscB, top[1].reduced (6)); + + // LFO knobs use the middle two cells + putKnob (lfoRate, lLfoRate, top[2]); + putKnob (lfoDepth, lLfoDepth, top[3]); + + putKnob (mix, lMix, top[4]); + putKnob (detune, lDetune, top[5]); + + // SAFE padding so backgrounds never overlap (use at most half the inter-column gap) + const int safePad = juce::jmax (2, gap / 2 - 2); // with gap=10, pad=3 + + groupLfoBounds = top[2].getUnion (top[3]).expanded (safePad) + .getIntersection (topRowArea).getIntersection (bodyClamp); + + groupMixDetBounds = top[4].getUnion (top[5]).expanded (safePad) + .getIntersection (topRowArea).getIntersection (bodyClamp); + } + + r.removeFromTop (6); + + // ---------- Middle row ---------- + auto midRowArea = r.removeFromTop (rowH); + auto mid = rowCells (midRowArea, 6); + { + putKnob (cutoff, lCutoff, mid[0]); + putKnob (reso, lReso, mid[1]); + putKnob (aF, lAF, mid[2]); + putKnob (dF, lDF, mid[3]); + putKnob (sF, lSF, mid[4]); + putKnob (rF, lRF, mid[5]); + + const int pad = 12; + groupCutResBounds = mid[0].getUnion (mid[1]).expanded (pad) + .getIntersection (midRowArea).getIntersection (bodyClamp); + } + + r.removeFromTop (6); + + // ---------- Bottom row ---------- + auto botRowArea = r.removeFromTop (rowH); + auto bot = rowCells (botRowArea, 6); + { + putKnob (envAmt, lEnv, bot[0]); + putKnob (aA, lAA, bot[1]); + putKnob (dA, lDA, bot[2]); + putKnob (sA, lSA, bot[3]); + putKnob (rA, lRA, bot[4]); + putKnob (masterGain, lMaster, bot[5]); + + const int pad = 14; + groupAmpBounds = bot[1].getUnion (bot[2]).getUnion (bot[3]).getUnion (bot[4]).expanded (pad) + .getIntersection (botRowArea).getIntersection (bodyClamp); + } +} + + + +//------------------------------------------------------------------------------ +// Preset UI +void TwoOscAudioProcessorEditor::updatePresetDisplay() +{ + const auto label = processor.getCurrentPresetLabel(); // "Category - Name" + presetDisplay.setText (label, juce::dontSendNotification); +} + +void TwoOscAudioProcessorEditor::showPresetPopup() +{ + juce::PopupMenu root; + + const juce::StringArray cats = processor.getPresetCategories(); + for (int c = 0; c < cats.size(); ++c) + { + juce::PopupMenu sub; + const auto indices = processor.getPresetIndicesForCategory (cats[c]); + for (int j = 0; j < indices.size(); ++j) + { + const int idx = indices[j]; + const juce::String nm = processor.getPresetLabel (idx); + sub.addItem (1000 + idx, nm); + } + root.addSubMenu (cats[c], sub, sub.getNumItems() > 0); + } + + root.addSeparator(); + root.addItem (999, "Init patch"); + + root.showMenuAsync (juce::PopupMenu::Options(), + [this](int res) + { + if (res == 999) + { + // "Init" = simple neutral patch + setChoiceParam (processor.apvts, "oscA", 0); + setChoiceParam (processor.apvts, "oscB", 0); + setFloatParam (processor.apvts, "mix", 0.5f); + setFloatParam (processor.apvts, "detune", 0.0f); + setFloatParam (processor.apvts, "cutoff", 1000.0f); + setFloatParam (processor.apvts, "reso", 0.24f); + setFloatParam (processor.apvts, "envAmt", 0.30f); + setFloatParam (processor.apvts, "aA", 0.01f); + setFloatParam (processor.apvts, "dA", 0.25f); + setFloatParam (processor.apvts, "sA", 0.85f); + setFloatParam (processor.apvts, "rA", 0.40f); + setFloatParam (processor.apvts, "aF", 0.05f); + setFloatParam (processor.apvts, "dF", 0.30f); + setFloatParam (processor.apvts, "sF", 0.40f); + setFloatParam (processor.apvts, "rF", 0.40f); + setFloatParam (processor.apvts, "masterGain", -18.0f); + + // Reset LFO for Init + setFloatParam (processor.apvts, "lfoRate", 0.0f); + setFloatParam (processor.apvts, "lfoToCut", 0.0f); + setFloatParam (processor.apvts, "lfoToPitch", 0.0f); + + presetDisplay.setText ("Init", juce::dontSendNotification); + return; + } + + if (res >= 1000) + { + const int patchIndex = res - 1000; + if (auto* prm = dynamic_cast (processor.apvts.getParameter ("presetIndex"))) + { + const int n = prm->choices.size(); + const float norm = (n > 1 ? (float) patchIndex / (float) (n - 1) : 0.0f); + prm->beginChangeGesture(); + prm->setValueNotifyingHost (juce::jlimit (0.0f, 1.0f, norm)); + prm->endChangeGesture(); + } + updatePresetDisplay(); + } + }); +} + diff --git a/PluginEditor.h b/PluginEditor.h new file mode 100644 index 0000000..410086a --- /dev/null +++ b/PluginEditor.h @@ -0,0 +1,139 @@ +#pragma once +#include +class TwoOscAudioProcessor; + +// ===== Retro 70s LookAndFeel for rotary knobs ===== +class RetroLookAndFeel : public juce::LookAndFeel_V4 +{ +public: + RetroLookAndFeel() + { + setColour (juce::Slider::textBoxTextColourId, juce::Colours::white.withAlpha (0.9f)); + setColour (juce::Slider::textBoxOutlineColourId, juce::Colours::transparentBlack); + } + + void drawRotarySlider (juce::Graphics& g, int x, int y, int w, int h, + float sliderPosProportional, + float /*rotaryStartAngle*/, float /*rotaryEndAngle*/, + juce::Slider& /*slider*/) override + { + const float size = (float) juce::jmin (w, h); + const float r = size * 0.5f; + const juce::Point c { x + w * 0.5f, y + h * 0.5f }; + const float bezelR = r * 0.98f; + const float faceR = r * 0.78f; + const float capR = r * 0.46f; + + // SW -> SE 270° clockwise sweep (min at bottom-left) + const float start = juce::MathConstants::pi * 0.75f; // 135° + const float sweep = juce::MathConstants::pi * 1.5f; // 270° + const float ang = start + sliderPosProportional * sweep; + + g.setColour (juce::Colour::fromRGB (12,12,12)); + g.fillEllipse (c.x - bezelR, c.y - bezelR, bezelR*2, bezelR*2); + g.setColour (juce::Colours::white.withAlpha (0.06f)); + g.drawEllipse (c.x - bezelR, c.y - bezelR, bezelR*2, bezelR*2, 1.2f); + + juce::ColourGradient faceGrad (juce::Colour::fromRGB (34,34,34), c.x, c.y - faceR, + juce::Colour::fromRGB (22,22,22), c.x, c.y + faceR, false); + g.setGradientFill (faceGrad); + g.fillEllipse (c.x - faceR, c.y - faceR, faceR*2, faceR*2); + + g.setColour (juce::Colours::white.withAlpha (0.25f)); + const int ticks = 9; + for (int i = 0; i < ticks; ++i) + { + const float t = (float) i / (ticks - 1); + const float aa = start + t * sweep; + const float r1 = faceR * 0.92f, r2 = faceR * 1.04f; + g.drawLine (c.x + r1*std::cos(aa), c.y + r1*std::sin(aa), + c.x + r2*std::cos(aa), c.y + r2*std::sin(aa), + (i % 2 == 0) ? 1.5f : 1.0f); + } + + juce::ColourGradient capGrad (juce::Colour::fromRGB (210,210,210), c.x - capR*0.4f, c.y - capR*0.6f, + juce::Colour::fromRGB (160,160,160), c.x + capR*0.6f, c.y + capR*0.6f, true); + g.setGradientFill (capGrad); + g.fillEllipse (c.x - capR, c.y - capR, capR*2, capR*2); + + g.setColour (juce::Colours::white.withAlpha (0.95f)); + juce::Point tip { c.x + faceR*0.9f * std::cos (ang), c.y + faceR*0.9f * std::sin (ang) }; + juce::Point inner { c.x + (capR+2.0f) * std::cos (ang), c.y + (capR+2.0f) * std::sin (ang) }; + g.drawLine (inner.x, inner.y, tip.x, tip.y, juce::jlimit (1.2f, 2.0f, r * 0.06f)); + } +}; + +//============================================================================== + +class TwoOscAudioProcessorEditor : public juce::AudioProcessorEditor +{ +public: + explicit TwoOscAudioProcessorEditor (TwoOscAudioProcessor&); + ~TwoOscAudioProcessorEditor() override; + + void paint (juce::Graphics&) override; + void resized() override; + +private: + TwoOscAudioProcessor& processor; + + // Header + juce::Label title; + + // Preset header: display + button that opens flyout menu + juce::Label presetDisplay; // shows "Category - Name" + juce::TextButton browseBtn { "Browse…" }; + + // Controls + juce::ComboBox oscA, oscB; + juce::Slider mix, detune, cutoff, reso, envAmt; + juce::Slider aA, dA, sA, rA, aF, dF, sF, rF; + juce::Slider masterGain; + + // NEW: LFO controls + juce::Slider lfoRate, lfoDepth; // lfoDepth mapped to lfoToCut (octaves) + + // Captions above knobs + juce::Label lOscA, lOscB; + juce::Label lMix, lDetune, lCutoff, lReso, lEnv; + juce::Label lAA, lDA, lSA, lRA, lAF, lDF, lSF, lRF, lMaster; + juce::Label lLfoRate, lLfoDepth; + + using CA = juce::AudioProcessorValueTreeState::ComboBoxAttachment; + using SA = juce::AudioProcessorValueTreeState::SliderAttachment; + + std::unique_ptr aOscA, aOscB; + std::unique_ptr aMix, aDetune, aCutoff, aReso, aEnv; + std::unique_ptr aAA, aDA, aSA, aRA, aAF, aDF, aSF, aRF; + std::unique_ptr aMaster; + + // NEW: attachments for LFO + std::unique_ptr aLfoRate, aLfoDepth; + + RetroLookAndFeel retroLNF; + + // Helpers + void styleKnob (juce::Slider& s); + void styleLabel (juce::Label& l); + void addLabeled (juce::Component& c, juce::Label& label, const juce::String& text); + void showPresetPopup(); + void updatePresetDisplay(); + + // Uniform sizing / spacing (smaller UI) + static constexpr int knobSize = 60; // was 72 + static constexpr int headerH = 48; // was 56 + static constexpr int captionH = 16; + static constexpr int gap = 10; // was 12 + + // Bounds of the framed "preset information screen" (drawn in paint) + juce::Rectangle presetBarBounds; + + // Background group rectangles (drawn in paint, set in resized) + juce::Rectangle groupCutResBounds; // Cutoff + Reso + juce::Rectangle groupMixDetBounds; // Mix + Detune + juce::Rectangle groupAmpBounds; // Amp A/D/S/R + juce::Rectangle groupLfoBounds; // LFO Rate + Depth + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TwoOscAudioProcessorEditor) +}; + diff --git a/PluginProcessor.cpp b/PluginProcessor.cpp new file mode 100644 index 0000000..75ee30f --- /dev/null +++ b/PluginProcessor.cpp @@ -0,0 +1,1078 @@ +#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(); +} + diff --git a/PluginProcessor.h b/PluginProcessor.h new file mode 100644 index 0000000..24b54e2 --- /dev/null +++ b/PluginProcessor.h @@ -0,0 +1,241 @@ +#pragma once +#include +#include + +//============================================================================== +// Forward declaration +class TwoOscAudioProcessor; + +//============================================================================== +// SimpleSound: a basic sound object (always valid for poly synth) +class SimpleSound : public juce::SynthesiserSound +{ +public: + bool appliesToNote (int) override; + bool appliesToChannel (int) override; +}; + +//============================================================================== +// SimpleVoice: single voice instance for the TwoOsc synth +//============================================================================== + +class SimpleVoice : public juce::SynthesiserVoice +{ +public: + SimpleVoice() = default; + + bool canPlaySound (juce::SynthesiserSound*) override; + void startNote (int midiNoteNumber, float velocity, juce::SynthesiserSound*, int) override; + void stopNote (float velocity, bool allowTailOff) override; + void pitchWheelMoved (int) override {} + void controllerMoved (int, int) override {} + void renderNextBlock (juce::AudioBuffer&, int startSample, int numSamples) override; + void reset(); + + // Pull the latest APVTS values into this voice + void setParameters (juce::AudioProcessorValueTreeState& apvts); + +private: + // ===== Helpers ===== + static inline float wrap01 (float x) { return x - std::floor (x); } + + // PolyBLEP step correction + static inline float polyBLEP (float t, float dt) + { + if (t < dt) + { + float x = t / dt; + return x + x - x * x - 1.0f; // 2x - x^2 - 1 + } + else if (t > 1.0f - dt) + { + float x = (t - 1.0f) / dt; + return x * x + 2.0f * x + 1.0f; // x^2 + 2x + 1 + } + return 0.0f; + } + + // Anti-aliased oscillators using PolyBLEP + DPW tri + inline float oscSaw (float t, float dt) const + { + float y = 2.0f * t - 1.0f; + y -= polyBLEP (t, dt); + return y; + } + + inline float oscSquare (float t, float dt) const + { + float y = (t < 0.5f ? 1.0f : -1.0f); + y += polyBLEP (t, dt); + y -= polyBLEP (wrap01 (t + 0.5f), dt); + return y; + } + + inline float oscTriangle (float t, float dt) + { + // DPW via BL square -> leaky integrator (SR-aware leak computed in startNote) + float sq = (t < 0.5f ? 1.0f : -1.0f); + sq += polyBLEP (t, dt); + sq -= polyBLEP (wrap01 (t + 0.5f), dt); + + // One-pole leaky integrator (avoid denormals) + const float k = 2.0f * dt; // integration step + triState += k * sq - triLeakCoeff * triState; + + if (! std::isfinite (triState)) + triState = 0.0f; + + return juce::jlimit (-1.0f, 1.0f, triState); + } + + inline float oscSine (float t) const + { + return std::sin (juce::MathConstants::twoPi * t); + } + + inline float oscBLEP (int mode, float t, float dt) + { + switch (mode) + { + case 1: return oscSaw (t, dt); + case 2: return oscSquare (t, dt); + case 3: return oscTriangle(t, dt); + default: return oscSine (t); + } + } + + // ===== parameters pushed from APVTS ===== + int oscAChoice = 1, oscBChoice = 2; + float mix = 0.35f; + float detune = -5.0f; // cents + float cutoff = 180.0f; // Hz + float reso = 0.18f; + float envAmt = 0.25f; + bool filterBypass = false; + + // --- LFO (per-voice, sine) --- + float lfoRateHz = 0.0f; // Hz + float lfoToCutOct = 0.0f; // octaves + float lfoToPitchCt = 0.0f; // cents + float lfoPhase = 0.0f; // 0..1 + + // ADSR + juce::ADSR adsrAmp, adsrFilter; + juce::ADSR::Parameters ampParams, filterParams; + + // State-variable filter (per-voice) – set in startNote() + juce::dsp::StateVariableTPTFilter lpFilter; + + // Free-running phases (0..1) + float phaseA = 0.0f, phaseB = 0.0f; + + // Voice state + float currentNoteHz = 440.0f; + float velocityGain = 1.0f; + + // Triangle integrator state & leak coefficient + float triState = 0.0f; + float triLeakCoeff = 0.0f; + + // Start/stop ramps + int rampSamples = 0; + int rampCounter = 0; + bool forcedOffActive = false; + int forcedOffSamples = 0; + int forcedOffCounter = 0; + + // Smoothed params + juce::SmoothedValue detuneSmoothed { 0.0f }; + juce::SmoothedValue cutoffSmoothed { 1200.0f }; + juce::SmoothedValue resoSmoothed { 0.3f }; + juce::SmoothedValue envAmtSmoothed { 0.2f }; + juce::SmoothedValue cutoffModSmooth{ 1000.0f }; + + // LFO smoothed depths + juce::SmoothedValue lfoCutSmoothed { 0.0f }; + juce::SmoothedValue lfoPitchSmoothed { 0.0f }; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SimpleVoice) +}; + +//============================================================================== +// Main processor +//============================================================================== + +class TwoOscAudioProcessor : public juce::AudioProcessor, + public juce::AudioProcessorValueTreeState::Listener +{ +public: + TwoOscAudioProcessor(); + ~TwoOscAudioProcessor() override; + + //========================================================================== + void prepareToPlay (double sampleRate, int samplesPerBlock) override; + void releaseResources() override {} + bool isBusesLayoutSupported (const BusesLayout& layouts) const override; + + void processBlock (juce::AudioBuffer&, juce::MidiBuffer&) override; + + //========================================================================== + juce::AudioProcessorEditor* createEditor() override; + bool hasEditor() const override; + + //========================================================================== + const juce::String getName() const override; + bool acceptsMidi() const override; + bool producesMidi() const override; + bool isMidiEffect() const override; + double getTailLengthSeconds() const override; + + //========================================================================== + int getNumPrograms() override { return 1; } + int getCurrentProgram() override { return 0; } + void setCurrentProgram (int) override {} + const juce::String getProgramName (int) override { return {}; } + void changeProgramName (int, const juce::String&) override {} + + //========================================================================== + void getStateInformation (juce::MemoryBlock& destData) override; + void setStateInformation (const void* data, int sizeInBytes) override; + + //========================================================================== + void parameterChanged (const juce::String& paramID, float newValue) override; + + // Preset helpers + juce::StringArray getPresetCategories() const; + juce::Array getPresetIndicesForCategory (const juce::String& category) const; + juce::String getPresetLabel (int index) const; + juce::String getCurrentPresetLabel() const; + void applyPresetByIndex (int index, bool setParamToo); + + // APVTS + juce::AudioProcessorValueTreeState apvts; + static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout(); + +private: + //========================================================================== + void updateFilter(); + + juce::Synthesiser synth; + double lastSampleRate = 44100.0; + + // Hidden HPF + juce::dsp::ProcessorDuplicator< + juce::dsp::IIR::Filter, + juce::dsp::IIR::Coefficients> hpFilter; + + // Global smoothers + juce::SmoothedValue smoothedCutoff; + juce::SmoothedValue smoothedReso; + juce::SmoothedValue smoothedEnvAmt; + + std::atomic isApplyingPreset { false }; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TwoOscAudioProcessor) +}; + +//============================================================================== +// Factory function +//============================================================================== +juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter(); +