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:
2026-09-09 14:33:41 +02:00
parent 3be1e42242
commit 8364737d1c
11 changed files with 215 additions and 115 deletions
+2 -2
View File
@@ -12,8 +12,8 @@ namespace serum
class Display : public juce::Component
{
public:
void setTitle (const juce::String& t) { title = t; repaint(); }
void setValue (const juce::String& v) { value = v; repaint(); }
void setTitle (const juce::String& t) { if (title != t) { title = t; repaint(); } }
void setValue (const juce::String& v) { if (value != v) { value = v; repaint(); } }
void paint (juce::Graphics& g) override;
+7 -5
View File
@@ -5,7 +5,10 @@ namespace serum
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;
repaint();
}
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 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 dSec = maps::toSeconds (decay);
const float rSec = maps::toSeconds (release);
@@ -36,7 +39,6 @@ void EnvelopeDisplay::paint (juce::Graphics& g)
juce::Path path;
path.startNewSubPath (left, bottom);
path.lineTo (left, top);
// Attack (curve-shaped).
const int steps = 48;
@@ -44,7 +46,7 @@ void EnvelopeDisplay::paint (juce::Graphics& g)
for (int i = 0; i <= steps; ++i)
{
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);
}
@@ -53,7 +55,7 @@ void EnvelopeDisplay::paint (juce::Graphics& g)
for (int i = 0; i <= steps; ++i)
{
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 (decX, sustainY);
@@ -64,7 +66,7 @@ void EnvelopeDisplay::paint (juce::Graphics& g)
for (int i = 0; i <= steps; ++i)
{
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);
}
+3
View File
@@ -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)
{
if (type == t && cutoff == c && res == r && drive == d && slope == s)
return;
type = t; cutoff = c; res = r; drive = d; slope = s;
repaint();
}
void FilterDisplay::paint (juce::Graphics& g)
+5 -3
View File
@@ -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:
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 setEnabled (bool e) { enabled = e; }
void setEnabled (bool e) { if (enabled != e) { enabled = e; repaint(); } }
void paint (juce::Graphics& g) override;
+10 -4
View File
@@ -5,10 +5,16 @@ namespace serum
void LFODisplay::setShapeData (const std::vector<float>& data, int s)
{
steps = juce::jlimit (2, 64, s);
shapeData = data;
if ((int) shapeData.size() < 64)
shapeData.resize (64, 0.0f);
const int newSteps = juce::jlimit (2, 64, s);
const size_t dataSize = std::min (data.size(), size_t (64));
if (steps == newSteps && shapeData.size() == 64
&& 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();
}
+1 -1
View File
@@ -13,7 +13,7 @@ namespace serum
class LFODisplay : public juce::Component
{
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 setOnShapeEdited (std::function<void (const std::vector<float>&, int)> cb) { onEdited = std::move (cb); }
+5 -5
View File
@@ -10,12 +10,12 @@ ToggleButton::ToggleButton (const juce::String& lbl) : label (lbl)
void ToggleButton::attach (juce::AudioProcessorValueTreeState& apvts, const juce::String& paramId)
{
param = apvts.getParameter (paramId);
if (param != nullptr)
attachment.reset();
if (auto* param = apvts.getParameter (paramId))
{
state = param->getValue() > 0.5f;
attachment = std::make_unique<juce::ParameterAttachment> (*param,
[this] (float newValue) { setToggleState (newValue > 0.5f); });
attachment->sendInitialUpdate();
}
}
@@ -60,9 +60,9 @@ void ToggleButton::mouseDown (const juce::MouseEvent&)
{
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);
-1
View File
@@ -35,7 +35,6 @@ public:
private:
bool state = false;
juce::String label;
juce::RangedAudioParameter* param = nullptr;
std::unique_ptr<juce::ParameterAttachment> attachment;
std::function<void (bool)> onClick;
juce::Colour onColour = theme::accent;
+4 -4
View File
@@ -13,10 +13,10 @@ namespace serum
class WaveformDisplay : public juce::Component
{
public:
void setWavetables (const WavetableLibrary* lib) { wtLib = lib; }
void setWaveIndex (int index) { wave = index; }
void setFramePosition (float pos) { wtPos = pos; }
void setEnabled (bool e) { enabled = e; }
void setWavetables (const WavetableLibrary* lib) { if (wtLib != lib) { wtLib = lib; repaint(); } }
void setWaveIndex (int index) { if (wave != index) { wave = index; repaint(); } }
void setFramePosition (float pos) { if (wtPos != pos) { wtPos = pos; repaint(); } }
void setEnabled (bool e) { if (enabled != e) { enabled = e; repaint(); } }
void paint (juce::Graphics& g) override;