Files
biggy 8364737d1c 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.
2026-09-09 14:33:41 +02:00

83 lines
2.9 KiB
C++

#include "FilterDisplay.h"
namespace serum
{
float FilterDisplay::magnitude (float freqHz, float cutoffHz, float res, int type)
{
const float w = (cutoffHz > 1.0f) ? freqHz / cutoffHz : 1.0f;
const float q = 0.6f + res * 14.0f;
const float w2 = w * w;
const float denom = std::sqrt ((1.0f - w2) * (1.0f - w2) + (w / q) * (w / q));
switch ((FilterModel) type)
{
case FilterModel::LadderHP: return (denom > 1e-6f) ? w2 / denom : 0.0f;
case FilterModel::LadderBP: return (denom > 1e-6f) ? (w / q) / denom : 0.0f;
case FilterModel::Screamer: return (denom > 1e-6f) ? (w / (q * 0.7f)) / denom : 0.0f;
case FilterModel::Comb:
return 0.5f + 0.5f * std::cos (2.0f * juce::MathConstants<float>::pi * freqHz / cutoffHz);
case FilterModel::Formant:
{
float m = 0.0f;
const float fc[3] = { cutoffHz * 0.5f, cutoffHz * 1.6f, cutoffHz * 3.2f };
for (int i = 0; i < 3; ++i)
{
const float ww = freqHz / fc[i];
const float dd = std::sqrt ((1.0f - ww * ww) * (1.0f - ww * ww) + (ww / q) * (ww / q));
m += (ww / q) / (dd + 1e-6f);
}
return m / 3.0f;
}
case FilterModel::Diode:
default:
return (denom > 1e-6f) ? 1.0f / denom : 0.0f;
}
}
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)
{
const auto b = getLocalBounds().toFloat();
g.setColour (juce::Colours::black.withAlpha (0.4f));
g.fillRoundedRectangle (b, 4.0f);
g.setColour (theme::outline);
g.drawRoundedRectangle (b.reduced (0.5f), 4.0f, 1.0f);
if (! enabled)
return;
const float cutoffHz = maps::cutoffToHz (cutoff);
const int n = 300;
juce::Path path;
path.startNewSubPath (b.getX() + 4.0f, b.getBottom() - 4.0f);
for (int i = 0; i <= n; ++i)
{
const float logF = std::log10 (20.0f) + ((float) i / (float) n) * (std::log10 (20000.0f) - std::log10 (20.0f));
const float freq = std::pow (10.0f, logF);
const float mag = magnitude (freq, cutoffHz, res, type);
const float db = juce::Decibels::gainToDecibels (mag + 1e-5f);
const float x = b.getX() + 4.0f + ((float) i / (float) n) * (b.getWidth() - 8.0f);
const float y = juce::jmap (db, -30.0f, 12.0f, b.getBottom() - 4.0f, b.getY() + 4.0f);
path.lineTo (x, y);
}
path.lineTo (b.getRight() - 4.0f, b.getBottom() - 4.0f);
path.closeSubPath();
g.setColour (theme::accent.withAlpha (0.25f));
g.fillPath (path);
g.setColour (theme::accent);
g.strokePath (path, juce::PathStrokeType (1.5f));
}
} // namespace serum