Files
serumalt/Source/GUI/FilterDisplay.cpp
T
biggy e4cea300db chore: land janitor round 1 and graft integration
Round 1 findings and fixes, all previously uncommitted:
- FXProcessor per-slot DSP history repair (HEAD does not compile without it)
- SynthVoice::getModulationValue, editor control-lock guard
- ExactCompare.h naming the deliberate exact float comparisons
- KnobLookAndFeel replacing the never-installed SerumLookAndFeel
- 55 first-party compiler warnings to 0
- AGENT.md renamed to AGENTS.md across Source/
- graft repository context graph and its hooks
- JANITOR_TASKS.md and UI_UX_AUDIT.md records
2026-10-06 08:50:45 +02:00

90 lines
3.2 KiB
C++

#include "FilterDisplay.h"
#include "../ExactCompare.h"
namespace serum
{
// Schematic frequency-response preview for this view only. It does not model the
// real DSP in Source/Filter.cpp, nor the drive, the slope or modulation, so it is
// not expected to match the nonlinear filter numerically.
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::LadderLP:
case FilterModel::Diode:
case FilterModel::Count:
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 && ! changed (cutoff, c) && ! changed (res, r)
&& ! changed (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