feat(plugin): add plugin processor and editor
This commit is contained in:
@@ -0,0 +1,711 @@
|
||||
#include "PluginEditor.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
juce::StringArray wavetableItems()
|
||||
{
|
||||
juce::StringArray a;
|
||||
for (auto n : kWavetableNames) a.add (n);
|
||||
return a;
|
||||
}
|
||||
|
||||
juce::StringArray modSourceItems()
|
||||
{
|
||||
juce::StringArray a;
|
||||
for (int i = 0; i < kNumModSources; ++i)
|
||||
a.add (modSourceName ((ModSource) i));
|
||||
return a;
|
||||
}
|
||||
|
||||
juce::StringArray modTargetItems (std::vector<ModTarget>& enums)
|
||||
{
|
||||
juce::StringArray a;
|
||||
for (int i = 0; i < kNumModTargets; ++i)
|
||||
{
|
||||
const auto t = (ModTarget) i;
|
||||
if (t == ModTarget::None) continue;
|
||||
a.add (modTargetName (t));
|
||||
enums.push_back (t);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
juce::String fmtUnison (float v) { return juce::String (1 + (int) std::llround (v * 15.0f)); }
|
||||
juce::String fmtSlope (float v) { const int s = (int) std::llround (v * 2.0f); return juce::String (s == 0 ? 6 : (s == 1 ? 12 : 24)) + " dB"; }
|
||||
juce::String fmtDepth (float v) { return juce::String (v, 2); }
|
||||
|
||||
void placeKnobs (std::vector<Knob*>& knobs, int x, int y, int w, int h, int gap = 6)
|
||||
{
|
||||
for (int i = 0; i < (int) knobs.size(); ++i)
|
||||
knobs[(size_t) i]->setBounds (x + i * (w + gap), y, w, h);
|
||||
}
|
||||
|
||||
const char* kFxType[8] = { ids::fx1Type, ids::fx2Type, ids::fx3Type, ids::fx4Type,
|
||||
ids::fx5Type, ids::fx6Type, ids::fx7Type, ids::fx8Type };
|
||||
const char* kFxMix[8] = { ids::fx1Mix, ids::fx2Mix, ids::fx3Mix, ids::fx4Mix,
|
||||
ids::fx5Mix, ids::fx6Mix, ids::fx7Mix, ids::fx8Mix };
|
||||
const char* kFxP1[8] = { ids::fx1P1, ids::fx2P1, ids::fx3P1, ids::fx4P1,
|
||||
ids::fx5P1, ids::fx6P1, ids::fx7P1, ids::fx8P1 };
|
||||
const char* kFxP2[8] = { ids::fx1P2, ids::fx2P2, ids::fx3P2, ids::fx4P2,
|
||||
ids::fx5P2, ids::fx6P2, ids::fx7P2, ids::fx8P2 };
|
||||
const char* kFxP3[8] = { ids::fx1P3, ids::fx2P3, ids::fx3P3, ids::fx4P3,
|
||||
ids::fx5P3, ids::fx6P3, ids::fx7P3, ids::fx8P3 };
|
||||
const char* kFxP4[8] = { ids::fx1P4, ids::fx2P4, ids::fx3P4, ids::fx4P4,
|
||||
ids::fx5P4, ids::fx6P4, ids::fx7P4, ids::fx8P4 };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
PluginEditor::PluginEditor (SerumAltAudioProcessor& p)
|
||||
: AudioProcessorEditor (p), processor (p),
|
||||
masterKnob ("Master", formatPercent)
|
||||
{
|
||||
root.setBounds (0, 0, kBaseW, kBaseH);
|
||||
addAndMakeVisible (root);
|
||||
|
||||
logo = createLogoDrawable();
|
||||
|
||||
buildTopBar();
|
||||
buildOscTab();
|
||||
buildFilterTab();
|
||||
buildModTab();
|
||||
buildFxTab();
|
||||
buildMacroTab();
|
||||
|
||||
setTab (0);
|
||||
applyUiScale();
|
||||
startTimerHz (30);
|
||||
}
|
||||
|
||||
PluginEditor::~PluginEditor()
|
||||
{
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
void PluginEditor::paint (juce::Graphics& g)
|
||||
{
|
||||
g.fillAll (theme::bg);
|
||||
g.setGradientFill (juce::ColourGradient (theme::panel, 0.0f, 0.0f,
|
||||
theme::bg, 0.0f, (float) getHeight(), false));
|
||||
g.fillAll();
|
||||
|
||||
if (logo != nullptr)
|
||||
logo->drawWithin (g, juce::Rectangle<float> (10.0f, 8.0f, 180.0f, 44.0f),
|
||||
juce::RectanglePlacement::centred, 1.0f);
|
||||
}
|
||||
|
||||
void PluginEditor::resized()
|
||||
{
|
||||
root.setBounds (0, 0, kBaseW, kBaseH);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
Knob* PluginEditor::makeKnob (juce::Component* parent, const juce::String& name,
|
||||
const juce::String& paramId, std::function<juce::String (float)> fmt)
|
||||
{
|
||||
auto* k = new Knob (name, std::move (fmt));
|
||||
parent->addAndMakeVisible (k);
|
||||
if (paramId.isNotEmpty())
|
||||
sliderAttachments.push_back (std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (
|
||||
processor.parameters, paramId, *k));
|
||||
return k;
|
||||
}
|
||||
|
||||
juce::ComboBox* PluginEditor::makeCombo (juce::Component* parent, const juce::String& paramId,
|
||||
const juce::StringArray& items)
|
||||
{
|
||||
auto* c = new juce::ComboBox();
|
||||
c->addItemList (items, 1);
|
||||
c->setSelectedItemIndex (0, juce::dontSendNotification);
|
||||
parent->addAndMakeVisible (c);
|
||||
if (paramId.isNotEmpty())
|
||||
comboAttachments.push_back (std::make_unique<juce::AudioProcessorValueTreeState::ComboBoxAttachment> (
|
||||
processor.parameters, paramId, *c));
|
||||
return c;
|
||||
}
|
||||
|
||||
ToggleButton* PluginEditor::makeToggle (juce::Component* parent, const juce::String& label,
|
||||
const juce::String& paramId)
|
||||
{
|
||||
auto* t = new ToggleButton (label);
|
||||
parent->addAndMakeVisible (t);
|
||||
if (paramId.isNotEmpty())
|
||||
t->attach (processor.parameters, paramId);
|
||||
return t;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
void PluginEditor::buildTopBar()
|
||||
{
|
||||
// Preset selector.
|
||||
juce::StringArray presetNames;
|
||||
for (int i = 0; i < processor.getNumPrograms(); ++i)
|
||||
presetNames.add (processor.getProgramName (i));
|
||||
presetCombo.addItemList (presetNames, 1);
|
||||
presetCombo.setSelectedItemIndex (processor.getCurrentProgram(), juce::dontSendNotification);
|
||||
presetCombo.onChange = [this] { processor.setCurrentProgram (presetCombo.getSelectedItemIndex()); };
|
||||
root.addAndMakeVisible (presetCombo);
|
||||
presetCombo.setBounds (200, 16, 220, 24);
|
||||
|
||||
// UI scale.
|
||||
scaleCombo.addItemList ({ "75%", "100%", "125%", "150%", "200%" }, 1);
|
||||
scaleCombo.setSelectedItemIndex (processor.getUiScaleIndex(), juce::dontSendNotification);
|
||||
scaleCombo.onChange = [this] { processor.setUiScaleIndex (scaleCombo.getSelectedItemIndex()); };
|
||||
root.addAndMakeVisible (scaleCombo);
|
||||
scaleCombo.setBounds (980, 16, 70, 24);
|
||||
|
||||
// RAVE.
|
||||
raveButton.setLabel ("RAVE");
|
||||
raveButton.setOnColour (theme::raveGlow);
|
||||
raveButton.setBounds (1062, 2, 50, 56);
|
||||
root.addAndMakeVisible (raveButton);
|
||||
// RAVE uses a callback rather than a direct parameter attachment.
|
||||
raveButton.setOnClick ([this] (bool on) { processor.setRaveEnabled (on); });
|
||||
|
||||
// Tab bar.
|
||||
juce::TextButton* tabs[5] = { &tabOsc, &tabFilter, &tabMod, &tabFx, &tabMacro };
|
||||
const char* tabNames[5] = { "OSC", "FILTER", "MOD", "FX", "MACRO" };
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
tabs[i]->setButtonText (tabNames[i]);
|
||||
tabs[i]->setClickingTogglesState (true);
|
||||
tabs[i]->setRadioGroupId (1001);
|
||||
tabs[i]->setBounds (10 + i * 92, 36, 84, 22);
|
||||
tabs[i]->onClick = [this, i] { setTab (i); };
|
||||
root.addAndMakeVisible (tabs[i]);
|
||||
}
|
||||
|
||||
// Master.
|
||||
masterKnob.setBounds (906, 0, 64, 60);
|
||||
root.addAndMakeVisible (masterKnob);
|
||||
sliderAttachments.push_back (std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (
|
||||
processor.parameters, ids::master, masterKnob));
|
||||
|
||||
masterDisplay.setTitle ("MASTER");
|
||||
root.addAndMakeVisible (masterDisplay);
|
||||
masterDisplay.setBounds (846, 8, 56, 44);
|
||||
}
|
||||
|
||||
void PluginEditor::buildOscTab()
|
||||
{
|
||||
oscView.setBounds (0, 60, kBaseW, kBaseH - 60);
|
||||
root.addChildComponent (oscView);
|
||||
|
||||
oscAPanel.setBounds (10, 8, 545, 300);
|
||||
oscBPanel.setBounds (565, 8, 545, 300);
|
||||
subPanel.setBounds (10, 318, 360, 150);
|
||||
noisePanel.setBounds (380, 318, 360, 150);
|
||||
for (auto* panel : { &oscAPanel, &oscBPanel, &subPanel, &noisePanel })
|
||||
oscView.addAndMakeVisible (panel);
|
||||
|
||||
// --- Oscillator A ---
|
||||
makeToggle (&oscAPanel, "On", ids::oscAOn)->setBounds (10, 8, 40, 28);
|
||||
makeCombo (&oscAPanel, ids::oscAWave, wavetableItems())->setBounds (56, 10, 200, 24);
|
||||
makeCombo (&oscAPanel, ids::oscAWarp, { "None", "Bend+", "Bend-", "Sync", "PWM", "Asym", "Mirror", "Fold" })->setBounds (262, 10, 130, 24);
|
||||
|
||||
oscAWave.setWavetables (&processor.engine.getWavetables());
|
||||
oscAPanel.addAndMakeVisible (oscAWave);
|
||||
oscAWave.setBounds (10, 44, 525, 66);
|
||||
|
||||
std::vector<Knob*> rowA1 = {
|
||||
makeKnob (&oscAPanel, "WT Pos", ids::oscAWtPos, formatPercent),
|
||||
makeKnob (&oscAPanel, "Warp Amt", ids::oscAWarpAmt, formatPercent),
|
||||
makeKnob (&oscAPanel, "Coarse", ids::oscACoarse, formatSemis),
|
||||
makeKnob (&oscAPanel, "Fine", ids::oscAFine, formatCents),
|
||||
makeKnob (&oscAPanel, "Level", ids::oscALevel, formatPercent),
|
||||
makeKnob (&oscAPanel, "Pan", ids::oscAPan, formatPan)
|
||||
};
|
||||
placeKnobs (rowA1, 10, 118, 76, 76, 6);
|
||||
|
||||
std::vector<Knob*> rowA2 = {
|
||||
makeKnob (&oscAPanel, "Unison", ids::oscAUnison, fmtUnison),
|
||||
makeKnob (&oscAPanel, "Detune", ids::oscADetune, formatPercent),
|
||||
makeKnob (&oscAPanel, "Spread", ids::oscASpread, formatPercent),
|
||||
makeKnob (&oscAPanel, "Phase", ids::oscAPhase, formatPercent),
|
||||
makeKnob (&oscAPanel, "Rand Ph", ids::oscARandPh, formatPercent)
|
||||
};
|
||||
placeKnobs (rowA2, 10, 198, 76, 76, 6);
|
||||
|
||||
// --- Oscillator B ---
|
||||
makeToggle (&oscBPanel, "On", ids::oscBOn)->setBounds (10, 8, 40, 28);
|
||||
makeCombo (&oscBPanel, ids::oscBWave, wavetableItems())->setBounds (56, 10, 200, 24);
|
||||
makeCombo (&oscBPanel, ids::oscBWarp, { "None", "Bend+", "Bend-", "Sync", "PWM", "Asym", "Mirror", "Fold" })->setBounds (262, 10, 130, 24);
|
||||
|
||||
oscBWave.setWavetables (&processor.engine.getWavetables());
|
||||
oscBPanel.addAndMakeVisible (oscBWave);
|
||||
oscBWave.setBounds (10, 44, 525, 66);
|
||||
|
||||
std::vector<Knob*> rowB1 = {
|
||||
makeKnob (&oscBPanel, "WT Pos", ids::oscBWtPos, formatPercent),
|
||||
makeKnob (&oscBPanel, "Warp Amt", ids::oscBWarpAmt, formatPercent),
|
||||
makeKnob (&oscBPanel, "Coarse", ids::oscBCoarse, formatSemis),
|
||||
makeKnob (&oscBPanel, "Fine", ids::oscBFine, formatCents),
|
||||
makeKnob (&oscBPanel, "Level", ids::oscBLevel, formatPercent),
|
||||
makeKnob (&oscBPanel, "Pan", ids::oscBPan, formatPan)
|
||||
};
|
||||
placeKnobs (rowB1, 10, 118, 76, 76, 6);
|
||||
|
||||
std::vector<Knob*> rowB2 = {
|
||||
makeKnob (&oscBPanel, "Unison", ids::oscBUnison, fmtUnison),
|
||||
makeKnob (&oscBPanel, "Detune", ids::oscBDetune, formatPercent),
|
||||
makeKnob (&oscBPanel, "Spread", ids::oscBSpread, formatPercent),
|
||||
makeKnob (&oscBPanel, "Phase", ids::oscBPhase, formatPercent),
|
||||
makeKnob (&oscBPanel, "Rand Ph", ids::oscBRandPh, formatPercent)
|
||||
};
|
||||
placeKnobs (rowB2, 10, 198, 76, 76, 6);
|
||||
|
||||
// --- Sub ---
|
||||
makeToggle (&subPanel, "On", ids::subOn)->setBounds (10, 10, 40, 28);
|
||||
makeCombo (&subPanel, ids::subShape, { "Sine", "Triangle" })->setBounds (56, 12, 130, 24);
|
||||
makeCombo (&subPanel, ids::subOct, { "-2 oct", "-1 oct", "0 oct" })->setBounds (192, 12, 90, 24);
|
||||
makeKnob (&subPanel, "Level", ids::subLevel, formatPercent)->setBounds (10, 46, 76, 76);
|
||||
|
||||
// --- Noise ---
|
||||
makeToggle (&noisePanel, "On", ids::noiseOn)->setBounds (10, 10, 40, 28);
|
||||
makeCombo (&noisePanel, ids::noiseType, { "White", "Pink" })->setBounds (56, 12, 130, 24);
|
||||
makeKnob (&noisePanel, "Level", ids::noiseLevel, formatPercent)->setBounds (10, 46, 76, 76);
|
||||
}
|
||||
|
||||
void PluginEditor::buildFilterTab()
|
||||
{
|
||||
filterView.setBounds (0, 60, kBaseW, kBaseH - 60);
|
||||
root.addChildComponent (filterView);
|
||||
|
||||
filter1Panel.setBounds (10, 8, 360, 300);
|
||||
filter2Panel.setBounds (380, 8, 360, 300);
|
||||
routingPanel.setBounds (750, 8, 360, 300);
|
||||
for (auto* panel : { &filter1Panel, &filter2Panel, &routingPanel })
|
||||
filterView.addAndMakeVisible (panel);
|
||||
|
||||
const juce::StringArray filterTypes = { "Ladder LP", "Ladder HP", "Ladder BP", "Diode", "Comb", "Formant", "Screamer" };
|
||||
const juce::StringArray slopes = { "6 dB", "12 dB", "24 dB" };
|
||||
|
||||
// Filter 1
|
||||
makeToggle (&filter1Panel, "On", ids::f1On)->setBounds (10, 8, 40, 28);
|
||||
makeCombo (&filter1Panel, ids::f1Type, filterTypes)->setBounds (56, 10, 180, 24);
|
||||
makeCombo (&filter1Panel, ids::f1Slope, slopes)->setBounds (242, 10, 100, 24);
|
||||
filter1Panel.addAndMakeVisible (filter1Display);
|
||||
filter1Display.setBounds (10, 44, 340, 70);
|
||||
|
||||
std::vector<Knob*> f1 = {
|
||||
makeKnob (&filter1Panel, "Cutoff", ids::f1Cutoff, formatHz),
|
||||
makeKnob (&filter1Panel, "Res", ids::f1Res, formatPercent),
|
||||
makeKnob (&filter1Panel, "Drive", ids::f1Drive, formatPercent),
|
||||
makeKnob (&filter1Panel, "Keytrack", ids::f1Key, formatPercent)
|
||||
};
|
||||
placeKnobs (f1, 10, 124, 76, 76, 6);
|
||||
|
||||
// Filter 2
|
||||
makeToggle (&filter2Panel, "On", ids::f2On)->setBounds (10, 8, 40, 28);
|
||||
makeCombo (&filter2Panel, ids::f2Type, filterTypes)->setBounds (56, 10, 180, 24);
|
||||
makeCombo (&filter2Panel, ids::f2Slope, slopes)->setBounds (242, 10, 100, 24);
|
||||
filter2Panel.addAndMakeVisible (filter2Display);
|
||||
filter2Display.setBounds (10, 44, 340, 70);
|
||||
|
||||
std::vector<Knob*> f2 = {
|
||||
makeKnob (&filter2Panel, "Cutoff", ids::f2Cutoff, formatHz),
|
||||
makeKnob (&filter2Panel, "Res", ids::f2Res, formatPercent),
|
||||
makeKnob (&filter2Panel, "Drive", ids::f2Drive, formatPercent),
|
||||
makeKnob (&filter2Panel, "Keytrack", ids::f2Key, formatPercent)
|
||||
};
|
||||
placeKnobs (f2, 10, 124, 76, 76, 6);
|
||||
|
||||
// Routing
|
||||
makeCombo (&routingPanel, ids::fRoute, { "Serial", "Parallel", "Split" })->setBounds (10, 10, 150, 24);
|
||||
makeKnob (&routingPanel, "Mix", ids::fMix, formatPercent)->setBounds (10, 50, 76, 76);
|
||||
makeKnob (&routingPanel, "Output", ids::fOut, formatPercent)->setBounds (100, 50, 76, 76);
|
||||
}
|
||||
|
||||
void PluginEditor::buildModTab()
|
||||
{
|
||||
modView.setBounds (0, 60, kBaseW, kBaseH - 60);
|
||||
root.addChildComponent (modView);
|
||||
|
||||
const juce::StringArray lfoShapes = { "Sine", "Triangle", "Saw", "Square", "S&H", "Step", "Freehand" };
|
||||
|
||||
// Envelopes (4 across).
|
||||
for (int i = 0; i < kNumEnvelopes; ++i)
|
||||
{
|
||||
const int x = 10 + i * 275;
|
||||
envPanels[(size_t) i].setBounds (x, 8, 265, 150);
|
||||
modView.addAndMakeVisible (envPanels[(size_t) i]);
|
||||
|
||||
envPanels[(size_t) i].addAndMakeVisible (envDisplays[(size_t) i]);
|
||||
envDisplays[(size_t) i].setBounds (8, 8, 249, 56);
|
||||
|
||||
const char* idsA[4] = { ids::env1A, ids::env2A, ids::env3A, ids::env4A };
|
||||
const char* idsD[4] = { ids::env1D, ids::env2D, ids::env3D, ids::env4D };
|
||||
const char* idsS[4] = { ids::env1S, ids::env2S, ids::env3S, ids::env4S };
|
||||
const char* idsR[4] = { ids::env1R, ids::env2R, ids::env3R, ids::env4R };
|
||||
const char* idsC[4] = { ids::env1Curve, ids::env2Curve, ids::env3Curve, ids::env4Curve };
|
||||
|
||||
std::vector<Knob*> knobs = {
|
||||
makeKnob (&envPanels[(size_t) i], "A", idsA[i], formatSeconds),
|
||||
makeKnob (&envPanels[(size_t) i], "D", idsD[i], formatSeconds),
|
||||
makeKnob (&envPanels[(size_t) i], "S", idsS[i], formatPercent),
|
||||
makeKnob (&envPanels[(size_t) i], "R", idsR[i], formatSeconds),
|
||||
makeKnob (&envPanels[(size_t) i], "Curve", idsC[i], formatPercent)
|
||||
};
|
||||
placeKnobs (knobs, 8, 72, 44, 60, 3);
|
||||
}
|
||||
|
||||
// LFOs (4 across).
|
||||
for (int i = 0; i < kNumLfos; ++i)
|
||||
{
|
||||
const int x = 10 + i * 275;
|
||||
lfoPanels[(size_t) i].setBounds (x, 168, 265, 180);
|
||||
modView.addAndMakeVisible (lfoPanels[(size_t) i]);
|
||||
|
||||
const char* rate[4] = { ids::lfo1Rate, ids::lfo2Rate, ids::lfo3Rate, ids::lfo4Rate };
|
||||
const char* sync[4] = { ids::lfo1Sync, ids::lfo2Sync, ids::lfo3Sync, ids::lfo4Sync };
|
||||
const char* beat[4] = { ids::lfo1Beat, ids::lfo2Beat, ids::lfo3Beat, ids::lfo4Beat };
|
||||
const char* shape[4] = { ids::lfo1Shape, ids::lfo2Shape, ids::lfo3Shape, ids::lfo4Shape };
|
||||
const char* phase[4] = { ids::lfo1Phase, ids::lfo2Phase, ids::lfo3Phase, ids::lfo4Phase };
|
||||
const char* fade[4] = { ids::lfo1Fade, ids::lfo2Fade, ids::lfo3Fade, ids::lfo4Fade };
|
||||
const char* delay[4] = { ids::lfo1Delay, ids::lfo2Delay, ids::lfo3Delay, ids::lfo4Delay };
|
||||
|
||||
makeCombo (&lfoPanels[(size_t) i], shape[i], lfoShapes)->setBounds (8, 8, 110, 22);
|
||||
makeToggle (&lfoPanels[(size_t) i], "Sync", sync[i])->setBounds (126, 4, 40, 28);
|
||||
|
||||
lfoPanels[(size_t) i].addAndMakeVisible (lfoDisplays[(size_t) i]);
|
||||
lfoDisplays[(size_t) i].setBounds (8, 38, 249, 64);
|
||||
|
||||
std::vector<Knob*> knobs = {
|
||||
makeKnob (&lfoPanels[(size_t) i], "Rate", rate[i], formatPercent),
|
||||
makeKnob (&lfoPanels[(size_t) i], "Beat", beat[i], formatPercent),
|
||||
makeKnob (&lfoPanels[(size_t) i], "Phase", phase[i], formatPercent),
|
||||
makeKnob (&lfoPanels[(size_t) i], "Fade", fade[i], formatPercent),
|
||||
makeKnob (&lfoPanels[(size_t) i], "Delay", delay[i], formatPercent)
|
||||
};
|
||||
placeKnobs (knobs, 8, 106, 44, 60, 3);
|
||||
|
||||
lfoDisplays[(size_t) i].setOnShapeEdited ([this, i] (const std::vector<float>& data, int steps)
|
||||
{
|
||||
processor.engine.setLfoShapeData (i, data, steps);
|
||||
});
|
||||
}
|
||||
|
||||
// Modulation matrix.
|
||||
matrixPanel.setBounds (10, 356, 1100, 268);
|
||||
modView.addAndMakeVisible (matrixPanel);
|
||||
|
||||
std::vector<ModTarget> targetEnums;
|
||||
const juce::StringArray targetItems = modTargetItems (targetEnums);
|
||||
|
||||
modSourceCombo.addItemList (modSourceItems(), 1);
|
||||
modSourceCombo.setSelectedItemIndex (0, juce::dontSendNotification);
|
||||
modSourceCombo.setBounds (10, 22, 170, 24);
|
||||
matrixPanel.addAndMakeVisible (modSourceCombo);
|
||||
|
||||
modTargetCombo.addItemList (targetItems, 1);
|
||||
modTargetCombo.setSelectedItemIndex (0, juce::dontSendNotification);
|
||||
modTargetCombo.setBounds (190, 22, 200, 24);
|
||||
matrixPanel.addAndMakeVisible (modTargetCombo);
|
||||
|
||||
modDepthKnob.setRange (-1.0, 1.0);
|
||||
modDepthKnob.setValue (0.5, juce::dontSendNotification);
|
||||
modDepthKnob.setFormatter (fmtDepth);
|
||||
modDepthKnob.setBounds (400, 4, 76, 60);
|
||||
matrixPanel.addAndMakeVisible (modDepthKnob);
|
||||
|
||||
modBipolarToggle.setBounds (486, 10, 56, 48);
|
||||
matrixPanel.addAndMakeVisible (modBipolarToggle);
|
||||
modBipolarToggle.setToggleState (true);
|
||||
|
||||
modAddButton.setButtonText ("Add");
|
||||
modAddButton.setBounds (550, 22, 70, 26);
|
||||
modAddButton.onClick = [this, targetEnums]
|
||||
{
|
||||
const ModSource src = (ModSource) modSourceCombo.getSelectedItemIndex();
|
||||
const int tid = modTargetCombo.getSelectedItemIndex();
|
||||
if (tid >= 0 && tid < (int) targetEnums.size())
|
||||
{
|
||||
processor.engine.getMatrix().addConnection (src, targetEnums[(size_t) tid],
|
||||
(float) modDepthKnob.getValue(),
|
||||
modBipolarToggle.getToggleState());
|
||||
updateModList();
|
||||
}
|
||||
};
|
||||
matrixPanel.addAndMakeVisible (modAddButton);
|
||||
|
||||
modRemoveButton.setButtonText ("Remove Last");
|
||||
modRemoveButton.setBounds (626, 22, 110, 26);
|
||||
modRemoveButton.onClick = [this]
|
||||
{
|
||||
processor.engine.getMatrix().removeConnection (processor.engine.getMatrix().size() - 1);
|
||||
updateModList();
|
||||
};
|
||||
matrixPanel.addAndMakeVisible (modRemoveButton);
|
||||
|
||||
modClearButton.setButtonText ("Clear");
|
||||
modClearButton.setBounds (742, 22, 70, 26);
|
||||
modClearButton.onClick = [this]
|
||||
{
|
||||
processor.engine.getMatrix().clear();
|
||||
updateModList();
|
||||
};
|
||||
matrixPanel.addAndMakeVisible (modClearButton);
|
||||
|
||||
modList.setBounds (10, 56, 1080, 200);
|
||||
modList.setReadOnly (true);
|
||||
modList.setMultiLine (true, false);
|
||||
modList.setColour (juce::TextEditor::backgroundColourId, juce::Colours::transparentBlack);
|
||||
modList.setColour (juce::TextEditor::textColourId, theme::text);
|
||||
modList.setColour (juce::TextEditor::outlineColourId, juce::Colours::transparentBlack);
|
||||
matrixPanel.addAndMakeVisible (modList);
|
||||
}
|
||||
|
||||
void PluginEditor::buildFxTab()
|
||||
{
|
||||
fxView.setBounds (0, 60, kBaseW, kBaseH - 60);
|
||||
root.addChildComponent (fxView);
|
||||
|
||||
const juce::StringArray fxTypes = { "Off", "Hyper", "Chorus", "Flanger", "Phaser",
|
||||
"Distortion", "EQ", "Compressor", "Delay", "Reverb" };
|
||||
|
||||
for (int i = 0; i < kNumFxSlots; ++i)
|
||||
{
|
||||
const int col = i % 2;
|
||||
const int row = i / 2;
|
||||
const int x = 10 + col * 555;
|
||||
const int y = 8 + row * 156;
|
||||
|
||||
fxPanels[(size_t) i].setBounds (x, y, 545, 148);
|
||||
fxView.addAndMakeVisible (fxPanels[(size_t) i]);
|
||||
|
||||
fxTypeCombos[(size_t) i] = makeCombo (&fxPanels[(size_t) i], kFxType[i], fxTypes);
|
||||
fxTypeCombos[(size_t) i]->setBounds (8, 8, 160, 24);
|
||||
|
||||
fxUp[(size_t) i] = new juce::TextButton ("\xe2\x86\x91");
|
||||
fxUp[(size_t) i]->setBounds (176, 8, 26, 24);
|
||||
fxPanels[(size_t) i].addAndMakeVisible (fxUp[(size_t) i]);
|
||||
fxDown[(size_t) i] = new juce::TextButton ("\xe2\x86\x93");
|
||||
fxDown[(size_t) i]->setBounds (204, 8, 26, 24);
|
||||
fxPanels[(size_t) i].addAndMakeVisible (fxDown[(size_t) i]);
|
||||
|
||||
fxUp[(size_t) i]->onClick = [this, i] { if (i > 0) swapFxSlots (i, i - 1); };
|
||||
fxDown[(size_t) i]->onClick = [this, i] { if (i < kNumFxSlots - 1) swapFxSlots (i, i + 1); };
|
||||
|
||||
std::vector<Knob*> knobs = {
|
||||
makeKnob (&fxPanels[(size_t) i], "Mix", kFxMix[i], formatPercent),
|
||||
makeKnob (&fxPanels[(size_t) i], "P1", kFxP1[i], formatPercent),
|
||||
makeKnob (&fxPanels[(size_t) i], "P2", kFxP2[i], formatPercent),
|
||||
makeKnob (&fxPanels[(size_t) i], "P3", kFxP3[i], formatPercent),
|
||||
makeKnob (&fxPanels[(size_t) i], "P4", kFxP4[i], formatPercent)
|
||||
};
|
||||
placeKnobs (knobs, 8, 40, 76, 76, 6);
|
||||
fxKnobs[(size_t) i] = knobs;
|
||||
}
|
||||
}
|
||||
|
||||
void PluginEditor::buildMacroTab()
|
||||
{
|
||||
macroView.setBounds (0, 60, kBaseW, kBaseH - 60);
|
||||
root.addChildComponent (macroView);
|
||||
|
||||
for (int i = 0; i < kNumMacros; ++i)
|
||||
{
|
||||
const int x = 10 + i * 275;
|
||||
macroPanels[(size_t) i].setBounds (x, 8, 265, 240);
|
||||
macroView.addAndMakeVisible (macroPanels[(size_t) i]);
|
||||
|
||||
const char* ids[4] = { ids::macro1, ids::macro2, ids::macro3, ids::macro4 };
|
||||
macroKnobs[(size_t) i] = makeKnob (¯oPanels[(size_t) i], MacroControls::macroName (i), ids[i], formatPercent);
|
||||
macroKnobs[(size_t) i]->setBounds (70, 20, 120, 120);
|
||||
}
|
||||
|
||||
// Macro assignment editor.
|
||||
macroAssignPanel.setBounds (10, 256, 1100, 380);
|
||||
macroView.addAndMakeVisible (macroAssignPanel);
|
||||
|
||||
std::vector<ModTarget> targetEnums;
|
||||
const juce::StringArray targetItems = modTargetItems (targetEnums);
|
||||
|
||||
macroAssignIndex.addItemList ({ "Macro 1", "Macro 2", "Macro 3", "Macro 4" }, 1);
|
||||
macroAssignIndex.setSelectedItemIndex (0, juce::dontSendNotification);
|
||||
macroAssignIndex.setBounds (10, 24, 140, 24);
|
||||
macroAssignPanel.addAndMakeVisible (macroAssignIndex);
|
||||
|
||||
macroAssignTarget.addItemList (targetItems, 1);
|
||||
macroAssignTarget.setSelectedItemIndex (0, juce::dontSendNotification);
|
||||
macroAssignTarget.setBounds (160, 24, 220, 24);
|
||||
macroAssignPanel.addAndMakeVisible (macroAssignTarget);
|
||||
|
||||
macroDepthKnob.setFormatter (fmtDepth);
|
||||
macroDepthKnob.setBounds (390, 6, 76, 60);
|
||||
macroAssignPanel.addAndMakeVisible (macroDepthKnob);
|
||||
|
||||
macroAssignButton.setButtonText ("Assign");
|
||||
macroAssignButton.setBounds (476, 24, 80, 26);
|
||||
macroAssignButton.onClick = [this, targetEnums]
|
||||
{
|
||||
const int macro = macroAssignIndex.getSelectedItemIndex();
|
||||
const int tid = macroAssignTarget.getSelectedItemIndex();
|
||||
if (tid >= 0 && tid < (int) targetEnums.size())
|
||||
{
|
||||
processor.engine.getMacros().addAssignment (macro, targetEnums[(size_t) tid],
|
||||
(float) macroDepthKnob.getValue());
|
||||
updateMacroList();
|
||||
}
|
||||
};
|
||||
macroAssignPanel.addAndMakeVisible (macroAssignButton);
|
||||
|
||||
macroClearButton.setButtonText ("Clear All");
|
||||
macroClearButton.setBounds (562, 24, 80, 26);
|
||||
macroClearButton.onClick = [this]
|
||||
{
|
||||
processor.engine.getMacros().clear();
|
||||
updateMacroList();
|
||||
};
|
||||
macroAssignPanel.addAndMakeVisible (macroClearButton);
|
||||
|
||||
macroList.setBounds (10, 58, 1080, 310);
|
||||
macroList.setReadOnly (true);
|
||||
macroList.setMultiLine (true, false);
|
||||
macroList.setColour (juce::TextEditor::backgroundColourId, juce::Colours::transparentBlack);
|
||||
macroList.setColour (juce::TextEditor::textColourId, theme::text);
|
||||
macroList.setColour (juce::TextEditor::outlineColourId, juce::Colours::transparentBlack);
|
||||
macroAssignPanel.addAndMakeVisible (macroList);
|
||||
}
|
||||
|
||||
void PluginEditor::swapFxSlots (int a, int b)
|
||||
{
|
||||
auto swapParam = [this] (const char* pa, const char* pb)
|
||||
{
|
||||
auto* p1 = processor.parameters.getParameter (pa);
|
||||
auto* p2 = processor.parameters.getParameter (pb);
|
||||
const float v1 = p1->getValue();
|
||||
const float v2 = p2->getValue();
|
||||
p1->setValueNotifyingHost (v2);
|
||||
p2->setValueNotifyingHost (v1);
|
||||
};
|
||||
|
||||
swapParam (kFxType[a], kFxType[b]);
|
||||
swapParam (kFxMix[a], kFxMix[b]);
|
||||
swapParam (kFxP1[a], kFxP1[b]);
|
||||
swapParam (kFxP2[a], kFxP2[b]);
|
||||
swapParam (kFxP3[a], kFxP3[b]);
|
||||
swapParam (kFxP4[a], kFxP4[b]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
void PluginEditor::setTab (int index)
|
||||
{
|
||||
currentTab = juce::jlimit (0, 4, index);
|
||||
oscView.setVisible (currentTab == 0);
|
||||
filterView.setVisible(currentTab == 1);
|
||||
modView.setVisible (currentTab == 2);
|
||||
fxView.setVisible (currentTab == 3);
|
||||
macroView.setVisible (currentTab == 4);
|
||||
|
||||
tabOsc.setToggleState (currentTab == 0, juce::dontSendNotification);
|
||||
tabFilter.setToggleState(currentTab == 1, juce::dontSendNotification);
|
||||
tabMod.setToggleState (currentTab == 2, juce::dontSendNotification);
|
||||
tabFx.setToggleState (currentTab == 3, juce::dontSendNotification);
|
||||
tabMacro.setToggleState (currentTab == 4, juce::dontSendNotification);
|
||||
}
|
||||
|
||||
void PluginEditor::applyUiScale()
|
||||
{
|
||||
const float scale = processor.getUiScale();
|
||||
currentScaleIndex = processor.getUiScaleIndex();
|
||||
root.setTransform (juce::AffineTransform::scale (scale));
|
||||
setSize ((int) (kBaseW * scale), (int) (kBaseH * scale));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
void PluginEditor::updateVisuals()
|
||||
{
|
||||
const auto& apvts = processor.parameters;
|
||||
auto gv = [&] (const char* id) { return apvts.getRawParameterValue (id)->load(); };
|
||||
|
||||
masterDisplay.setValue (formatPercent (gv (ids::master)));
|
||||
|
||||
// Waveforms.
|
||||
const int waveA = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (gv (ids::oscAWave) * (kNumWavetables - 1)));
|
||||
const int waveB = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (gv (ids::oscBWave) * (kNumWavetables - 1)));
|
||||
oscAWave.setWaveIndex (waveA);
|
||||
oscAWave.setFramePosition (gv (ids::oscAWtPos));
|
||||
oscAWave.setEnabled (gv (ids::oscAOn) > 0.5f);
|
||||
oscBWave.setWaveIndex (waveB);
|
||||
oscBWave.setFramePosition (gv (ids::oscBWtPos));
|
||||
oscBWave.setEnabled (gv (ids::oscBOn) > 0.5f);
|
||||
oscAWave.repaint();
|
||||
oscBWave.repaint();
|
||||
|
||||
// Filters.
|
||||
filter1Display.setParams ((int) std::llround (gv (ids::f1Type) * 6.0f), gv (ids::f1Cutoff), gv (ids::f1Res),
|
||||
gv (ids::f1Drive), (int) std::llround (gv (ids::f1Slope) * 2.0f));
|
||||
filter1Display.setEnabled (gv (ids::f1On) > 0.5f);
|
||||
filter2Display.setParams ((int) std::llround (gv (ids::f2Type) * 6.0f), gv (ids::f2Cutoff), gv (ids::f2Res),
|
||||
gv (ids::f2Drive), (int) std::llround (gv (ids::f2Slope) * 2.0f));
|
||||
filter2Display.setEnabled (gv (ids::f2On) > 0.5f);
|
||||
filter1Display.repaint();
|
||||
filter2Display.repaint();
|
||||
|
||||
// Envelopes.
|
||||
const char* a[4] = { ids::env1A, ids::env2A, ids::env3A, ids::env4A };
|
||||
const char* d[4] = { ids::env1D, ids::env2D, ids::env3D, ids::env4D };
|
||||
const char* s[4] = { ids::env1S, ids::env2S, ids::env3S, ids::env4S };
|
||||
const char* r[4] = { ids::env1R, ids::env2R, ids::env3R, ids::env4R };
|
||||
const char* c[4] = { ids::env1Curve, ids::env2Curve, ids::env3Curve, ids::env4Curve };
|
||||
for (int i = 0; i < kNumEnvelopes; ++i)
|
||||
{
|
||||
envDisplays[(size_t) i].setParams (gv (a[i]), gv (d[i]), gv (s[i]), gv (r[i]), gv (c[i]));
|
||||
envDisplays[(size_t) i].repaint();
|
||||
}
|
||||
|
||||
// LFOs.
|
||||
const char* lshape[4] = { ids::lfo1Shape, ids::lfo2Shape, ids::lfo3Shape, ids::lfo4Shape };
|
||||
for (int i = 0; i < kNumLfos; ++i)
|
||||
{
|
||||
lfoDisplays[(size_t) i].setShape ((int) std::llround (gv (lshape[i]) * 6.0f));
|
||||
lfoDisplays[(size_t) i].setShapeData (processor.engine.getLfos()[(size_t) i].getShapeData(),
|
||||
processor.engine.getLfos()[(size_t) i].getShapeSteps());
|
||||
lfoDisplays[(size_t) i].repaint();
|
||||
}
|
||||
|
||||
// RAVE state.
|
||||
raveButton.setToggleState (processor.isRaveEnabled());
|
||||
|
||||
// Scale change.
|
||||
if (processor.getUiScaleIndex() != currentScaleIndex)
|
||||
applyUiScale();
|
||||
}
|
||||
|
||||
void PluginEditor::updateModList()
|
||||
{
|
||||
juce::String text;
|
||||
const auto& cons = processor.engine.getMatrix().connections;
|
||||
for (const auto& c : cons)
|
||||
text += modSourceName (c.source) + " -> " + modTargetName (c.target)
|
||||
+ " [" + juce::String (c.depth, 2) + (c.bipolar ? ", bipolar]" : "]") + "\n";
|
||||
modList.setText (text, false);
|
||||
}
|
||||
|
||||
void PluginEditor::updateMacroList()
|
||||
{
|
||||
juce::String text;
|
||||
for (int m = 0; m < kNumMacros; ++m)
|
||||
{
|
||||
text += MacroControls::macroName (m) + ":\n";
|
||||
const auto& assigns = processor.engine.getMacros().assignments[(size_t) m];
|
||||
if (assigns.empty())
|
||||
text += " (none)\n";
|
||||
for (const auto& a : assigns)
|
||||
text += " " + modTargetName (a.target) + " [" + juce::String (a.depth, 2) + "]\n";
|
||||
}
|
||||
macroList.setText (text, false);
|
||||
}
|
||||
|
||||
void PluginEditor::timerCallback()
|
||||
{
|
||||
updateVisuals();
|
||||
updateModList();
|
||||
updateMacroList();
|
||||
}
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,125 @@
|
||||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "PluginProcessor.h"
|
||||
#include "GUI/SerumLookAndFeel.h"
|
||||
#include "GUI/Knob.h"
|
||||
#include "GUI/Slider.h"
|
||||
#include "GUI/ToggleButton.h"
|
||||
#include "GUI/Display.h"
|
||||
#include "GUI/Panel.h"
|
||||
#include "GUI/WaveformDisplay.h"
|
||||
#include "GUI/FilterDisplay.h"
|
||||
#include "GUI/EnvelopeDisplay.h"
|
||||
#include "GUI/LFODisplay.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
// ===========================================================================
|
||||
// SerumAlt editor: dark vector GUI with a tabbed layout, real-time
|
||||
// visualisations, RAVE, macro controls and preset-scale (75%..200%) scaling.
|
||||
// ===========================================================================
|
||||
class PluginEditor : public juce::AudioProcessorEditor,
|
||||
public juce::Timer
|
||||
{
|
||||
public:
|
||||
explicit PluginEditor (SerumAltAudioProcessor& p);
|
||||
~PluginEditor() override;
|
||||
|
||||
void paint (juce::Graphics&) override;
|
||||
void resized() override;
|
||||
void timerCallback() override;
|
||||
|
||||
void setTab (int index);
|
||||
|
||||
private:
|
||||
SerumAltAudioProcessor& processor;
|
||||
SerumLookAndFeel laf;
|
||||
|
||||
static constexpr int kBaseW = 1120;
|
||||
static constexpr int kBaseH = 740;
|
||||
|
||||
juce::Component root;
|
||||
|
||||
// --- top bar ---
|
||||
std::unique_ptr<juce::Drawable> logo;
|
||||
juce::ComboBox presetCombo;
|
||||
juce::ComboBox scaleCombo;
|
||||
ToggleButton raveButton;
|
||||
Knob masterKnob;
|
||||
Display masterDisplay;
|
||||
|
||||
// --- tabs ---
|
||||
juce::TextButton tabOsc, tabFilter, tabMod, tabFx, tabMacro;
|
||||
juce::Component oscView, filterView, modView, fxView, macroView;
|
||||
int currentTab = 0;
|
||||
|
||||
// --- OSC ---
|
||||
Panel oscAPanel { "Oscillator A" }, oscBPanel { "Oscillator B" };
|
||||
Panel subPanel { "Sub" }, noisePanel { "Noise" };
|
||||
WaveformDisplay oscAWave, oscBWave;
|
||||
std::vector<juce::Component*> oscAComponents, oscBComponents;
|
||||
|
||||
// --- FILTER ---
|
||||
Panel filter1Panel { "Filter 1" }, filter2Panel { "Filter 2" }, routingPanel { "Routing" };
|
||||
FilterDisplay filter1Display, filter2Display;
|
||||
|
||||
// --- MOD ---
|
||||
std::array<Panel, kNumEnvelopes> envPanels { { Panel ("Env 1 (Amp)"), Panel ("Env 2 (Filter)"),
|
||||
Panel ("Env 3"), Panel ("Env 4") } };
|
||||
std::array<EnvelopeDisplay, kNumEnvelopes> envDisplays;
|
||||
std::array<Panel, kNumLfos> lfoPanels { { Panel ("LFO 1"), Panel ("LFO 2"), Panel ("LFO 3"), Panel ("LFO 4") } };
|
||||
std::array<LFODisplay, kNumLfos> lfoDisplays;
|
||||
|
||||
Panel matrixPanel { "Modulation Matrix" };
|
||||
juce::ComboBox modSourceCombo, modTargetCombo;
|
||||
Knob modDepthKnob { "Depth" };
|
||||
ToggleButton modBipolarToggle;
|
||||
juce::TextButton modAddButton, modRemoveButton, modClearButton;
|
||||
juce::TextEditor modList;
|
||||
|
||||
// --- FX ---
|
||||
std::array<Panel, kNumFxSlots> fxPanels;
|
||||
std::array<juce::ComboBox*, kNumFxSlots> fxTypeCombos;
|
||||
std::array<std::vector<Knob*>, kNumFxSlots> fxKnobs;
|
||||
std::array<juce::TextButton*, kNumFxSlots> fxUp, fxDown;
|
||||
|
||||
// --- MACRO ---
|
||||
std::array<Panel, kNumMacros> macroPanels;
|
||||
std::array<Knob*, kNumMacros> macroKnobs;
|
||||
Panel macroAssignPanel { "Macro Assignments" };
|
||||
juce::ComboBox macroAssignTarget;
|
||||
juce::ComboBox macroAssignIndex;
|
||||
Knob macroDepthKnob { "Depth" };
|
||||
juce::TextButton macroAssignButton, macroClearButton;
|
||||
juce::TextEditor macroList;
|
||||
|
||||
// --- attachments ---
|
||||
std::vector<std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment>> sliderAttachments;
|
||||
std::vector<std::unique_ptr<juce::AudioProcessorValueTreeState::ComboBoxAttachment>> comboAttachments;
|
||||
|
||||
int currentScaleIndex = -1;
|
||||
|
||||
// --- helpers ---
|
||||
Knob* makeKnob (juce::Component* parent, const juce::String& name, const juce::String& paramId,
|
||||
std::function<juce::String (float)> fmt = {});
|
||||
juce::ComboBox* makeCombo (juce::Component* parent, const juce::String& paramId, const juce::StringArray& items);
|
||||
ToggleButton* makeToggle (juce::Component* parent, const juce::String& label, const juce::String& paramId);
|
||||
|
||||
void buildTopBar();
|
||||
void buildOscTab();
|
||||
void buildFilterTab();
|
||||
void buildModTab();
|
||||
void buildFxTab();
|
||||
void buildMacroTab();
|
||||
void swapFxSlots (int a, int b);
|
||||
void applyUiScale();
|
||||
void updateVisuals();
|
||||
void updateModList();
|
||||
void updateMacroList();
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginEditor)
|
||||
};
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,381 @@
|
||||
#include "PluginProcessor.h"
|
||||
#include "PluginEditor.h"
|
||||
#include "Presets/FactoryPresets.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
std::unique_ptr<juce::AudioParameterFloat> f (const char* id, const juce::String& name, float def)
|
||||
{
|
||||
return std::make_unique<juce::AudioParameterFloat> (juce::ParameterID { id, 1 }, name,
|
||||
juce::NormalisableRange<float> (0.0f, 1.0f), def);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
SerumAltAudioProcessor::SerumAltAudioProcessor()
|
||||
: AudioProcessor (BusesProperties().withInput ("Input", juce::AudioChannelSet::stereo(), false)
|
||||
.withOutput ("Output", juce::AudioChannelSet::stereo(), true)),
|
||||
parameters (*this, nullptr, juce::Identifier ("SerumAlt"), createParameterLayout())
|
||||
{
|
||||
}
|
||||
|
||||
SerumAltAudioProcessor::~SerumAltAudioProcessor() = default;
|
||||
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout SerumAltAudioProcessor::createParameterLayout()
|
||||
{
|
||||
std::vector<std::unique_ptr<juce::RangedAudioParameter>> params;
|
||||
|
||||
// Global
|
||||
params.push_back (f (ids::master, "Master", 0.8f));
|
||||
params.push_back (f (ids::uiScale, "UI Scale", 0.25f));
|
||||
params.push_back (f (ids::rave, "RAVE", 0.0f));
|
||||
|
||||
// Oscillator A
|
||||
params.push_back (f (ids::oscAOn, "Osc A On", 1.0f));
|
||||
params.push_back (f (ids::oscAWave, "Osc A Wave", 0.0f));
|
||||
params.push_back (f (ids::oscAWtPos, "Osc A WT Pos", 0.0f));
|
||||
params.push_back (f (ids::oscAWarp, "Osc A Warp", 0.0f));
|
||||
params.push_back (f (ids::oscAWarpAmt, "Osc A Warp Amt", 0.0f));
|
||||
params.push_back (f (ids::oscACoarse, "Osc A Coarse", 0.5f));
|
||||
params.push_back (f (ids::oscAFine, "Osc A Fine", 0.5f));
|
||||
params.push_back (f (ids::oscALevel, "Osc A Level", 0.8f));
|
||||
params.push_back (f (ids::oscAPan, "Osc A Pan", 0.5f));
|
||||
params.push_back (f (ids::oscAUnison, "Osc A Unison", 0.0f));
|
||||
params.push_back (f (ids::oscADetune, "Osc A Detune", 0.0f));
|
||||
params.push_back (f (ids::oscASpread, "Osc A Spread", 0.0f));
|
||||
params.push_back (f (ids::oscAPhase, "Osc A Phase", 0.0f));
|
||||
params.push_back (f (ids::oscARandPh, "Osc A Rand Phase", 0.0f));
|
||||
|
||||
// Oscillator B
|
||||
params.push_back (f (ids::oscBOn, "Osc B On", 0.0f));
|
||||
params.push_back (f (ids::oscBWave, "Osc B Wave", 1.0f / 9.0f));
|
||||
params.push_back (f (ids::oscBWtPos, "Osc B WT Pos", 0.0f));
|
||||
params.push_back (f (ids::oscBWarp, "Osc B Warp", 0.0f));
|
||||
params.push_back (f (ids::oscBWarpAmt, "Osc B Warp Amt", 0.0f));
|
||||
params.push_back (f (ids::oscBCoarse, "Osc B Coarse", 0.5f));
|
||||
params.push_back (f (ids::oscBFine, "Osc B Fine", 0.5f));
|
||||
params.push_back (f (ids::oscBLevel, "Osc B Level", 0.5f));
|
||||
params.push_back (f (ids::oscBPan, "Osc B Pan", 0.5f));
|
||||
params.push_back (f (ids::oscBUnison, "Osc B Unison", 0.0f));
|
||||
params.push_back (f (ids::oscBDetune, "Osc B Detune", 0.0f));
|
||||
params.push_back (f (ids::oscBSpread, "Osc B Spread", 0.0f));
|
||||
params.push_back (f (ids::oscBPhase, "Osc B Phase", 0.0f));
|
||||
params.push_back (f (ids::oscBRandPh, "Osc B Rand Phase", 0.0f));
|
||||
|
||||
// Sub
|
||||
params.push_back (f (ids::subOn, "Sub On", 0.0f));
|
||||
params.push_back (f (ids::subShape, "Sub Shape", 0.0f));
|
||||
params.push_back (f (ids::subOct, "Sub Octave", 1.0f / 2.0f));
|
||||
params.push_back (f (ids::subLevel, "Sub Level", 0.5f));
|
||||
|
||||
// Noise
|
||||
params.push_back (f (ids::noiseOn, "Noise On", 0.0f));
|
||||
params.push_back (f (ids::noiseType, "Noise Type", 0.0f));
|
||||
params.push_back (f (ids::noiseLevel, "Noise Level", 0.5f));
|
||||
|
||||
// Filter 1
|
||||
params.push_back (f (ids::f1On, "Filter 1 On", 1.0f));
|
||||
params.push_back (f (ids::f1Type, "Filter 1 Type", 0.0f));
|
||||
params.push_back (f (ids::f1Cutoff, "Filter 1 Cutoff", 0.65f));
|
||||
params.push_back (f (ids::f1Res, "Filter 1 Res", 0.05f));
|
||||
params.push_back (f (ids::f1Drive, "Filter 1 Drive", 0.0f));
|
||||
params.push_back (f (ids::f1Key, "Filter 1 Keytrack", 0.0f));
|
||||
params.push_back (f (ids::f1Slope, "Filter 1 Slope", 1.0f));
|
||||
|
||||
// Filter 2
|
||||
params.push_back (f (ids::f2On, "Filter 2 On", 0.0f));
|
||||
params.push_back (f (ids::f2Type, "Filter 2 Type", 0.0f));
|
||||
params.push_back (f (ids::f2Cutoff, "Filter 2 Cutoff", 0.5f));
|
||||
params.push_back (f (ids::f2Res, "Filter 2 Res", 0.0f));
|
||||
params.push_back (f (ids::f2Drive, "Filter 2 Drive", 0.0f));
|
||||
params.push_back (f (ids::f2Key, "Filter 2 Keytrack", 0.0f));
|
||||
params.push_back (f (ids::f2Slope, "Filter 2 Slope", 1.0f));
|
||||
|
||||
params.push_back (f (ids::fRoute, "Filter Route", 0.0f));
|
||||
params.push_back (f (ids::fMix, "Filter Mix", 0.5f));
|
||||
params.push_back (f (ids::fOut, "Filter Out", 0.667f));
|
||||
|
||||
// Envelopes 1..4
|
||||
const char* envA[4] = { ids::env1A, ids::env2A, ids::env3A, ids::env4A };
|
||||
const char* envD[4] = { ids::env1D, ids::env2D, ids::env3D, ids::env4D };
|
||||
const char* envS[4] = { ids::env1S, ids::env2S, ids::env3S, ids::env4S };
|
||||
const char* envR[4] = { ids::env1R, ids::env2R, ids::env3R, ids::env4R };
|
||||
const char* envC[4] = { ids::env1Curve, ids::env2Curve, ids::env3Curve, ids::env4Curve };
|
||||
const float envADef[4] = { 0.05f, 0.1f, 0.2f, 0.2f };
|
||||
const float envDDef[4] = { 0.25f, 0.3f, 0.3f, 0.3f };
|
||||
const float envSDef[4] = { 0.8f, 0.5f, 0.5f, 0.5f };
|
||||
const float envRDef[4] = { 0.3f, 0.3f, 0.4f, 0.4f };
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
params.push_back (f (envA[i], juce::String ("Env ") + juce::String (i + 1) + " Attack", envADef[i]));
|
||||
params.push_back (f (envD[i], juce::String ("Env ") + juce::String (i + 1) + " Decay", envDDef[i]));
|
||||
params.push_back (f (envS[i], juce::String ("Env ") + juce::String (i + 1) + " Sustain", envSDef[i]));
|
||||
params.push_back (f (envR[i], juce::String ("Env ") + juce::String (i + 1) + " Release", envRDef[i]));
|
||||
params.push_back (f (envC[i], juce::String ("Env ") + juce::String (i + 1) + " Curve", 0.5f));
|
||||
}
|
||||
|
||||
// LFOs 1..4
|
||||
const char* lfoRate[4] = { ids::lfo1Rate, ids::lfo2Rate, ids::lfo3Rate, ids::lfo4Rate };
|
||||
const char* lfoSync[4] = { ids::lfo1Sync, ids::lfo2Sync, ids::lfo3Sync, ids::lfo4Sync };
|
||||
const char* lfoBeat[4] = { ids::lfo1Beat, ids::lfo2Beat, ids::lfo3Beat, ids::lfo4Beat };
|
||||
const char* lfoShape[4] = { ids::lfo1Shape, ids::lfo2Shape, ids::lfo3Shape, ids::lfo4Shape };
|
||||
const char* lfoPhase[4] = { ids::lfo1Phase, ids::lfo2Phase, ids::lfo3Phase, ids::lfo4Phase };
|
||||
const char* lfoFade[4] = { ids::lfo1Fade, ids::lfo2Fade, ids::lfo3Fade, ids::lfo4Fade };
|
||||
const char* lfoDelay[4] = { ids::lfo1Delay, ids::lfo2Delay, ids::lfo3Delay, ids::lfo4Delay };
|
||||
const float lfoShapeDef[4] = { 0.0f, 1.0f / 6.0f, 2.0f / 6.0f, 3.0f / 6.0f };
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
const juce::String n = juce::String ("LFO ") + juce::String (i + 1);
|
||||
params.push_back (f (lfoRate[i], n + " Rate", 0.5f));
|
||||
params.push_back (f (lfoSync[i], n + " Sync", 0.0f));
|
||||
params.push_back (f (lfoBeat[i], n + " Beat", 0.5f));
|
||||
params.push_back (f (lfoShape[i], n + " Shape", lfoShapeDef[i]));
|
||||
params.push_back (f (lfoPhase[i], n + " Phase", 0.0f));
|
||||
params.push_back (f (lfoFade[i], n + " Fade", 0.0f));
|
||||
params.push_back (f (lfoDelay[i], n + " Delay", 0.0f));
|
||||
}
|
||||
|
||||
// FX slots 1..8
|
||||
const char* fxType[8] = { ids::fx1Type, ids::fx2Type, ids::fx3Type, ids::fx4Type,
|
||||
ids::fx5Type, ids::fx6Type, ids::fx7Type, ids::fx8Type };
|
||||
const char* fxMix[8] = { ids::fx1Mix, ids::fx2Mix, ids::fx3Mix, ids::fx4Mix,
|
||||
ids::fx5Mix, ids::fx6Mix, ids::fx7Mix, ids::fx8Mix };
|
||||
const char* fxP[8][4] = {
|
||||
{ ids::fx1P1, ids::fx1P2, ids::fx1P3, ids::fx1P4 },
|
||||
{ ids::fx2P1, ids::fx2P2, ids::fx2P3, ids::fx2P4 },
|
||||
{ ids::fx3P1, ids::fx3P2, ids::fx3P3, ids::fx3P4 },
|
||||
{ ids::fx4P1, ids::fx4P2, ids::fx4P3, ids::fx4P4 },
|
||||
{ ids::fx5P1, ids::fx5P2, ids::fx5P3, ids::fx5P4 },
|
||||
{ ids::fx6P1, ids::fx6P2, ids::fx6P3, ids::fx6P4 },
|
||||
{ ids::fx7P1, ids::fx7P2, ids::fx7P3, ids::fx7P4 },
|
||||
{ ids::fx8P1, ids::fx8P2, ids::fx8P3, ids::fx8P4 }
|
||||
};
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
const juce::String n = juce::String ("FX ") + juce::String (i + 1);
|
||||
params.push_back (f (fxType[i], n + " Type", 0.0f));
|
||||
params.push_back (f (fxMix[i], n + " Mix", 0.5f));
|
||||
params.push_back (f (fxP[i][0], n + " P1", 0.5f));
|
||||
params.push_back (f (fxP[i][1], n + " P2", 0.5f));
|
||||
params.push_back (f (fxP[i][2], n + " P3", 0.5f));
|
||||
params.push_back (f (fxP[i][3], n + " P4", 0.5f));
|
||||
}
|
||||
|
||||
// Macros
|
||||
params.push_back (f (ids::macro1, "Macro 1", 0.0f));
|
||||
params.push_back (f (ids::macro2, "Macro 2", 0.0f));
|
||||
params.push_back (f (ids::macro3, "Macro 3", 0.0f));
|
||||
params.push_back (f (ids::macro4, "Macro 4", 0.0f));
|
||||
|
||||
return { params.begin(), params.end() };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
void SerumAltAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
|
||||
{
|
||||
engine.prepare (sampleRate, samplesPerBlock);
|
||||
}
|
||||
|
||||
void SerumAltAudioProcessor::releaseResources()
|
||||
{
|
||||
engine.reset();
|
||||
}
|
||||
|
||||
void SerumAltAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi)
|
||||
{
|
||||
juce::ScopedNoDenormals noDenormals;
|
||||
engine.processBlock (buffer, midi, parameters, getPlayHead());
|
||||
}
|
||||
|
||||
juce::AudioProcessorEditor* SerumAltAudioProcessor::createEditor()
|
||||
{
|
||||
return new PluginEditor (*this);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Programs / presets
|
||||
// ---------------------------------------------------------------------------
|
||||
int SerumAltAudioProcessor::getNumPrograms()
|
||||
{
|
||||
return (int) getFactoryPresets().size();
|
||||
}
|
||||
|
||||
int SerumAltAudioProcessor::getCurrentProgram()
|
||||
{
|
||||
return currentProgram;
|
||||
}
|
||||
|
||||
void SerumAltAudioProcessor::setCurrentProgram (int index)
|
||||
{
|
||||
index = juce::jlimit (0, getNumPrograms() - 1, index);
|
||||
loadFactoryPreset (index);
|
||||
currentProgram = index;
|
||||
}
|
||||
|
||||
const juce::String SerumAltAudioProcessor::getProgramName (int index)
|
||||
{
|
||||
const auto& presets = getFactoryPresets();
|
||||
if (index >= 0 && index < (int) presets.size())
|
||||
return presets[(size_t) index].name;
|
||||
return {};
|
||||
}
|
||||
|
||||
void SerumAltAudioProcessor::changeProgramName (int, const juce::String&)
|
||||
{
|
||||
}
|
||||
|
||||
int SerumAltAudioProcessor::getNumFactoryPresets() const
|
||||
{
|
||||
return (int) getFactoryPresets().size();
|
||||
}
|
||||
|
||||
void SerumAltAudioProcessor::loadFactoryPreset (int index)
|
||||
{
|
||||
const auto& presets = getFactoryPresets();
|
||||
if (index < 0 || index >= (int) presets.size())
|
||||
return;
|
||||
|
||||
const FactoryPreset& preset = presets[(size_t) index];
|
||||
|
||||
// RAVE should start off for a freshly loaded preset.
|
||||
rave.resetSnapshot();
|
||||
if (auto* raveParam = parameters.getParameter (ids::rave))
|
||||
raveParam->setValueNotifyingHost (0.0f);
|
||||
|
||||
for (const auto& kv : preset.params)
|
||||
if (auto* param = parameters.getParameter (kv.first))
|
||||
param->setValueNotifyingHost (kv.second);
|
||||
|
||||
engine.getMatrix().clear();
|
||||
for (const auto& mod : preset.mods)
|
||||
engine.getMatrix().addConnection (mod.source, mod.target, mod.depth, mod.bipolar);
|
||||
|
||||
engine.getMacros().clear();
|
||||
for (const auto& ma : preset.macroAssigns)
|
||||
engine.getMacros().addAssignment (ma.macro, ma.target, ma.depth);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RAVE / UI scale
|
||||
// ---------------------------------------------------------------------------
|
||||
void SerumAltAudioProcessor::setRaveEnabled (bool enabled)
|
||||
{
|
||||
rave.setEnabled (enabled, parameters);
|
||||
if (auto* raveParam = parameters.getParameter (ids::rave))
|
||||
raveParam->setValueNotifyingHost (enabled ? 1.0f : 0.0f);
|
||||
}
|
||||
|
||||
bool SerumAltAudioProcessor::isRaveEnabled() const
|
||||
{
|
||||
return rave.isEnabled();
|
||||
}
|
||||
|
||||
int SerumAltAudioProcessor::getUiScaleIndex() const
|
||||
{
|
||||
if (auto* p = parameters.getRawParameterValue (ids::uiScale))
|
||||
return juce::jlimit (0, 4, (int) std::llround (p->load() * 4.0f));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void SerumAltAudioProcessor::setUiScaleIndex (int index)
|
||||
{
|
||||
index = juce::jlimit (0, 4, index);
|
||||
if (auto* p = parameters.getParameter (ids::uiScale))
|
||||
p->setValueNotifyingHost ((float) index / 4.0f);
|
||||
}
|
||||
|
||||
float SerumAltAudioProcessor::getUiScale() const
|
||||
{
|
||||
static constexpr float scales[5] = { 0.75f, 1.0f, 1.25f, 1.5f, 2.0f };
|
||||
return scales[getUiScaleIndex()];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State
|
||||
// ---------------------------------------------------------------------------
|
||||
void SerumAltAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
|
||||
{
|
||||
auto state = parameters.copyState();
|
||||
state.appendChild (engine.getMatrix().toValueTree(), nullptr);
|
||||
state.appendChild (engine.getMacros().toValueTree(), nullptr);
|
||||
saveLfoShapesToState (state);
|
||||
|
||||
std::unique_ptr<juce::XmlElement> xml (state.createXml());
|
||||
if (xml != nullptr)
|
||||
copyXmlToBinary (*xml, destData);
|
||||
}
|
||||
|
||||
void SerumAltAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
|
||||
{
|
||||
std::unique_ptr<juce::XmlElement> xml (getXmlFromBinary (data, sizeInBytes));
|
||||
if (xml == nullptr)
|
||||
return;
|
||||
|
||||
juce::ValueTree state = juce::ValueTree::fromXml (*xml);
|
||||
if (! state.isValid())
|
||||
return;
|
||||
|
||||
parameters.replaceState (state);
|
||||
engine.getMatrix().fromValueTree (state.getChildWithName ("MODMATRIX"));
|
||||
engine.getMacros().fromValueTree (state.getChildWithName ("MACROS"));
|
||||
restoreLfoShapesFromState (state);
|
||||
|
||||
// A persisted RAVE toggle has no live snapshot, so start it off.
|
||||
rave.resetSnapshot();
|
||||
if (auto* raveParam = parameters.getParameter (ids::rave))
|
||||
raveParam->setValueNotifyingHost (0.0f);
|
||||
}
|
||||
|
||||
void SerumAltAudioProcessor::saveLfoShapesToState (juce::ValueTree& state) const
|
||||
{
|
||||
juce::ValueTree tree ("LFOSHAPES");
|
||||
for (int i = 0; i < kNumLfos; ++i)
|
||||
{
|
||||
const auto& data = engine.getLfos()[(size_t) i].getShapeData();
|
||||
juce::ValueTree lfo ("LFO");
|
||||
lfo.setProperty ("index", i, nullptr);
|
||||
lfo.setProperty ("steps", engine.getLfos()[(size_t) i].getShapeSteps(), nullptr);
|
||||
|
||||
juce::Array<juce::var> arr;
|
||||
for (float vv : data)
|
||||
arr.add (vv);
|
||||
lfo.setProperty ("data", juce::var (arr), nullptr);
|
||||
tree.appendChild (lfo, nullptr);
|
||||
}
|
||||
state.appendChild (tree, nullptr);
|
||||
}
|
||||
|
||||
void SerumAltAudioProcessor::restoreLfoShapesFromState (const juce::ValueTree& state)
|
||||
{
|
||||
const juce::ValueTree tree = state.getChildWithName ("LFOSHAPES");
|
||||
if (! tree.isValid())
|
||||
return;
|
||||
|
||||
for (const auto& lfo : tree)
|
||||
{
|
||||
if (! lfo.hasType ("LFO"))
|
||||
continue;
|
||||
const int index = juce::jlimit (0, kNumLfos - 1, (int) lfo.getProperty ("index", 0));
|
||||
const int steps = (int) lfo.getProperty ("steps", 16);
|
||||
|
||||
std::vector<float> data;
|
||||
if (auto* arr = lfo.getProperty ("data").getArray())
|
||||
for (const auto& vv : *arr)
|
||||
data.push_back ((float) vv);
|
||||
|
||||
engine.setLfoShapeData (index, data, steps);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace serum
|
||||
|
||||
// ===========================================================================
|
||||
// Plugin entry point (required by the JUCE plugin clients).
|
||||
// ===========================================================================
|
||||
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
|
||||
{
|
||||
return new serum::SerumAltAudioProcessor();
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "Params.h"
|
||||
#include "Engine.h"
|
||||
#include "RAVEButton.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
// ===========================================================================
|
||||
// SerumAlt — a wavetable synthesiser (VST3 / AU). Hosts the APVTS, the audio
|
||||
// engine, preset management and the RAVE controller.
|
||||
// ===========================================================================
|
||||
class SerumAltAudioProcessor : public juce::AudioProcessor
|
||||
{
|
||||
public:
|
||||
SerumAltAudioProcessor();
|
||||
~SerumAltAudioProcessor() override;
|
||||
|
||||
// --- AudioProcessor ---
|
||||
void prepareToPlay (double sampleRate, int samplesPerBlock) override;
|
||||
void releaseResources() override;
|
||||
void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
|
||||
|
||||
juce::AudioProcessorEditor* createEditor() override;
|
||||
bool hasEditor() const override { return true; }
|
||||
|
||||
const juce::String getName() const override { return "SerumAlt"; }
|
||||
bool acceptsMidi() const override { return true; }
|
||||
bool producesMidi() const override { return false; }
|
||||
bool isMidiEffect() const override { return false; }
|
||||
double getTailLengthSeconds() const override { return 2.0; }
|
||||
|
||||
int getNumPrograms() override;
|
||||
int getCurrentProgram() override;
|
||||
void setCurrentProgram (int index) override;
|
||||
const juce::String getProgramName (int index) override;
|
||||
void changeProgramName (int index, const juce::String& newName) override;
|
||||
|
||||
void getStateInformation (juce::MemoryBlock& destData) override;
|
||||
void setStateInformation (const void* data, int sizeInBytes) override;
|
||||
|
||||
// --- SerumAlt ---
|
||||
void setRaveEnabled (bool enabled);
|
||||
bool isRaveEnabled() const;
|
||||
|
||||
int getUiScaleIndex() const;
|
||||
void setUiScaleIndex (int index);
|
||||
float getUiScale() const;
|
||||
|
||||
void loadFactoryPreset (int index);
|
||||
int getNumFactoryPresets() const;
|
||||
|
||||
// Public DSP state (read/write from the GUI thread).
|
||||
juce::AudioProcessorValueTreeState parameters;
|
||||
Engine engine;
|
||||
RaveController rave;
|
||||
|
||||
static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
|
||||
|
||||
private:
|
||||
int currentProgram = 0;
|
||||
|
||||
void restoreLfoShapesFromState (const juce::ValueTree& state);
|
||||
void saveLfoShapesToState (juce::ValueTree& state) const;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SerumAltAudioProcessor)
|
||||
};
|
||||
|
||||
} // namespace serum
|
||||
Reference in New Issue
Block a user