This commit is contained in:
Armin 2026-08-15 15:36:28 +02:00
commit 42cf8432bf
57 changed files with 5782 additions and 0 deletions

View file

@ -0,0 +1,354 @@
#include "AmbivalenceUI.h"
#include "../PluginProcessor.h"
// --- AmbivalenceLookAndFeel ---------------------------------------------
AmbivalenceLookAndFeel::AmbivalenceLookAndFeel()
{
setColour(juce::Slider::backgroundColourId, AmbivalenceColors::ArcTrack);
setColour(juce::Slider::thumbColourId, AmbivalenceColors::Accent);
setColour(juce::Slider::trackColourId, AmbivalenceColors::ArcFill);
setColour(juce::Label::textColourId, AmbivalenceColors::TextSecondary);
setColour(juce::ComboBox::backgroundColourId, AmbivalenceColors::Surface);
setColour(juce::ComboBox::textColourId, AmbivalenceColors::TextPrimary);
setColour(juce::ComboBox::outlineColourId, AmbivalenceColors::Border);
mainFont = juce::Font(juce::FontOptions("Helvetica Neue", 11.f, juce::Font::plain));
}
void AmbivalenceLookAndFeel::drawRotarySlider(juce::Graphics& g,
int x, int y, int w, int h,
float sliderPos, float startAngle, float endAngle, juce::Slider&)
{
auto b = juce::Rectangle<float>((float)x, (float)y, (float)w, (float)h).reduced(4.f);
float cx = b.getCentreX(), cy = b.getCentreY();
float r = juce::jmin(b.getWidth(), b.getHeight()) * 0.45f;
float th = r * 0.22f;
juce::Path track;
track.addCentredArc(cx, cy, r, r, 0.f, startAngle, endAngle, true);
g.setColour(AmbivalenceColors::ArcTrack);
g.strokePath(track, juce::PathStrokeType(th,
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
float angle = startAngle + sliderPos * (endAngle - startAngle);
juce::Path fill;
fill.addCentredArc(cx, cy, r, r, 0.f, startAngle, angle, true);
juce::ColourGradient grad(AmbivalenceColors::AccentBlue, cx - r, cy,
AmbivalenceColors::Accent, cx + r, cy, false);
g.setGradientFill(grad);
g.strokePath(fill, juce::PathStrokeType(th,
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
g.setColour(AmbivalenceColors::Panel);
g.fillEllipse(cx - r * 0.28f, cy - r * 0.28f, r * 0.56f, r * 0.56f);
float ix = cx + r * 0.6f * std::sin(angle);
float iy = cy - r * 0.6f * std::cos(angle);
g.setColour(AmbivalenceColors::TextPrimary);
g.drawLine(cx, cy, ix, iy, 2.f);
}
void AmbivalenceLookAndFeel::drawLinearSlider(juce::Graphics& g,
int x, int y, int w, int h,
float sliderPos, float, float, juce::Slider::SliderStyle, juce::Slider&)
{
auto b = juce::Rectangle<int>(x, y, w, h).toFloat();
float ty = b.getCentreY() - 2.f;
g.setColour(AmbivalenceColors::ArcTrack);
g.fillRoundedRectangle(b.getX(), ty, b.getWidth(), 4.f, 2.f);
g.setColour(AmbivalenceColors::Accent);
g.fillRoundedRectangle(b.getX(), ty, sliderPos - b.getX(), 4.f, 2.f);
float r = 7.f;
g.setColour(AmbivalenceColors::TextPrimary);
g.fillEllipse(sliderPos - r, b.getCentreY() - r, r * 2.f, r * 2.f);
}
void AmbivalenceLookAndFeel::drawComboBox(juce::Graphics& g,
int w, int h, bool isDown, int, int, int, int, juce::ComboBox&)
{
auto b = juce::Rectangle<int>(0, 0, w, h).toFloat();
g.setColour(isDown ? AmbivalenceColors::Panel : AmbivalenceColors::Surface);
g.fillRoundedRectangle(b, 3.f);
g.setColour(AmbivalenceColors::Border);
g.drawRoundedRectangle(b.reduced(0.5f), 3.f, 1.f);
juce::Path arrow;
arrow.addTriangle(w - 16.f, h * 0.5f - 3.f,
w - 8.f, h * 0.5f - 3.f,
w - 12.f, h * 0.5f + 3.f);
g.setColour(AmbivalenceColors::TextSecondary);
g.fillPath(arrow);
}
void AmbivalenceLookAndFeel::positionComboBoxText(juce::ComboBox& box, juce::Label& label) {
label.setBounds(6, 1, box.getWidth() - 22, box.getHeight() - 2);
label.setFont(getComboBoxFont(box));
}
juce::Font AmbivalenceLookAndFeel::getLabelFont(juce::Label&) { return mainFont.withHeight(10.f); }
juce::Font AmbivalenceLookAndFeel::getComboBoxFont(juce::ComboBox&) { return mainFont.withHeight(11.f); }
void AmbivalenceLookAndFeel::drawGroupComponentOutline(juce::Graphics& g,
int w, int h, const juce::String& text,
const juce::Justification&, juce::GroupComponent&)
{
float textH = 12.f, indent = 8.f, yOff = textH * 0.5f;
juce::Path p;
p.startNewSubPath(indent + 4.f, yOff); p.lineTo(indent, yOff);
p.lineTo(indent, (float)h - 1.f);
p.lineTo((float)w - indent, (float)h - 1.f);
p.lineTo((float)w - indent, yOff);
// * redraw after changes
juce::GlyphArrangement ga;
ga.addLineOfText(mainFont.withHeight(textH), text, 0.f, 0.f);
float tw = ga.getBoundingBox(0, -1, true).getWidth() + 6.f;
p.lineTo(indent + 14.f + tw, yOff);
g.setColour(AmbivalenceColors::Border);
g.strokePath(p, juce::PathStrokeType(1.f));
g.setColour(AmbivalenceColors::TextSecondary);
g.setFont(mainFont.withHeight(textH).boldened());
g.drawText(text, (int)(indent + 14.f), 0, (int)tw, (int)textH,
juce::Justification::centredLeft);
}
// --- RT60Visualizer --------------------------------------------------
RT60Visualizer::RT60Visualizer() {
displayRT60.fill(1.0f);
startTimerHz(30);
}
RT60Visualizer::~RT60Visualizer() { stopTimer(); }
void RT60Visualizer::timerCallback() {
if (!processor) return;
auto live = processor->getRT60ForDisplay();
for (int i = 0; i < FDNReverb::NUM_BANDS; ++i)
displayRT60[i] += 0.25f * (live[i] - displayRT60[i]);
// * dynamic Y axis above : current maximum RT60 value x 1.3 smoothly
float maxVal = *std::max_element(displayRT60.begin(), displayRT60.end());
float targetMax = std::max(MAX_RT60_DISPLAY_FLOOR, maxVal * 1.3f);
// exponential ( rise quickly , fall gentle -> frequently )
float smoothFactor = (targetMax > dynamicMaxRT60) ? 0.15f : 0.03f;
dynamicMaxRT60 += smoothFactor * (targetMax - dynamicMaxRT60);
repaint();
}
void RT60Visualizer::paint(juce::Graphics& g)
{
auto b = getLocalBounds().toFloat().reduced(2.f);
float W = b.getWidth(), H = b.getHeight();
float x0 = b.getX(), y0 = b.getY();
g.setColour(AmbivalenceColors::Surface);
g.fillRoundedRectangle(b, 4.f);
g.setColour(AmbivalenceColors::Border);
g.drawRoundedRectangle(b.reduced(0.5f), 4.f, 1.f);
// * dynamic Y axis
float logMin = std::log10(MIN_RT60_DISPLAY);
float logMax = std::log10(dynamicMaxRT60);
// grid value dynamic Y axis
// fixed value dynamicMaxRT60 below drawing
static constexpr float kAllGridVals[] = {
0.1f, 0.3f, 0.5f, 1.0f, 2.0f, 4.0f,
8.0f, 12.0f, 16.0f, 20.0f
};
// grid
g.setColour(AmbivalenceColors::Separator);
for (float v : kAllGridVals) {
if (v > dynamicMaxRT60 * 1.05f) break;
float ny = 1.f - (std::log10(v) - logMin) / (logMax - logMin);
g.drawHorizontalLine((int)(y0 + ny * H), x0 + 36.f, x0 + W - 4.f);
}
// frequency label (X axis )
g.setFont(8.5f);
g.setColour(AmbivalenceColors::TextSecondary);
static const char* fLbls[] = {
"31","63","125","250","500","1k","2k","4k","8k","16k"
};
for (int i = 0; i < FDNReverb::NUM_BANDS; ++i) {
float px = x0 + 36.f + (float)i / (FDNReverb::NUM_BANDS - 1) * (W - 40.f);
g.drawText(fLbls[i], (int)(px - 12.f), (int)(y0 + H - 14.f),
24, 13, juce::Justification::centred);
}
// seconds label (Y axis ) - dynamic
for (float v : kAllGridVals) {
if (v > dynamicMaxRT60 * 1.05f) break;
float ny = 1.f - (std::log10(v) - logMin) / (logMax - logMin);
float py = y0 + ny * H;
juce::String lbl = (v < 1.f)
? juce::String(v, 1) + "s"
: (v < 10.f ? juce::String(v, 1) : juce::String((int)v)) + "s";
g.drawText(lbl, (int)(x0 + 2.f), (int)(py - 7.f), 32, 14,
juce::Justification::centredLeft);
}
auto plotCurve = [&](const std::array<float, FDNReverb::NUM_BANDS>& rt60,
juce::Colour col, float thick)
{
juce::Path path;
bool first = true;
for (int i = 0; i < FDNReverb::NUM_BANDS; ++i) {
float v = std::clamp(rt60[i], MIN_RT60_DISPLAY, dynamicMaxRT60);
float ny = 1.f - (std::log10(v) - logMin) / (logMax - logMin);
float px = x0 + 36.f + (float)i / (FDNReverb::NUM_BANDS - 1) * (W - 40.f);
float py = y0 + ny * H;
if (first) { path.startNewSubPath(px, py); first = false; }
else path.lineTo(px, py);
}
g.setColour(col);
g.strokePath(path, juce::PathStrokeType(thick,
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
for (int i = 0; i < FDNReverb::NUM_BANDS; ++i) {
float v = std::clamp(rt60[i], MIN_RT60_DISPLAY, dynamicMaxRT60);
float ny = 1.f - (std::log10(v) - logMin) / (logMax - logMin);
float px = x0 + 36.f + (float)i / (FDNReverb::NUM_BANDS - 1) * (W - 40.f);
float py = y0 + ny * H;
g.fillEllipse(px - 3.f, py - 3.f, 6.f, 6.f);
}
};
// source preset curve (from the selected algorithm)
if (processor) {
int algo = (int)*processor->apvts.getRawParameterValue("algorithm");
auto& preset = *FDNReverb::ALL_PRESETS[
juce::jlimit(0, FDNReverb::NUM_ALGORITHMS - 1, algo)];
plotCurve(preset.acoustics.rt60,
AmbivalenceColors::TextSecondary.withAlpha(0.5f), 1.f);
}
// current RT60 curve (measured)
plotCurve(displayRT60, AmbivalenceColors::Accent, 2.f);
// top right title
g.setColour(AmbivalenceColors::TextSecondary);
g.setFont(9.f);
g.drawText("RT60 (s) per band",
(int)x0 + 36, (int)y0 + 3, (int)W - 40, 12,
juce::Justification::right);
}
// --- VUMeter ---------------------------------------------------------
VUMeter::VUMeter(const juce::String& lbl, Side s) : label(lbl), side(s) {}
void VUMeter::paint(juce::Graphics& g)
{
auto b = getLocalBounds().toFloat().reduced(1.f);
g.setColour(AmbivalenceColors::Surface);
g.fillRoundedRectangle(b, 3.f);
float bx = b.getX() + 22.f, bw = b.getWidth() - 22.f;
auto bar = [&](float y, float level) {
float n = juce::jlimit(0.f, 1.f, juce::jmap(
juce::Decibels::gainToDecibels(level + 1e-9f), -60.f, 0.f, 0.f, 1.f));
g.setColour(AmbivalenceColors::ArcTrack);
g.fillRoundedRectangle(bx, y, bw, 7.f, 2.f);
juce::ColourGradient gr(AmbivalenceColors::AccentBlue, bx, y,
AmbivalenceColors::Accent, bx + bw, y, false);
g.setGradientFill(gr);
g.fillRoundedRectangle(bx, y, bw * n, 7.f, 2.f);
};
bar(b.getY() + 2.f, levelL);
bar(b.getY() + 11.f, levelR);
g.setColour(AmbivalenceColors::TextSecondary);
g.setFont(8.f);
g.drawText(label, (int)b.getX(), (int)b.getY(), 20, (int)b.getHeight(),
juce::Justification::centredLeft);
}
// --- ArcKnob ---------------------------------------------------------
void ArcKnob::build(juce::AudioProcessorValueTreeState& apvts,
const juce::String& paramID,
const juce::String& labelText,
juce::Component* parent,
AmbivalenceLookAndFeel& laf)
{
slider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag);
slider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 62, 14);
slider.setLookAndFeel(&laf);
slider.setColour(juce::Slider::textBoxTextColourId,
AmbivalenceColors::TextSecondary);
slider.setColour(juce::Slider::textBoxOutlineColourId,
juce::Colours::transparentBlack);
parent->addAndMakeVisible(slider);
label.setText(labelText, juce::dontSendNotification);
label.setJustificationType(juce::Justification::centred);
label.setFont(juce::Font(juce::FontOptions(9.f)));
label.setColour(juce::Label::textColourId, AmbivalenceColors::TextSecondary);
parent->addAndMakeVisible(label);
// * after changes
attachment.reset(
new juce::AudioProcessorValueTreeState::SliderAttachment(
apvts, paramID, slider));
}
// --- AlgorithmSelector -----------------------------------------------
AlgorithmSelector::AlgorithmSelector(juce::AudioProcessorValueTreeState& a)
: apvts(a)
{
static const char* names[] = {
"ROOM1","ROOM2","HALL1","HALL2","PLATE","SPRING","GOLDFOIL"
};
for (int i = 0; i < FDNReverb::NUM_ALGORITHMS; ++i) {
buttons[i].setButtonText(names[i]);
addAndMakeVisible(buttons[i]);
int idx = i;
buttons[i].onClick = [this, idx] {
if (auto* param = apvts.getParameter("algorithm"))
param->setValueNotifyingHost(
(float)idx / (float)(FDNReverb::NUM_ALGORITHMS - 1));
};
}
apvts.addParameterListener("algorithm", this);
currentAlgo = juce::roundToInt(
*apvts.getRawParameterValue("algorithm")
* (FDNReverb::NUM_ALGORITHMS - 1));
}
AlgorithmSelector::~AlgorithmSelector() {
apvts.removeParameterListener("algorithm", this);
}
void AlgorithmSelector::parameterChanged(const juce::String&, float newVal) {
int newAlgo = juce::jlimit(0, FDNReverb::NUM_ALGORITHMS - 1,
juce::roundToInt(newVal));
juce::MessageManager::callAsync([this, newAlgo] {
currentAlgo = newAlgo;
updateButtonColors();
});
}
void AlgorithmSelector::updateButtonColors() {
for (int i = 0; i < FDNReverb::NUM_ALGORITHMS; ++i) {
bool on = (i == currentAlgo);
buttons[i].setColour(juce::TextButton::buttonColourId,
on ? AmbivalenceColors::Accent : AmbivalenceColors::Surface);
buttons[i].setColour(juce::TextButton::textColourOffId,
on ? AmbivalenceColors::Background : AmbivalenceColors::TextSecondary);
buttons[i].repaint();
}
}
void AlgorithmSelector::paint(juce::Graphics& g) {
g.setColour(AmbivalenceColors::Surface);
g.fillRoundedRectangle(getLocalBounds().toFloat(), 4.f);
}
void AlgorithmSelector::resized() {
auto area = getLocalBounds().reduced(2);
int btnW = area.getWidth() / FDNReverb::NUM_ALGORITHMS;
for (int i = 0; i < FDNReverb::NUM_ALGORITHMS; ++i)
buttons[i].setBounds(area.getX() + i * btnW, area.getY(),
btnW - 1, area.getHeight());
updateButtonColors();
}