perf(gui): skip unchanged repaints and gate updates by tab
Add change detection to Display, WaveformDisplay, FilterDisplay, EnvelopeDisplay, and LFODisplay so repaint() is only called when values actually change. Gate updateVisuals() by currentTab so only the visible tab's displays are updated each timer tick. Similarly gate updateModList/updateMacroList to their respective tabs. Manage dynamically created controls via ownedControls unique_ptr vector instead of raw new leaks. Null-check parameter lookups before creating attachments. Hold engine control lock when reading matrix/macros/LFO shape data. Fix envelope display curve directions (attack rising from bottom, decay falling toward sustain, release falling toward bottom). Add tooltip to FilterDisplay noting the approximation. Replace ToggleButton raw param pointer with ParameterAttachment for proper gesture handling. Sync preset and scale combos to processor state.
This commit is contained in:
@@ -12,8 +12,8 @@ namespace serum
|
|||||||
class Display : public juce::Component
|
class Display : public juce::Component
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void setTitle (const juce::String& t) { title = t; repaint(); }
|
void setTitle (const juce::String& t) { if (title != t) { title = t; repaint(); } }
|
||||||
void setValue (const juce::String& v) { value = v; repaint(); }
|
void setValue (const juce::String& v) { if (value != v) { value = v; repaint(); } }
|
||||||
|
|
||||||
void paint (juce::Graphics& g) override;
|
void paint (juce::Graphics& g) override;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ namespace serum
|
|||||||
|
|
||||||
void EnvelopeDisplay::setParams (float a, float d, float s, float r, float c)
|
void EnvelopeDisplay::setParams (float a, float d, float s, float r, float c)
|
||||||
{
|
{
|
||||||
|
if (attack == a && decay == d && sustain == s && release == r && curve == c)
|
||||||
|
return;
|
||||||
attack = a; decay = d; sustain = s; release = r; curve = c;
|
attack = a; decay = d; sustain = s; release = r; curve = c;
|
||||||
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
void EnvelopeDisplay::paint (juce::Graphics& g)
|
void EnvelopeDisplay::paint (juce::Graphics& g)
|
||||||
@@ -19,7 +22,7 @@ void EnvelopeDisplay::paint (juce::Graphics& g)
|
|||||||
const float atkShape = 0.3f + curve * 2.7f;
|
const float atkShape = 0.3f + curve * 2.7f;
|
||||||
const float decShape = 3.0f - curve * 2.7f;
|
const float decShape = 3.0f - curve * 2.7f;
|
||||||
|
|
||||||
// Normalise durations for display (attack 0..1, decay 0..0.6, release 0..0.6).
|
// Normalise durations for display, allowing a short sustain plateau.
|
||||||
const float aSec = maps::toSeconds (attack);
|
const float aSec = maps::toSeconds (attack);
|
||||||
const float dSec = maps::toSeconds (decay);
|
const float dSec = maps::toSeconds (decay);
|
||||||
const float rSec = maps::toSeconds (release);
|
const float rSec = maps::toSeconds (release);
|
||||||
@@ -36,7 +39,6 @@ void EnvelopeDisplay::paint (juce::Graphics& g)
|
|||||||
|
|
||||||
juce::Path path;
|
juce::Path path;
|
||||||
path.startNewSubPath (left, bottom);
|
path.startNewSubPath (left, bottom);
|
||||||
path.lineTo (left, top);
|
|
||||||
|
|
||||||
// Attack (curve-shaped).
|
// Attack (curve-shaped).
|
||||||
const int steps = 48;
|
const int steps = 48;
|
||||||
@@ -44,7 +46,7 @@ void EnvelopeDisplay::paint (juce::Graphics& g)
|
|||||||
for (int i = 0; i <= steps; ++i)
|
for (int i = 0; i <= steps; ++i)
|
||||||
{
|
{
|
||||||
const float p = (float) i / steps;
|
const float p = (float) i / steps;
|
||||||
const float y = top + (bottom - top) * std::pow (p, atkShape);
|
const float y = bottom - (bottom - top) * std::pow (p, atkShape);
|
||||||
path.lineTo (left + p * (peakX - left), y);
|
path.lineTo (left + p * (peakX - left), y);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +55,7 @@ void EnvelopeDisplay::paint (juce::Graphics& g)
|
|||||||
for (int i = 0; i <= steps; ++i)
|
for (int i = 0; i <= steps; ++i)
|
||||||
{
|
{
|
||||||
const float p = (float) i / steps;
|
const float p = (float) i / steps;
|
||||||
const float y = sustainY + (bottom - sustainY) * std::pow (1.0f - p, decShape);
|
const float y = sustainY + (top - sustainY) * std::pow (1.0f - p, decShape);
|
||||||
path.lineTo (peakX + p * (decX - peakX), y);
|
path.lineTo (peakX + p * (decX - peakX), y);
|
||||||
}
|
}
|
||||||
path.lineTo (decX, sustainY);
|
path.lineTo (decX, sustainY);
|
||||||
@@ -64,7 +66,7 @@ void EnvelopeDisplay::paint (juce::Graphics& g)
|
|||||||
for (int i = 0; i <= steps; ++i)
|
for (int i = 0; i <= steps; ++i)
|
||||||
{
|
{
|
||||||
const float p = (float) i / steps;
|
const float p = (float) i / steps;
|
||||||
const float y = sustainY + (bottom - sustainY) * std::pow (1.0f - p, decShape);
|
const float y = bottom - (bottom - sustainY) * std::pow (1.0f - p, decShape);
|
||||||
path.lineTo (relStartX + p * (right - relStartX), y);
|
path.lineTo (relStartX + p * (right - relStartX), y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ float FilterDisplay::magnitude (float freqHz, float cutoffHz, float res, int typ
|
|||||||
|
|
||||||
void FilterDisplay::setParams (int t, float c, float r, float d, int s)
|
void FilterDisplay::setParams (int t, float c, float r, float d, int s)
|
||||||
{
|
{
|
||||||
|
if (type == t && cutoff == c && res == r && drive == d && slope == s)
|
||||||
|
return;
|
||||||
type = t; cutoff = c; res = r; drive = d; slope = s;
|
type = t; cutoff = c; res = r; drive = d; slope = s;
|
||||||
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FilterDisplay::paint (juce::Graphics& g)
|
void FilterDisplay::paint (juce::Graphics& g)
|
||||||
|
|||||||
@@ -8,13 +8,15 @@ namespace serum
|
|||||||
{
|
{
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Filter frequency-response view (magnitude vs log frequency).
|
// Approximate filter frequency-response view (magnitude vs log frequency).
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
class FilterDisplay : public juce::Component
|
class FilterDisplay : public juce::Component,
|
||||||
|
public juce::SettableTooltipClient
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
FilterDisplay() { setTooltip ("Approximate response preview; slope, drive and modulation are not modelled."); }
|
||||||
void setParams (int type, float cutoff, float res, float drive, int slope);
|
void setParams (int type, float cutoff, float res, float drive, int slope);
|
||||||
void setEnabled (bool e) { enabled = e; }
|
void setEnabled (bool e) { if (enabled != e) { enabled = e; repaint(); } }
|
||||||
|
|
||||||
void paint (juce::Graphics& g) override;
|
void paint (juce::Graphics& g) override;
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,16 @@ namespace serum
|
|||||||
|
|
||||||
void LFODisplay::setShapeData (const std::vector<float>& data, int s)
|
void LFODisplay::setShapeData (const std::vector<float>& data, int s)
|
||||||
{
|
{
|
||||||
steps = juce::jlimit (2, 64, s);
|
const int newSteps = juce::jlimit (2, 64, s);
|
||||||
shapeData = data;
|
const size_t dataSize = std::min (data.size(), size_t (64));
|
||||||
if ((int) shapeData.size() < 64)
|
if (steps == newSteps && shapeData.size() == 64
|
||||||
shapeData.resize (64, 0.0f);
|
&& std::equal (data.begin(), data.begin() + dataSize, shapeData.begin())
|
||||||
|
&& std::all_of (shapeData.begin() + dataSize, shapeData.end(), [] (float v) { return v == 0.0f; }))
|
||||||
|
return;
|
||||||
|
|
||||||
|
steps = newSteps;
|
||||||
|
shapeData.assign (data.begin(), data.begin() + dataSize);
|
||||||
|
shapeData.resize (64, 0.0f);
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace serum
|
|||||||
class LFODisplay : public juce::Component
|
class LFODisplay : public juce::Component
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void setShape (int s) { shape = s; repaint(); }
|
void setShape (int s) { if (shape != s) { shape = s; repaint(); } }
|
||||||
void setShapeData (const std::vector<float>& data, int steps);
|
void setShapeData (const std::vector<float>& data, int steps);
|
||||||
void setOnShapeEdited (std::function<void (const std::vector<float>&, int)> cb) { onEdited = std::move (cb); }
|
void setOnShapeEdited (std::function<void (const std::vector<float>&, int)> cb) { onEdited = std::move (cb); }
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ ToggleButton::ToggleButton (const juce::String& lbl) : label (lbl)
|
|||||||
|
|
||||||
void ToggleButton::attach (juce::AudioProcessorValueTreeState& apvts, const juce::String& paramId)
|
void ToggleButton::attach (juce::AudioProcessorValueTreeState& apvts, const juce::String& paramId)
|
||||||
{
|
{
|
||||||
param = apvts.getParameter (paramId);
|
attachment.reset();
|
||||||
if (param != nullptr)
|
if (auto* param = apvts.getParameter (paramId))
|
||||||
{
|
{
|
||||||
state = param->getValue() > 0.5f;
|
|
||||||
attachment = std::make_unique<juce::ParameterAttachment> (*param,
|
attachment = std::make_unique<juce::ParameterAttachment> (*param,
|
||||||
[this] (float newValue) { setToggleState (newValue > 0.5f); });
|
[this] (float newValue) { setToggleState (newValue > 0.5f); });
|
||||||
|
attachment->sendInitialUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,9 +60,9 @@ void ToggleButton::mouseDown (const juce::MouseEvent&)
|
|||||||
{
|
{
|
||||||
onClick (newState);
|
onClick (newState);
|
||||||
}
|
}
|
||||||
else if (param != nullptr)
|
else if (attachment != nullptr)
|
||||||
{
|
{
|
||||||
param->setValueNotifyingHost (newState ? 1.0f : 0.0f);
|
attachment->setValueAsCompleteGesture (newState ? 1.0f : 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
setToggleState (newState);
|
setToggleState (newState);
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ public:
|
|||||||
private:
|
private:
|
||||||
bool state = false;
|
bool state = false;
|
||||||
juce::String label;
|
juce::String label;
|
||||||
juce::RangedAudioParameter* param = nullptr;
|
|
||||||
std::unique_ptr<juce::ParameterAttachment> attachment;
|
std::unique_ptr<juce::ParameterAttachment> attachment;
|
||||||
std::function<void (bool)> onClick;
|
std::function<void (bool)> onClick;
|
||||||
juce::Colour onColour = theme::accent;
|
juce::Colour onColour = theme::accent;
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ namespace serum
|
|||||||
class WaveformDisplay : public juce::Component
|
class WaveformDisplay : public juce::Component
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void setWavetables (const WavetableLibrary* lib) { wtLib = lib; }
|
void setWavetables (const WavetableLibrary* lib) { if (wtLib != lib) { wtLib = lib; repaint(); } }
|
||||||
void setWaveIndex (int index) { wave = index; }
|
void setWaveIndex (int index) { if (wave != index) { wave = index; repaint(); } }
|
||||||
void setFramePosition (float pos) { wtPos = pos; }
|
void setFramePosition (float pos) { if (wtPos != pos) { wtPos = pos; repaint(); } }
|
||||||
void setEnabled (bool e) { enabled = e; }
|
void setEnabled (bool e) { if (enabled != e) { enabled = e; repaint(); } }
|
||||||
|
|
||||||
void paint (juce::Graphics& g) override;
|
void paint (juce::Graphics& g) override;
|
||||||
|
|
||||||
|
|||||||
+176
-90
@@ -94,18 +94,25 @@ namespace
|
|||||||
knobs[(size_t) i]->setBounds (x + i * (w + gap), y, w, h);
|
knobs[(size_t) i]->setBounds (x + i * (w + gap), y, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* kFxType[8] = { ids::fx1Type, ids::fx2Type, ids::fx3Type, ids::fx4Type,
|
constexpr auto& kFxType = paramIds::fxType;
|
||||||
ids::fx5Type, ids::fx6Type, ids::fx7Type, ids::fx8Type };
|
constexpr auto& kFxMix = paramIds::fxMix;
|
||||||
const char* kFxMix[8] = { ids::fx1Mix, ids::fx2Mix, ids::fx3Mix, ids::fx4Mix,
|
constexpr auto& kFxP1 = paramIds::fxP1;
|
||||||
ids::fx5Mix, ids::fx6Mix, ids::fx7Mix, ids::fx8Mix };
|
constexpr auto& kFxP2 = paramIds::fxP2;
|
||||||
const char* kFxP1[8] = { ids::fx1P1, ids::fx2P1, ids::fx3P1, ids::fx4P1,
|
constexpr auto& kFxP3 = paramIds::fxP3;
|
||||||
ids::fx5P1, ids::fx6P1, ids::fx7P1, ids::fx8P1 };
|
constexpr auto& kFxP4 = paramIds::fxP4;
|
||||||
const char* kFxP2[8] = { ids::fx1P2, ids::fx2P2, ids::fx3P2, ids::fx4P2,
|
|
||||||
ids::fx5P2, ids::fx6P2, ids::fx7P2, ids::fx8P2 };
|
constexpr const char* kFxParamNames[(int) FxType::Count][4] = {
|
||||||
const char* kFxP3[8] = { ids::fx1P3, ids::fx2P3, ids::fx3P3, ids::fx4P3,
|
{ "P1", "P2", "P3", "P4" },
|
||||||
ids::fx5P3, ids::fx6P3, ids::fx7P3, ids::fx8P3 };
|
{ "Intensity", "Low Amount", "High Amount", "Output" },
|
||||||
const char* kFxP4[8] = { ids::fx1P4, ids::fx2P4, ids::fx3P4, ids::fx4P4,
|
{ "Rate", "Depth", "Width", "Unused" },
|
||||||
ids::fx5P4, ids::fx6P4, ids::fx7P4, ids::fx8P4 };
|
{ "Rate", "Depth", "Feedback", "Unused" },
|
||||||
|
{ "Rate", "Depth", "Feedback", "Stages" },
|
||||||
|
{ "Drive", "Shape", "Tone", "Output" },
|
||||||
|
{ "Low Gain", "Mid Gain", "High Gain", "Mid Freq" },
|
||||||
|
{ "Threshold", "Ratio", "Attack", "Release" },
|
||||||
|
{ "Time", "Feedback", "Damping", "Ping-Pong" },
|
||||||
|
{ "Size", "Damping", "Width", "Predelay" }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -131,7 +138,6 @@ PluginEditor::PluginEditor (SerumAltAudioProcessor& p)
|
|||||||
buildMacroTab();
|
buildMacroTab();
|
||||||
|
|
||||||
setTab (0);
|
setTab (0);
|
||||||
applyUiScale();
|
|
||||||
startTimerHz (30);
|
startTimerHz (30);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,9 +169,11 @@ Knob* PluginEditor::makeKnob (juce::Component* parent, const juce::String& name,
|
|||||||
const juce::String& paramId, std::function<juce::String (float)> fmt,
|
const juce::String& paramId, std::function<juce::String (float)> fmt,
|
||||||
const juce::String& tooltip)
|
const juce::String& tooltip)
|
||||||
{
|
{
|
||||||
auto* k = new Knob (name, std::move (fmt));
|
auto control = std::make_unique<Knob> (name, std::move (fmt));
|
||||||
|
auto* k = control.get();
|
||||||
|
ownedControls.push_back (std::move (control));
|
||||||
parent->addAndMakeVisible (k);
|
parent->addAndMakeVisible (k);
|
||||||
if (paramId.isNotEmpty())
|
if (paramId.isNotEmpty() && processor.parameters.getParameter (paramId) != nullptr)
|
||||||
sliderAttachments.push_back (std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (
|
sliderAttachments.push_back (std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (
|
||||||
processor.parameters, paramId, *k));
|
processor.parameters, paramId, *k));
|
||||||
k->setTooltip (tooltip.isNotEmpty() ? tooltip : name);
|
k->setTooltip (tooltip.isNotEmpty() ? tooltip : name);
|
||||||
@@ -175,11 +183,13 @@ Knob* PluginEditor::makeKnob (juce::Component* parent, const juce::String& name,
|
|||||||
juce::ComboBox* PluginEditor::makeCombo (juce::Component* parent, const juce::String& paramId,
|
juce::ComboBox* PluginEditor::makeCombo (juce::Component* parent, const juce::String& paramId,
|
||||||
const juce::StringArray& items, const juce::String& tooltip)
|
const juce::StringArray& items, const juce::String& tooltip)
|
||||||
{
|
{
|
||||||
auto* c = new juce::ComboBox();
|
auto control = std::make_unique<juce::ComboBox>();
|
||||||
|
auto* c = control.get();
|
||||||
|
ownedControls.push_back (std::move (control));
|
||||||
c->addItemList (items, 1);
|
c->addItemList (items, 1);
|
||||||
c->setSelectedItemIndex (0, juce::dontSendNotification);
|
c->setSelectedItemIndex (0, juce::dontSendNotification);
|
||||||
parent->addAndMakeVisible (c);
|
parent->addAndMakeVisible (c);
|
||||||
if (paramId.isNotEmpty())
|
if (paramId.isNotEmpty() && processor.parameters.getParameter (paramId) != nullptr)
|
||||||
comboAttachments.push_back (std::make_unique<juce::AudioProcessorValueTreeState::ComboBoxAttachment> (
|
comboAttachments.push_back (std::make_unique<juce::AudioProcessorValueTreeState::ComboBoxAttachment> (
|
||||||
processor.parameters, paramId, *c));
|
processor.parameters, paramId, *c));
|
||||||
if (tooltip.isNotEmpty())
|
if (tooltip.isNotEmpty())
|
||||||
@@ -190,7 +200,9 @@ juce::ComboBox* PluginEditor::makeCombo (juce::Component* parent, const juce::St
|
|||||||
ToggleButton* PluginEditor::makeToggle (juce::Component* parent, const juce::String& label,
|
ToggleButton* PluginEditor::makeToggle (juce::Component* parent, const juce::String& label,
|
||||||
const juce::String& paramId, const juce::String& tooltip)
|
const juce::String& paramId, const juce::String& tooltip)
|
||||||
{
|
{
|
||||||
auto* t = new ToggleButton (label);
|
auto control = std::make_unique<ToggleButton> (label);
|
||||||
|
auto* t = control.get();
|
||||||
|
ownedControls.push_back (std::move (control));
|
||||||
parent->addAndMakeVisible (t);
|
parent->addAndMakeVisible (t);
|
||||||
if (paramId.isNotEmpty())
|
if (paramId.isNotEmpty())
|
||||||
t->attach (processor.parameters, paramId);
|
t->attach (processor.parameters, paramId);
|
||||||
@@ -430,11 +442,11 @@ void PluginEditor::buildModTab()
|
|||||||
envDisplays[(size_t) i].setBounds (layout::padding, layout::titleHeight,
|
envDisplays[(size_t) i].setBounds (layout::padding, layout::titleHeight,
|
||||||
colW - 2 * layout::padding, layout::envDisplayHeight);
|
colW - 2 * layout::padding, layout::envDisplayHeight);
|
||||||
|
|
||||||
const char* idsA[4] = { ids::env1A, ids::env2A, ids::env3A, ids::env4A };
|
const auto& idsA = paramIds::envAttack;
|
||||||
const char* idsD[4] = { ids::env1D, ids::env2D, ids::env3D, ids::env4D };
|
const auto& idsD = paramIds::envDecay;
|
||||||
const char* idsS[4] = { ids::env1S, ids::env2S, ids::env3S, ids::env4S };
|
const auto& idsS = paramIds::envSustain;
|
||||||
const char* idsR[4] = { ids::env1R, ids::env2R, ids::env3R, ids::env4R };
|
const auto& idsR = paramIds::envRelease;
|
||||||
const char* idsC[4] = { ids::env1Curve, ids::env2Curve, ids::env3Curve, ids::env4Curve };
|
const auto& idsC = paramIds::envCurve;
|
||||||
|
|
||||||
std::vector<Knob*> knobs = {
|
std::vector<Knob*> knobs = {
|
||||||
makeKnob (&envPanels[(size_t) i], "Attack", idsA[i], formatSeconds, "Attack time (0.5 ms - 12 s)"),
|
makeKnob (&envPanels[(size_t) i], "Attack", idsA[i], formatSeconds, "Attack time (0.5 ms - 12 s)"),
|
||||||
@@ -455,13 +467,13 @@ void PluginEditor::buildModTab()
|
|||||||
lfoPanels[(size_t) i].setBounds (gridX (i, colW, colGap), lfoTop, colW, lfoH);
|
lfoPanels[(size_t) i].setBounds (gridX (i, colW, colGap), lfoTop, colW, lfoH);
|
||||||
modView.addAndMakeVisible (lfoPanels[(size_t) i]);
|
modView.addAndMakeVisible (lfoPanels[(size_t) i]);
|
||||||
|
|
||||||
const char* rate[4] = { ids::lfo1Rate, ids::lfo2Rate, ids::lfo3Rate, ids::lfo4Rate };
|
const auto& rate = paramIds::lfoRate;
|
||||||
const char* sync[4] = { ids::lfo1Sync, ids::lfo2Sync, ids::lfo3Sync, ids::lfo4Sync };
|
const auto& sync = paramIds::lfoSync;
|
||||||
const char* beat[4] = { ids::lfo1Beat, ids::lfo2Beat, ids::lfo3Beat, ids::lfo4Beat };
|
const auto& beat = paramIds::lfoBeat;
|
||||||
const char* shape[4] = { ids::lfo1Shape, ids::lfo2Shape, ids::lfo3Shape, ids::lfo4Shape };
|
const auto& shape = paramIds::lfoShape;
|
||||||
const char* phase[4] = { ids::lfo1Phase, ids::lfo2Phase, ids::lfo3Phase, ids::lfo4Phase };
|
const auto& phase = paramIds::lfoPhase;
|
||||||
const char* fade[4] = { ids::lfo1Fade, ids::lfo2Fade, ids::lfo3Fade, ids::lfo4Fade };
|
const auto& fade = paramIds::lfoFade;
|
||||||
const char* delay[4] = { ids::lfo1Delay, ids::lfo2Delay, ids::lfo3Delay, ids::lfo4Delay };
|
const auto& delay = paramIds::lfoDelay;
|
||||||
|
|
||||||
makeCombo (&lfoPanels[(size_t) i], shape[i], lfoShapes, "LFO shape")->setBounds (layout::padding, comboRowY(), 110, layout::comboHeight);
|
makeCombo (&lfoPanels[(size_t) i], shape[i], lfoShapes, "LFO shape")->setBounds (layout::padding, comboRowY(), 110, layout::comboHeight);
|
||||||
makeToggle (&lfoPanels[(size_t) i], "Sync", sync[i], "Tempo-sync LFO")->setBounds (layout::padding + 110 + layout::gap, layout::titleHeight, layout::toggleWidth, layout::toggleHeight);
|
makeToggle (&lfoPanels[(size_t) i], "Sync", sync[i], "Tempo-sync LFO")->setBounds (layout::padding + 110 + layout::gap, layout::titleHeight, layout::toggleWidth, layout::toggleHeight);
|
||||||
@@ -533,9 +545,12 @@ void PluginEditor::buildModTab()
|
|||||||
const int tid = modTargetCombo.getSelectedItemIndex();
|
const int tid = modTargetCombo.getSelectedItemIndex();
|
||||||
if (tid >= 0 && tid < (int) targetEnums.size())
|
if (tid >= 0 && tid < (int) targetEnums.size())
|
||||||
{
|
{
|
||||||
processor.engine.getMatrix().addConnection (src, targetEnums[(size_t) tid],
|
{
|
||||||
(float) modDepthKnob.getValue(),
|
const juce::ScopedLock lock (processor.engine.getControlLock());
|
||||||
modBipolarToggle.getToggleState());
|
processor.engine.getMatrix().addConnection (src, targetEnums[(size_t) tid],
|
||||||
|
(float) modDepthKnob.getValue(),
|
||||||
|
modBipolarToggle.getToggleState());
|
||||||
|
}
|
||||||
updateModList();
|
updateModList();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -546,7 +561,12 @@ void PluginEditor::buildModTab()
|
|||||||
modRemoveButton.setTooltip ("Remove last modulation connection");
|
modRemoveButton.setTooltip ("Remove last modulation connection");
|
||||||
modRemoveButton.onClick = [this]
|
modRemoveButton.onClick = [this]
|
||||||
{
|
{
|
||||||
processor.engine.getMatrix().removeConnection (processor.engine.getMatrix().size() - 1);
|
{
|
||||||
|
const juce::ScopedLock lock (processor.engine.getControlLock());
|
||||||
|
auto& matrix = processor.engine.getMatrix();
|
||||||
|
if (matrix.size() > 0)
|
||||||
|
matrix.removeConnection (matrix.size() - 1);
|
||||||
|
}
|
||||||
updateModList();
|
updateModList();
|
||||||
};
|
};
|
||||||
matrixPanel.addAndMakeVisible (modRemoveButton);
|
matrixPanel.addAndMakeVisible (modRemoveButton);
|
||||||
@@ -556,7 +576,10 @@ void PluginEditor::buildModTab()
|
|||||||
modClearButton.setTooltip ("Clear all modulation connections");
|
modClearButton.setTooltip ("Clear all modulation connections");
|
||||||
modClearButton.onClick = [this]
|
modClearButton.onClick = [this]
|
||||||
{
|
{
|
||||||
processor.engine.getMatrix().clear();
|
{
|
||||||
|
const juce::ScopedLock lock (processor.engine.getControlLock());
|
||||||
|
processor.engine.getMatrix().clear();
|
||||||
|
}
|
||||||
updateModList();
|
updateModList();
|
||||||
};
|
};
|
||||||
matrixPanel.addAndMakeVisible (modClearButton);
|
matrixPanel.addAndMakeVisible (modClearButton);
|
||||||
@@ -599,11 +622,15 @@ void PluginEditor::buildFxTab()
|
|||||||
fxTypeCombos[(size_t) i]->setBounds (layout::padding, comboRowY(), 160, layout::comboHeight);
|
fxTypeCombos[(size_t) i]->setBounds (layout::padding, comboRowY(), 160, layout::comboHeight);
|
||||||
|
|
||||||
const int upX = layout::padding + 160 + layout::gap;
|
const int upX = layout::padding + 160 + layout::gap;
|
||||||
fxUp[(size_t) i] = new juce::TextButton ("\xe2\x86\x91");
|
auto up = std::make_unique<juce::TextButton> ("\xe2\x86\x91");
|
||||||
|
fxUp[(size_t) i] = up.get();
|
||||||
|
ownedControls.push_back (std::move (up));
|
||||||
fxUp[(size_t) i]->setBounds (upX, comboRowY(), layout::arrowWidth, layout::comboHeight);
|
fxUp[(size_t) i]->setBounds (upX, comboRowY(), layout::arrowWidth, layout::comboHeight);
|
||||||
fxUp[(size_t) i]->setTooltip ("Move effect up");
|
fxUp[(size_t) i]->setTooltip ("Move effect up");
|
||||||
fxPanels[(size_t) i].addAndMakeVisible (fxUp[(size_t) i]);
|
fxPanels[(size_t) i].addAndMakeVisible (fxUp[(size_t) i]);
|
||||||
fxDown[(size_t) i] = new juce::TextButton ("\xe2\x86\x93");
|
auto down = std::make_unique<juce::TextButton> ("\xe2\x86\x93");
|
||||||
|
fxDown[(size_t) i] = down.get();
|
||||||
|
ownedControls.push_back (std::move (down));
|
||||||
fxDown[(size_t) i]->setBounds (upX + layout::arrowWidth + layout::gap, comboRowY(), layout::arrowWidth, layout::comboHeight);
|
fxDown[(size_t) i]->setBounds (upX + layout::arrowWidth + layout::gap, comboRowY(), layout::arrowWidth, layout::comboHeight);
|
||||||
fxDown[(size_t) i]->setTooltip ("Move effect down");
|
fxDown[(size_t) i]->setTooltip ("Move effect down");
|
||||||
fxPanels[(size_t) i].addAndMakeVisible (fxDown[(size_t) i]);
|
fxPanels[(size_t) i].addAndMakeVisible (fxDown[(size_t) i]);
|
||||||
@@ -637,8 +664,8 @@ void PluginEditor::buildMacroTab()
|
|||||||
macroPanels[(size_t) i].setBounds (gridX (i, colW, colGap), layout::margin, colW, macroH);
|
macroPanels[(size_t) i].setBounds (gridX (i, colW, colGap), layout::margin, colW, macroH);
|
||||||
macroView.addAndMakeVisible (macroPanels[(size_t) i]);
|
macroView.addAndMakeVisible (macroPanels[(size_t) i]);
|
||||||
|
|
||||||
const char* ids[4] = { ids::macro1, ids::macro2, ids::macro3, ids::macro4 };
|
const auto& macroIds = paramIds::macros;
|
||||||
macroKnobs[(size_t) i] = makeKnob (¯oPanels[(size_t) i], MacroControls::macroName (i), ids[i], formatPercent,
|
macroKnobs[(size_t) i] = makeKnob (¯oPanels[(size_t) i], MacroControls::macroName (i), macroIds[i], formatPercent,
|
||||||
MacroControls::macroName (i) + " macro (0-100%)");
|
MacroControls::macroName (i) + " macro (0-100%)");
|
||||||
macroKnobs[(size_t) i]->setBounds ((colW - layout::macroKnobSize) / 2,
|
macroKnobs[(size_t) i]->setBounds ((colW - layout::macroKnobSize) / 2,
|
||||||
layout::titleHeight + layout::padding,
|
layout::titleHeight + layout::padding,
|
||||||
@@ -684,8 +711,11 @@ void PluginEditor::buildMacroTab()
|
|||||||
const int tid = macroAssignTarget.getSelectedItemIndex();
|
const int tid = macroAssignTarget.getSelectedItemIndex();
|
||||||
if (tid >= 0 && tid < (int) targetEnums.size())
|
if (tid >= 0 && tid < (int) targetEnums.size())
|
||||||
{
|
{
|
||||||
processor.engine.getMacros().addAssignment (macro, targetEnums[(size_t) tid],
|
{
|
||||||
(float) macroDepthKnob.getValue());
|
const juce::ScopedLock lock (processor.engine.getControlLock());
|
||||||
|
processor.engine.getMacros().addAssignment (macro, targetEnums[(size_t) tid],
|
||||||
|
(float) macroDepthKnob.getValue());
|
||||||
|
}
|
||||||
updateMacroList();
|
updateMacroList();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -696,7 +726,10 @@ void PluginEditor::buildMacroTab()
|
|||||||
macroClearButton.setTooltip ("Clear all macro assignments");
|
macroClearButton.setTooltip ("Clear all macro assignments");
|
||||||
macroClearButton.onClick = [this]
|
macroClearButton.onClick = [this]
|
||||||
{
|
{
|
||||||
processor.engine.getMacros().clear();
|
{
|
||||||
|
const juce::ScopedLock lock (processor.engine.getControlLock());
|
||||||
|
processor.engine.getMacros().clear();
|
||||||
|
}
|
||||||
updateMacroList();
|
updateMacroList();
|
||||||
};
|
};
|
||||||
macroAssignPanel.addAndMakeVisible (macroClearButton);
|
macroAssignPanel.addAndMakeVisible (macroClearButton);
|
||||||
@@ -715,10 +748,15 @@ void PluginEditor::buildMacroTab()
|
|||||||
|
|
||||||
void PluginEditor::swapFxSlots (int a, int b)
|
void PluginEditor::swapFxSlots (int a, int b)
|
||||||
{
|
{
|
||||||
|
if (a < 0 || b < 0 || a >= kNumFxSlots || b >= kNumFxSlots || a == b)
|
||||||
|
return;
|
||||||
|
|
||||||
auto swapParam = [this] (const char* pa, const char* pb)
|
auto swapParam = [this] (const char* pa, const char* pb)
|
||||||
{
|
{
|
||||||
auto* p1 = processor.parameters.getParameter (pa);
|
auto* p1 = processor.parameters.getParameter (pa);
|
||||||
auto* p2 = processor.parameters.getParameter (pb);
|
auto* p2 = processor.parameters.getParameter (pb);
|
||||||
|
if (p1 == nullptr || p2 == nullptr)
|
||||||
|
return;
|
||||||
const float v1 = p1->getValue();
|
const float v1 = p1->getValue();
|
||||||
const float v2 = p2->getValue();
|
const float v2 = p2->getValue();
|
||||||
p1->setValueNotifyingHost (v2);
|
p1->setValueNotifyingHost (v2);
|
||||||
@@ -748,6 +786,7 @@ void PluginEditor::setTab (int index)
|
|||||||
tabMod.setToggleState (currentTab == 2, juce::dontSendNotification);
|
tabMod.setToggleState (currentTab == 2, juce::dontSendNotification);
|
||||||
tabFx.setToggleState (currentTab == 3, juce::dontSendNotification);
|
tabFx.setToggleState (currentTab == 3, juce::dontSendNotification);
|
||||||
tabMacro.setToggleState (currentTab == 4, juce::dontSendNotification);
|
tabMacro.setToggleState (currentTab == 4, juce::dontSendNotification);
|
||||||
|
timerCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginEditor::cycleTab (int delta)
|
void PluginEditor::cycleTab (int delta)
|
||||||
@@ -791,92 +830,139 @@ void PluginEditor::applyUiScale()
|
|||||||
void PluginEditor::updateVisuals()
|
void PluginEditor::updateVisuals()
|
||||||
{
|
{
|
||||||
const auto& apvts = processor.parameters;
|
const auto& apvts = processor.parameters;
|
||||||
auto gv = [&] (const char* id) { return apvts.getRawParameterValue (id)->load(); };
|
auto gv = [&] (const char* id)
|
||||||
|
{
|
||||||
|
if (auto* value = apvts.getRawParameterValue (id))
|
||||||
|
return value->load();
|
||||||
|
return 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
masterDisplay.setValue (formatPercent (gv (ids::master)));
|
masterDisplay.setValue (formatPercent (gv (ids::master)));
|
||||||
|
|
||||||
// Waveforms.
|
// Waveforms.
|
||||||
const int waveA = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (gv (ids::oscAWave) * (kNumWavetables - 1)));
|
if (currentTab == 0)
|
||||||
const int waveB = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (gv (ids::oscBWave) * (kNumWavetables - 1)));
|
{
|
||||||
oscAWave.setWaveIndex (waveA);
|
const int waveA = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (gv (ids::oscAWave) * (kNumWavetables - 1)));
|
||||||
oscAWave.setFramePosition (gv (ids::oscAWtPos));
|
const int waveB = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (gv (ids::oscBWave) * (kNumWavetables - 1)));
|
||||||
oscAWave.setEnabled (gv (ids::oscAOn) > 0.5f);
|
oscAWave.setWaveIndex (waveA);
|
||||||
oscBWave.setWaveIndex (waveB);
|
oscAWave.setFramePosition (gv (ids::oscAWtPos));
|
||||||
oscBWave.setFramePosition (gv (ids::oscBWtPos));
|
oscAWave.setEnabled (gv (ids::oscAOn) > 0.5f);
|
||||||
oscBWave.setEnabled (gv (ids::oscBOn) > 0.5f);
|
oscBWave.setWaveIndex (waveB);
|
||||||
oscAWave.repaint();
|
oscBWave.setFramePosition (gv (ids::oscBWtPos));
|
||||||
oscBWave.repaint();
|
oscBWave.setEnabled (gv (ids::oscBOn) > 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
// Filters.
|
// Filters.
|
||||||
filter1Display.setParams ((int) std::llround (gv (ids::f1Type) * 6.0f), gv (ids::f1Cutoff), gv (ids::f1Res),
|
if (currentTab == 1)
|
||||||
gv (ids::f1Drive), (int) std::llround (gv (ids::f1Slope) * 2.0f));
|
{
|
||||||
filter1Display.setEnabled (gv (ids::f1On) > 0.5f);
|
filter1Display.setParams ((int) std::llround (gv (ids::f1Type) * 6.0f), gv (ids::f1Cutoff), gv (ids::f1Res),
|
||||||
filter2Display.setParams ((int) std::llround (gv (ids::f2Type) * 6.0f), gv (ids::f2Cutoff), gv (ids::f2Res),
|
gv (ids::f1Drive), (int) std::llround (gv (ids::f1Slope) * 2.0f));
|
||||||
gv (ids::f2Drive), (int) std::llround (gv (ids::f2Slope) * 2.0f));
|
filter1Display.setEnabled (gv (ids::f1On) > 0.5f);
|
||||||
filter2Display.setEnabled (gv (ids::f2On) > 0.5f);
|
filter2Display.setParams ((int) std::llround (gv (ids::f2Type) * 6.0f), gv (ids::f2Cutoff), gv (ids::f2Res),
|
||||||
filter1Display.repaint();
|
gv (ids::f2Drive), (int) std::llround (gv (ids::f2Slope) * 2.0f));
|
||||||
filter2Display.repaint();
|
filter2Display.setEnabled (gv (ids::f2On) > 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
// Envelopes.
|
// Envelopes.
|
||||||
const char* a[4] = { ids::env1A, ids::env2A, ids::env3A, ids::env4A };
|
if (currentTab == 2)
|
||||||
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]));
|
const auto& a = paramIds::envAttack;
|
||||||
envDisplays[(size_t) i].repaint();
|
const auto& d = paramIds::envDecay;
|
||||||
|
const auto& s = paramIds::envSustain;
|
||||||
|
const auto& r = paramIds::envRelease;
|
||||||
|
const auto& c = paramIds::envCurve;
|
||||||
|
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]));
|
||||||
}
|
}
|
||||||
|
|
||||||
// LFOs.
|
// LFOs.
|
||||||
const char* lshape[4] = { ids::lfo1Shape, ids::lfo2Shape, ids::lfo3Shape, ids::lfo4Shape };
|
if (currentTab == 2)
|
||||||
for (int i = 0; i < kNumLfos; ++i)
|
|
||||||
{
|
{
|
||||||
lfoDisplays[(size_t) i].setShape ((int) std::llround (gv (lshape[i]) * 6.0f));
|
const auto& lshape = paramIds::lfoShape;
|
||||||
lfoDisplays[(size_t) i].setShapeData (processor.engine.getLfos()[(size_t) i].getShapeData(),
|
const juce::ScopedLock lock (processor.engine.getControlLock());
|
||||||
processor.engine.getLfos()[(size_t) i].getShapeSteps());
|
for (int i = 0; i < kNumLfos; ++i)
|
||||||
lfoDisplays[(size_t) i].repaint();
|
{
|
||||||
|
const auto& lfo = processor.engine.getLfos()[(size_t) i];
|
||||||
|
lfoDisplays[(size_t) i].setShape ((int) std::llround (gv (lshape[i]) * 6.0f));
|
||||||
|
lfoDisplays[(size_t) i].setShapeData (lfo.getShapeData(), lfo.getShapeSteps());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTab == 3)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < kNumFxSlots; ++i)
|
||||||
|
{
|
||||||
|
const int type = juce::jlimit (0, (int) FxType::Count - 1,
|
||||||
|
(int) std::llround (gv (kFxType[i]) * ((int) FxType::Count - 1)));
|
||||||
|
for (int p = 0; p < 4; ++p)
|
||||||
|
{
|
||||||
|
auto* knob = fxKnobs[(size_t) i][(size_t) p + 1];
|
||||||
|
const juce::String name = kFxParamNames[type][p];
|
||||||
|
if (knob->getName() != name)
|
||||||
|
{
|
||||||
|
knob->setName (name);
|
||||||
|
knob->setTooltip (name == "Unused" ? "Not used by this effect"
|
||||||
|
: name + " (normalised 0-100%)");
|
||||||
|
knob->repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RAVE state.
|
// RAVE state.
|
||||||
raveButton.setToggleState (processor.isRaveEnabled());
|
raveButton.setToggleState (processor.isRaveEnabled());
|
||||||
|
|
||||||
|
const int program = processor.getCurrentProgram();
|
||||||
|
if (presetCombo.getSelectedItemIndex() != program)
|
||||||
|
presetCombo.setSelectedItemIndex (program, juce::dontSendNotification);
|
||||||
|
|
||||||
// Scale change.
|
// Scale change.
|
||||||
if (processor.getUiScaleIndex() != currentScaleIndex)
|
if (processor.getUiScaleIndex() != currentScaleIndex)
|
||||||
applyUiScale();
|
applyUiScale();
|
||||||
|
if (scaleCombo.getSelectedItemIndex() != currentScaleIndex)
|
||||||
|
scaleCombo.setSelectedItemIndex (currentScaleIndex, juce::dontSendNotification);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginEditor::updateModList()
|
void PluginEditor::updateModList()
|
||||||
{
|
{
|
||||||
juce::String text;
|
juce::String text;
|
||||||
const auto& cons = processor.engine.getMatrix().connections;
|
{
|
||||||
for (const auto& c : cons)
|
const juce::ScopedLock lock (processor.engine.getControlLock());
|
||||||
text += modSourceName (c.source) + " -> " + modTargetName (c.target)
|
const auto& cons = processor.engine.getMatrix().connections;
|
||||||
+ " [" + juce::String (c.depth, 2) + (c.bipolar ? ", bipolar]" : "]") + "\n";
|
for (const auto& c : cons)
|
||||||
modList.setText (text, false);
|
text += modSourceName (c.source) + " -> " + modTargetName (c.target)
|
||||||
|
+ " [" + juce::String (c.depth, 2) + (c.bipolar ? ", bipolar]" : "]") + "\n";
|
||||||
|
}
|
||||||
|
if (modList.getText() != text)
|
||||||
|
modList.setText (text, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginEditor::updateMacroList()
|
void PluginEditor::updateMacroList()
|
||||||
{
|
{
|
||||||
juce::String text;
|
juce::String text;
|
||||||
for (int m = 0; m < kNumMacros; ++m)
|
|
||||||
{
|
{
|
||||||
text += MacroControls::macroName (m) + ":\n";
|
const juce::ScopedLock lock (processor.engine.getControlLock());
|
||||||
const auto& assigns = processor.engine.getMacros().assignments[(size_t) m];
|
for (int m = 0; m < kNumMacros; ++m)
|
||||||
if (assigns.empty())
|
{
|
||||||
text += " (none)\n";
|
text += MacroControls::macroName (m) + ":\n";
|
||||||
for (const auto& a : assigns)
|
const auto& assigns = processor.engine.getMacros().assignments[(size_t) m];
|
||||||
text += " " + modTargetName (a.target) + " [" + juce::String (a.depth, 2) + "]\n";
|
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);
|
if (macroList.getText() != text)
|
||||||
|
macroList.setText (text, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginEditor::timerCallback()
|
void PluginEditor::timerCallback()
|
||||||
{
|
{
|
||||||
updateVisuals();
|
updateVisuals();
|
||||||
updateModList();
|
if (currentTab == 2)
|
||||||
updateMacroList();
|
updateModList();
|
||||||
|
else if (currentTab == 4)
|
||||||
|
updateMacroList();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace serum
|
} // namespace serum
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ private:
|
|||||||
juce::TextButton macroAssignButton, macroClearButton;
|
juce::TextButton macroAssignButton, macroClearButton;
|
||||||
juce::TextEditor macroList;
|
juce::TextEditor macroList;
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<juce::Component>> ownedControls;
|
||||||
|
|
||||||
// --- attachments ---
|
// --- attachments ---
|
||||||
std::vector<std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment>> sliderAttachments;
|
std::vector<std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment>> sliderAttachments;
|
||||||
std::vector<std::unique_ptr<juce::AudioProcessorValueTreeState::ComboBoxAttachment>> comboAttachments;
|
std::vector<std::unique_ptr<juce::AudioProcessorValueTreeState::ComboBoxAttachment>> comboAttachments;
|
||||||
|
|||||||
Reference in New Issue
Block a user