Initial commit
This commit is contained in:
33
AGENTS.md
Normal file
33
AGENTS.md
Normal file
@@ -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.
|
||||||
431
PluginEditor.cpp
Normal file
431
PluginEditor.cpp
Normal file
@@ -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<juce::RangedAudioParameter*> (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<juce::AudioParameterChoice*> (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<float>::pi * 0.75f; // 135°
|
||||||
|
const float sweep = juce::MathConstants<float>::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<int> 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<int> 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<int> cell)
|
||||||
|
{
|
||||||
|
const int labelH = 14;
|
||||||
|
lab.setBounds (cell.removeFromTop (labelH));
|
||||||
|
cell.removeFromTop (2);
|
||||||
|
auto k = juce::Rectangle<int> (knobSize, knobSize)
|
||||||
|
.withCentre ({ cell.getCentreX(), cell.getY() + knobSize / 2 });
|
||||||
|
s.setBounds (k);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto rowCells = [&](juce::Rectangle<int> row, int cols)
|
||||||
|
{
|
||||||
|
juce::Array<juce::Rectangle<int>> 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<juce::AudioParameterChoice*> (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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
139
PluginEditor.h
Normal file
139
PluginEditor.h
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
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<float> 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<float>::pi * 0.75f; // 135°
|
||||||
|
const float sweep = juce::MathConstants<float>::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<float> tip { c.x + faceR*0.9f * std::cos (ang), c.y + faceR*0.9f * std::sin (ang) };
|
||||||
|
juce::Point<float> 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<CA> aOscA, aOscB;
|
||||||
|
std::unique_ptr<SA> aMix, aDetune, aCutoff, aReso, aEnv;
|
||||||
|
std::unique_ptr<SA> aAA, aDA, aSA, aRA, aAF, aDF, aSF, aRF;
|
||||||
|
std::unique_ptr<SA> aMaster;
|
||||||
|
|
||||||
|
// NEW: attachments for LFO
|
||||||
|
std::unique_ptr<SA> 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<int> presetBarBounds;
|
||||||
|
|
||||||
|
// Background group rectangles (drawn in paint, set in resized)
|
||||||
|
juce::Rectangle<int> groupCutResBounds; // Cutoff + Reso
|
||||||
|
juce::Rectangle<int> groupMixDetBounds; // Mix + Detune
|
||||||
|
juce::Rectangle<int> groupAmpBounds; // Amp A/D/S/R
|
||||||
|
juce::Rectangle<int> groupLfoBounds; // LFO Rate + Depth
|
||||||
|
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TwoOscAudioProcessorEditor)
|
||||||
|
};
|
||||||
|
|
||||||
1078
PluginProcessor.cpp
Normal file
1078
PluginProcessor.cpp
Normal file
File diff suppressed because it is too large
Load Diff
241
PluginProcessor.h
Normal file
241
PluginProcessor.h
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
// 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<float>&, 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<float>::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<float> 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<float> detuneSmoothed { 0.0f };
|
||||||
|
juce::SmoothedValue<float> cutoffSmoothed { 1200.0f };
|
||||||
|
juce::SmoothedValue<float> resoSmoothed { 0.3f };
|
||||||
|
juce::SmoothedValue<float> envAmtSmoothed { 0.2f };
|
||||||
|
juce::SmoothedValue<float> cutoffModSmooth{ 1000.0f };
|
||||||
|
|
||||||
|
// LFO smoothed depths
|
||||||
|
juce::SmoothedValue<float> lfoCutSmoothed { 0.0f };
|
||||||
|
juce::SmoothedValue<float> 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<float>&, 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<int> 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<float>,
|
||||||
|
juce::dsp::IIR::Coefficients<float>> hpFilter;
|
||||||
|
|
||||||
|
// Global smoothers
|
||||||
|
juce::SmoothedValue<float> smoothedCutoff;
|
||||||
|
juce::SmoothedValue<float> smoothedReso;
|
||||||
|
juce::SmoothedValue<float> smoothedEnvAmt;
|
||||||
|
|
||||||
|
std::atomic<bool> isApplyingPreset { false };
|
||||||
|
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TwoOscAudioProcessor)
|
||||||
|
};
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
// Factory function
|
||||||
|
//==============================================================================
|
||||||
|
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter();
|
||||||
|
|
||||||
Reference in New Issue
Block a user