UI changes, layout redefinition

This commit is contained in:
Armin 2026-08-15 18:31:51 +02:00
commit 1d9ba772fe
12 changed files with 536 additions and 528 deletions

View file

@ -32,9 +32,7 @@ void AmbivalenceLookAndFeel::drawRotarySlider(juce::Graphics& g,
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.setColour(AmbivalenceColors::ArcFill);
g.strokePath(fill, juce::PathStrokeType(th,
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
@ -83,8 +81,8 @@ void AmbivalenceLookAndFeel::positionComboBoxText(juce::ComboBox& box, juce::Lab
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); }
juce::Font AmbivalenceLookAndFeel::getLabelFont(juce::Label& label) { return label.getFont(); }
juce::Font AmbivalenceLookAndFeel::getComboBoxFont(juce::ComboBox&) { return mainFont.withHeight(11.f * zoomScale); }
void AmbivalenceLookAndFeel::drawGroupComponentOutline(juce::Graphics& g,
int w, int h, const juce::String& text,
@ -111,130 +109,6 @@ void AmbivalenceLookAndFeel::drawGroupComponentOutline(juce::Graphics& g,
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) {}
@ -244,24 +118,33 @@ void VUMeter::paint(juce::Graphics& g)
g.setColour(AmbivalenceColors::Surface);
g.fillRoundedRectangle(b, 3.f);
float bx = b.getX() + 22.f, bw = b.getWidth() - 22.f;
const float inset = 8.f;
const float labelW = 20.f;
const float bx = b.getX() + inset + labelW + 2.f;
const float bw = b.getWidth() - (bx - b.getX()) - inset;
const float barH = 7.f;
const float gap = 2.f;
const float blockH = barH * 2.f + gap;
const float startY = b.getY() + (b.getHeight() - blockH) * 0.5f;
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);
g.fillRoundedRectangle(bx, y, bw, barH, 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);
g.fillRoundedRectangle(bx, y, bw * n, barH, 2.f);
};
bar(b.getY() + 2.f, levelL);
bar(b.getY() + 11.f, levelR);
bar(startY, levelL);
bar(startY + barH + gap, 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);
g.setFont(8.f * zoomScale);
g.drawText(label, (int)(b.getX() + inset), (int)b.getY(), (int)labelW,
(int)b.getHeight(), juce::Justification::centredLeft);
}
// --- ArcKnob ---------------------------------------------------------
@ -290,65 +173,4 @@ void ArcKnob::build(juce::AudioProcessorValueTreeState& apvts,
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();
}

View file

@ -1,22 +1,21 @@
#pragma once
#include <JuceHeader.h>
#include "../AlgorithmPresets.h"
class FDNReverbAudioProcessor;
// --- Ambivalence Design System ------------------------------------------
namespace AmbivalenceColors {
const juce::Colour Background{ 0xFF1A1A1A };
const juce::Colour Surface{ 0xFF242424 };
const juce::Colour Panel{ 0xFF2C2C2C };
const juce::Colour Border{ 0xFF3C3C3C };
const juce::Colour Accent{ 0xFFFF6B00 };
const juce::Colour AccentBlue{ 0xFF4090FF };
const juce::Colour TextPrimary{ 0xFFE8E8E8 };
const juce::Colour TextSecondary{ 0xFF888888 };
const juce::Colour ArcTrack{ 0xFF3A3A3A };
const juce::Colour ArcFill{ 0xFFFF6B00 };
const juce::Colour Separator{ 0xFF383838 };
const juce::Colour Background{ 0xFF181819 }; // palette 0
const juce::Colour Surface{ 0xFF2C2E34 }; // theme background
const juce::Colour Panel{ 0xFF2C2E34 }; // theme background
const juce::Colour Border{ 0xFF3B3E48 }; // selection background
const juce::Colour Accent{ 0xFFF39660 }; // palette 6/14
const juce::Colour AccentBlue{ 0xFF76CCE0 }; // palette 4/12
const juce::Colour TextPrimary{ 0xFFE2E2E3 }; // foreground
const juce::Colour TextSecondary{ 0xFF7F8490 }; // palette 8
const juce::Colour ArcTrack{ 0xFF3B3E48 }; // selection background
const juce::Colour ArcFill{ 0xFF76CCE0 }; // plain blue arc
const juce::Colour Separator{ 0xFF2C2E34 }; // theme background
}
// --- Ambivalence LookAndFeel --------------------------------------------
@ -38,32 +37,12 @@ public:
void drawGroupComponentOutline(juce::Graphics&, int w, int h,
const juce::String&, const juce::Justification&,
juce::GroupComponent&) override;
float zoomScale{ 1.0f }; // pushed by the editor so fonts scale with zoom
private:
juce::Font mainFont;
};
// --- RT60 Visualizer -------------------------------------------------
class RT60Visualizer : public juce::Component, private juce::Timer
{
public:
RT60Visualizer();
~RT60Visualizer() override;
void setProcessor(FDNReverbAudioProcessor* p) { processor = p; }
void paint(juce::Graphics&) override;
private:
void timerCallback() override;
FDNReverbAudioProcessor* processor{ nullptr };
std::array<float, FDNReverb::NUM_BANDS> displayRT60;
static constexpr float MIN_RT60_DISPLAY = 0.05f;
static constexpr float MAX_RT60_DISPLAY_FLOOR = 4.0f; // Y axis above value
// * dynamic Y axis above : effectiveRT60 maximum value smoothly
float dynamicMaxRT60{ MAX_RT60_DISPLAY_FLOOR };
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(RT60Visualizer)
};
// --- VU Meter --------------------------------------------------------
class VUMeter : public juce::Component
{
@ -72,10 +51,12 @@ public:
VUMeter(const juce::String& label, Side side);
void paint(juce::Graphics&) override;
void setLevels(float l, float r) noexcept { levelL = l; levelR = r; }
void setZoomScale(float s) noexcept { zoomScale = s; repaint(); }
private:
juce::String label;
Side side;
float levelL{ 0.f }, levelR{ 0.f };
float zoomScale{ 1.0f };
};
// --- Labelled Arc Knob -----------------------------------------------
@ -89,22 +70,4 @@ struct ArcKnob {
const juce::String& labelText,
juce::Component* parent,
AmbivalenceLookAndFeel& laf);
};
// --- Algorithm Selector ----------------------------------------------
class AlgorithmSelector : public juce::Component,
private juce::AudioProcessorValueTreeState::Listener
{
public:
AlgorithmSelector(juce::AudioProcessorValueTreeState& apvts);
~AlgorithmSelector() override;
void paint(juce::Graphics&) override;
void resized() override;
private:
void parameterChanged(const juce::String&, float) override;
void updateButtonColors();
std::array<juce::TextButton, FDNReverb::NUM_ALGORITHMS> buttons;
juce::AudioProcessorValueTreeState& apvts;
int currentAlgo{ 0 };
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AlgorithmSelector)
};

View file

@ -84,7 +84,7 @@ void DecayCurveViz::paint(juce::Graphics& g)
// --- ER zone background ---
{
const float erZoneW = plotW * splitRatio;
g.setColour(juce::Colour(0xFF1A2535));
g.setColour(AmbivalenceColors::Surface);
g.fillRect(plotX, plotY, erZoneW, plotH);
}
@ -122,7 +122,7 @@ void DecayCurveViz::paint(juce::Graphics& g)
}
// --- axis label ---
g.setFont(juce::Font(juce::FontOptions(8.5f)));
g.setFont(juce::Font(juce::FontOptions(8.5f * zoomScale)));
g.setColour(AmbivalenceColors::TextSecondary.withAlpha(0.6f));
for (float db = 0.0f; db >= -60.0f; db -= 20.0f) {
@ -197,7 +197,7 @@ void DecayCurveViz::paint(juce::Graphics& g)
// : wide 5px + marker + envelope fill
// -------------------------------------------------------------------------
if (!cachedERBypassed && cachedERTapCount > 0) {
const juce::Colour blueColor = juce::Colour::fromRGB(80, 160, 230);
const juce::Colour blueColor = AmbivalenceColors::AccentBlue;
// -- ER envelope fill region --
if (cachedERTapCount >= 2) {
@ -300,9 +300,9 @@ void DecayCurveViz::paint(juce::Graphics& g)
// --- zone label ---
g.setFont(juce::Font(juce::FontOptions(
"Helvetica Neue", 8.0f, juce::Font::bold)));
"Helvetica Neue", 8.0f * zoomScale, juce::Font::bold)));
g.setColour(juce::Colour::fromRGB(80, 160, 230).withAlpha(0.9f));
g.setColour(AmbivalenceColors::AccentBlue.withAlpha(0.9f));
g.drawText("ER",
(int)(plotX + 4), (int)(plotY + 2),
30, 12, juce::Justification::centredLeft);
@ -315,7 +315,7 @@ void DecayCurveViz::paint(juce::Graphics& g)
40, 12, juce::Justification::centredLeft);
}
g.setFont(juce::Font(juce::FontOptions(7.5f)));
g.setFont(juce::Font(juce::FontOptions(7.5f * zoomScale)));
g.setColour(AmbivalenceColors::TextSecondary.withAlpha(0.4f));
g.drawText("0-200ms (x2)",
(int)(plotX + 2), (int)(plotY + plotH - 14),

View file

@ -9,11 +9,13 @@ public:
~DecayCurveViz() override;
void setProcessor(FDNReverbAudioProcessor* p) noexcept { processor = p; }
void setZoomScale(float s) noexcept { zoomScale = s; repaint(); }
void paint(juce::Graphics& g) override;
void resized() override;
private:
void timerCallback() override;
float zoomScale{ 1.0f };
// --- time axis -----------------------------------------------
// time axis :

View file

@ -3,13 +3,12 @@
#include "BuildInfo.h"
static constexpr int Y_HEADER = 8;
static constexpr int Y_ALGO = 48;
static constexpr int Y_SLABEL1 = 86;
static constexpr int Y_ROW1 = 104;
static constexpr int Y_SLABEL2 = 204;
static constexpr int Y_ROW2 = 222;
static constexpr int Y_SEP = 322;
static constexpr int Y_VIZ = 326;
static constexpr int Y_SLABEL1 = 46;
static constexpr int Y_ROW1 = 64;
static constexpr int Y_SLABEL2 = 164;
static constexpr int Y_ROW2 = 182;
static constexpr int Y_SEP = 282;
static constexpr int Y_VIZ = 286;
static constexpr int SEC_TIME = 8;
static constexpr int SEC_FREQUENCY = 254;
@ -21,28 +20,26 @@ static constexpr int SEP_FD = 409;
static constexpr int SEP_DS = 655;
static constexpr int SEP_SC = 737;
// -----------------------------------------------------------------------------
// EditorContent
// -----------------------------------------------------------------------------
// constructor
// -----------------------------------------------------------------------------
FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
: AudioProcessorEditor(&p),
audioProcessor(p),
algoSelector(p.apvts),
vuIn("IN", VUMeter::Side::Input),
EditorContent::EditorContent(FDNReverbAudioProcessor& p, AmbivalenceLookAndFeel& laf)
: vuIn("IN", VUMeter::Side::Input),
vuOut("OUT", VUMeter::Side::Output)
{
setLookAndFeel(&laf);
setSize(W, H);
setOpaque(true);
lookAndFeel = &laf;
// -- Title --
titleLabel.setText("AMBIVALENCE 1.1", juce::dontSendNotification);
titleLabel.setText("AMBIVALENCE", juce::dontSendNotification);
titleLabel.setFont(juce::Font(juce::FontOptions(
"Helvetica Neue", 14.f, juce::Font::bold)));
"Helvetica Neue", 24.f, juce::Font::bold)));
titleLabel.setColour(juce::Label::textColourId, AmbivalenceColors::TextPrimary);
titleLabel.setJustificationType(juce::Justification::centredLeft);
addAndMakeVisible(titleLabel);
addAndMakeVisible(algoSelector);
auto BK = [&](ArcKnob& k, const char* id, const char* lbl) {
k.build(p.apvts, id, lbl, this, laf);
};
@ -68,7 +65,7 @@ FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
BK(kHiCutNorm, "hicut", "HI CUT");
// -- ProMode button --
proModeButton.setButtonText("PRO");
proModeButton.setButtonText("EQ");
proModeButton.setClickingTogglesState(true);
proModeButton.setColour(juce::TextButton::buttonOnColourId, AmbivalenceColors::Accent);
proModeButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
@ -79,8 +76,16 @@ FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
new juce::AudioProcessorValueTreeState::ButtonAttachment(
p.apvts, "promode", proModeButton));
// -- Zoom menu button --
zoomButton.setButtonText(juce::String(
juce::roundToInt(EditorContent::kDefaultZoom * 100.f)) + "%");
zoomButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
zoomButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextPrimary);
zoomButton.setTooltip("UI zoom");
addAndMakeVisible(zoomButton);
// -- ER SOLO button --
erSoloButton.setButtonText("ER SOLO");
erSoloButton.setButtonText("ER ONLY");
erSoloButton.setClickingTogglesState(true);
erSoloButton.setColour(juce::TextButton::buttonOnColourId, AmbivalenceColors::AccentBlue);
erSoloButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
@ -124,82 +129,8 @@ FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
BK(kLoCutPro, "locut", "LO CUT");
BK(kHiCutPro, "hicut", "HI CUT");
// -------------------------------------------------------------------------
// preset UI
// -------------------------------------------------------------------------
presetManager = std::make_unique<PresetManager>(p);
// < PREV
presetPrevButton.setButtonText("<");
presetPrevButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
presetPrevButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextPrimary);
addAndMakeVisible(presetPrevButton);
presetPrevButton.onClick = [this] {
presetManager->loadPrevPreset();
};
// preset name combo
presetCombo.setLookAndFeel(&laf);
addAndMakeVisible(presetCombo);
presetCombo.onChange = [this] {
int idx = presetCombo.getSelectedItemIndex();
auto names = presetManager->getPresetNames();
if (idx >= 0 && idx < names.size())
presetManager->loadPreset(names[idx]);
};
// > NEXT
presetNextButton.setButtonText(">");
presetNextButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
presetNextButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextPrimary);
addAndMakeVisible(presetNextButton);
presetNextButton.onClick = [this] {
presetManager->loadNextPreset();
};
// SAVE
presetSaveButton.setButtonText("SAVE");
presetSaveButton.setColour(juce::TextButton::buttonColourId,
AmbivalenceColors::Accent.withAlpha(0.75f));
presetSaveButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::Background);
addAndMakeVisible(presetSaveButton);
presetSaveButton.onClick = [this] { savePresetWithDialog(); };
// --- after changes ---
// LOAD
presetLoadButton.setButtonText("LOAD");
presetLoadButton.setColour(juce::TextButton::buttonColourId,
AmbivalenceColors::AccentBlue.withAlpha(0.75f));
presetLoadButton.setColour(juce::TextButton::textColourOffId,
AmbivalenceColors::Background);
addAndMakeVisible(presetLoadButton);
presetLoadButton.onClick = [this] {
auto names = presetManager->getPresetNames();
int idx = presetCombo.getSelectedItemIndex();
if (idx >= 0 && idx < names.size())
presetManager->loadPreset(names[idx]);
};
// DELETE
presetDeleteButton.setButtonText("DELETE");
presetDeleteButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
presetDeleteButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextSecondary);
addAndMakeVisible(presetDeleteButton);
presetDeleteButton.onClick = [this] { deleteCurrentPreset(); };
// callback setup
// in the constructor callback setup
presetManager->onPresetListChanged = [this] { refreshPresetCombo(); };
// * fix : whenever the preset name changes Processor notify
presetManager->onPresetLoaded = [this](const juce::String& name) {
audioProcessor.setLastSavedPresetName(name);
refreshPresetCombo();
};
// -- Visualizers --
rt60Viz.setProcessor(&p);
decayCurveViz.setProcessor(&p);
addAndMakeVisible(rt60Viz);
addAndMakeVisible(decayCurveViz);
addAndMakeVisible(vuIn);
@ -254,75 +185,32 @@ FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
"Helvetica Neue", 8.5f, juce::Font::plain)));
statusLabel.setColour(juce::Label::textColourId,
AmbivalenceColors::TextSecondary.withAlpha(0.6f));
statusLabel.setJustificationType(juce::Justification::centred);
statusLabel.setJustificationType(juce::Justification::centredRight);
addAndMakeVisible(statusLabel);
// --- after changes ---
refreshPresetCombo(); // added: initialize the combo at startup
// -- Preset UI (wiring is done by the editor) --
addAndMakeVisible(presetPrevButton);
addAndMakeVisible(presetCombo);
addAndMakeVisible(presetNextButton);
addAndMakeVisible(presetSaveButton);
addAndMakeVisible(presetLoadButton);
addAndMakeVisible(presetDeleteButton);
updatePanelVisibility();
startTimerHz(60);
}
// -----------------------------------------------------------------------------
// destructor
// EditorContent::updatePanelVisibility
// -----------------------------------------------------------------------------
FDNReverbEditor::~FDNReverbEditor() {
stopTimer();
setLookAndFeel(nullptr);
satTypeCombo.setLookAndFeel(nullptr);
presetCombo.setLookAndFeel(nullptr);
}
// -----------------------------------------------------------------------------
// timerCallback
// -----------------------------------------------------------------------------
void FDNReverbEditor::timerCallback()
{
vuIn.setLevels(audioProcessor.getInputRMSL(),
audioProcessor.getInputRMSR());
vuOut.setLevels(audioProcessor.getOutputRMSL(),
audioProcessor.getOutputRMSR());
vuIn.repaint();
vuOut.repaint();
static int metricsCounter = 0;
if (++metricsCounter >= 2) {
metricsCounter = 0;
labelD50Value.setText(
juce::String(audioProcessor.getD50() * 100.0f, 1) + "%",
juce::dontSendNotification);
labelC50Value.setText(
juce::String(audioProcessor.getC50(), 1) + "dB",
juce::dontSendNotification);
labelC80Value.setText(
juce::String(audioProcessor.getC80(), 1) + "dB",
juce::dontSendNotification);
labelEDTValue.setText(
juce::String(audioProcessor.getEDT(), 2) + "s",
juce::dontSendNotification);
}
bool newProMode = (*audioProcessor.apvts.getRawParameterValue("promode") > 0.5f);
if (newProMode != isProMode) {
isProMode = newProMode;
updatePanelVisibility();
resized();
repaint();
}
}
// -----------------------------------------------------------------------------
// updatePanelVisibility
// -----------------------------------------------------------------------------
void FDNReverbEditor::updatePanelVisibility()
void EditorContent::updatePanelVisibility()
{
auto setKnob = [](ArcKnob& k, bool vis) {
k.slider.setVisible(vis);
k.label.setVisible(vis);
};
const bool showNormal = !isProMode;
const bool showPro = isProMode;
const bool showNormal = !proMode;
const bool showPro = proMode;
setKnob(kPreDelay, showNormal);
setKnob(kRoomSize, showNormal);
@ -357,37 +245,100 @@ void FDNReverbEditor::updatePanelVisibility()
presetPrevButton.setVisible(true);
presetCombo.setVisible(true);
presetNextButton.setVisible(true);
// --- after changes ---
presetSaveButton.setVisible(true);
presetLoadButton.setVisible(true); // * added
presetLoadButton.setVisible(true);
presetDeleteButton.setVisible(true);
}
// -----------------------------------------------------------------------------
// resized
// EditorContent::setProMode
// -----------------------------------------------------------------------------
void FDNReverbEditor::resized()
void EditorContent::setProMode(bool pro)
{
titleLabel.setBounds(PAD, Y_HEADER, 180, 32);
proModeButton.setBounds(196, Y_HEADER + 5, 52, 22);
erSoloButton.setBounds(256, Y_HEADER + 5, 72, 22);
vuIn.setBounds(W - 220, Y_HEADER + 2, 96, 28);
vuOut.setBounds(W - 120, Y_HEADER + 2, 96, 28);
if (proMode == pro)
return;
algoSelector.setBounds(PAD, Y_ALGO, W - PAD * 2, 30);
proMode = pro;
updatePanelVisibility();
resized();
repaint();
}
// -----------------------------------------------------------------------------
// EditorContent::resized
// -----------------------------------------------------------------------------
void EditorContent::resized()
{
uiScale = getWidth() > 0 ? (float)getWidth() / (float)W : 1.0f;
// scale fonts that are set explicitly (knob/button/combo fonts handled
// via the LookAndFeel which already has the same uiScale pushed to it).
auto labelFont = [this](float h, int style = juce::Font::plain) {
return juce::Font(juce::FontOptions("Helvetica Neue", h * uiScale, style));
};
titleLabel.setFont(labelFont(24.f, juce::Font::bold));
satTypeLabel.setFont(labelFont(9.f));
labelMetricsTitle.setFont(labelFont(8.5f, juce::Font::bold));
labelD50Caption.setFont(labelFont(8.f));
labelC50Caption.setFont(labelFont(8.f));
labelC80Caption.setFont(labelFont(8.f));
labelEDTCaption.setFont(labelFont(8.f));
labelD50Value.setFont(labelFont(9.5f, juce::Font::bold));
labelC50Value.setFont(labelFont(9.5f, juce::Font::bold));
labelC80Value.setFont(labelFont(9.5f, juce::Font::bold));
labelEDTValue.setFont(labelFont(9.5f, juce::Font::bold));
statusLabel.setFont(labelFont(8.5f));
ArcKnob* knobs[] = {
&kPreDelay, &kRoomSize, &kDecay,
&kHFDamp, &kLFAbsorb,
&kDiffusion, &kModAmt, &kModRate,
&kStereoW,
&kERLevel, &kSaturation,
&kWet, &kDry,
&kDuckAmt, &kDuckThr, &kDuckAtt, &kDuckRel,
&kLoCutNorm, &kHiCutNorm,
&kRTBands[0], &kRTBands[1], &kRTBands[2], &kRTBands[3], &kRTBands[4],
&kRTBands[5], &kRTBands[6], &kRTBands[7], &kRTBands[8], &kRTBands[9],
&kTiltLow, &kTiltMid, &kTiltHigh,
&kLoCutPro, &kHiCutPro
};
for (auto* k : knobs)
k->label.setFont(labelFont(9.f));
if (lookAndFeel != nullptr)
lookAndFeel->zoomScale = uiScale;
decayCurveViz.setZoomScale(uiScale);
// layout is written in fixed design coords (W x H); S() maps to the
// actual component size so no host-hostile setTransform is needed.
auto S = [this](int x, int y, int w, int h) {
return juce::Rectangle<int>(juce::roundToInt(x * uiScale),
juce::roundToInt(y * uiScale),
juce::roundToInt(w * uiScale),
juce::roundToInt(h * uiScale));
};
titleLabel.setBounds(S(PAD, Y_HEADER - 6, 300, 44));
proModeButton.setBounds(S(316, Y_HEADER + 5, 52, 22));
erSoloButton.setBounds(S(376, Y_HEADER + 5, 72, 22));
vuIn.setBounds(S(W - 220, Y_HEADER + 2, 96, 28));
vuOut.setBounds(S(W - 120, Y_HEADER + 2, 96, 28));
zoomButton.setBounds(S(W - 272, Y_HEADER + 5, 48, 22));
auto place1 = [&](ArcKnob& k, int& x, int y) {
k.label.setBounds(x, y, KNOB_W, KNOB_LBL_H);
k.slider.setBounds(x, y + KNOB_LBL_H, KNOB_W, KNOB_H);
k.label.setBounds(S(x, y, KNOB_W, KNOB_LBL_H));
k.slider.setBounds(S(x, y + KNOB_LBL_H, KNOB_W, KNOB_H));
x += KNOB_W + ROW1_GAP;
};
auto place2 = [&](ArcKnob& k, int& x, int y) {
k.label.setBounds(x, y, KNOB_W, KNOB_LBL_H);
k.slider.setBounds(x, y + KNOB_LBL_H, KNOB_W, KNOB_H);
k.label.setBounds(S(x, y, KNOB_W, KNOB_LBL_H));
k.slider.setBounds(S(x, y + KNOB_LBL_H, KNOB_W, KNOB_H));
x += KNOB_W + PAD;
};
if (!isProMode) {
if (!proMode) {
// -- Row 1 --
int kx = PAD;
place1(kPreDelay, kx, Y_ROW1);
@ -424,8 +375,8 @@ void FDNReverbEditor::resized()
// -- ProMode row 2 --
int kx2 = PAD;
satTypeLabel.setBounds(kx2, Y_SLABEL2, KNOB_W, KNOB_LBL_H);
satTypeCombo.setBounds(kx2, Y_SLABEL2 + KNOB_LBL_H + 2, KNOB_W + PAD, 24);
satTypeLabel.setBounds(S(kx2, Y_SLABEL2, KNOB_W, KNOB_LBL_H));
satTypeCombo.setBounds(S(kx2, Y_SLABEL2 + KNOB_LBL_H + 2, KNOB_W + PAD, 24));
kx2 += KNOB_W + PAD + PAD + 8;
place2(kTiltLow, kx2, Y_ROW2);
place2(kTiltMid, kx2, Y_ROW2);
@ -439,10 +390,10 @@ void FDNReverbEditor::resized()
// preset UI (always, mode-independent)
// -------------------------------------------------------------------------
// layout (PRESET_PANEL_X=632 starting from ):
//
//
// top row Y=Y_ROW2: [<(26)] gap4 [combo(154)] gap4 [>(26)] -> right edge 848
// bottom row Y=Y_ROW2+34:[SAVE(104)] gap8 [DELETE(104)] -> right edge 848
//
//
// separator vertical line : PRESET_PANEL_X - 9 = 623
// -------------------------------------------------------------------------
{
@ -450,31 +401,26 @@ void FDNReverbEditor::resized()
const int btnH = 26;
// top row
presetPrevButton.setBounds(px, Y_ROW2, 26, btnH);
presetCombo.setBounds(px + 30, Y_ROW2, 154, btnH);
presetNextButton.setBounds(px + 188, Y_ROW2, 26, btnH);
presetPrevButton.setBounds(S(px, Y_ROW2, 26, btnH));
presetCombo.setBounds(S(px + 30, Y_ROW2, 154, btnH));
presetNextButton.setBounds(S(px + 188, Y_ROW2, 26, btnH));
// --- after changes ---
// bottom row : [SAVE(68)] [LOAD(68)] [DELETE(68)]
presetSaveButton.setBounds(px, Y_ROW2 + 34, 68, btnH);
presetLoadButton.setBounds(px + 72, Y_ROW2 + 34, 68, btnH);
presetDeleteButton.setBounds(px + 144, Y_ROW2 + 34, 68, btnH);
// bottom row : [SAVE(68)] [LOAD(68)] [DELETE(68)]
presetSaveButton.setBounds(S(px, Y_ROW2 + 34, 68, btnH));
presetLoadButton.setBounds(S(px + 72, Y_ROW2 + 34, 68, btnH));
presetDeleteButton.setBounds(S(px + 144, Y_ROW2 + 34, 68, btnH));
}
// -- Visualizers --
const int vizTotalH = H - Y_VIZ - STATUS_H - PAD;
const int rt60Height = vizTotalH / 2 - 2;
const int decayHeight = vizTotalH / 2 - 2;
const int decayY = Y_VIZ + rt60Height + 4;
// -- Visualizer --
const int decayHeight = H - Y_VIZ - STATUS_H - PAD;
rt60Viz.setBounds(PAD, Y_VIZ, W - PAD * 2, rt60Height);
decayCurveViz.setBounds(PAD, decayY, W - PAD * 2, decayHeight);
decayCurveViz.setBounds(S(PAD, Y_VIZ, W - PAD * 2, decayHeight));
// -- AcousticMetrics --
const int metricsRight = W - PAD - 8;
const int metricsW = 280;
const int metricsLeft = metricsRight - metricsW;
const int metricsTop = decayY + 6;
const int metricsTop = Y_VIZ + 6;
const int metricsRowH = 14;
const int metricsRow1Y = metricsTop + 14;
const int metricsRow2Y = metricsRow1Y + metricsRowH;
@ -483,37 +429,33 @@ void FDNReverbEditor::resized()
const int colSpacing = 8;
const int colW = captionW + valueW;
labelMetricsTitle.setBounds(metricsLeft, metricsTop, 80, 12);
labelD50Caption.setBounds(metricsLeft, metricsRow1Y, captionW, metricsRowH);
labelD50Value.setBounds(metricsLeft + captionW, metricsRow1Y, valueW, metricsRowH);
labelC50Caption.setBounds(metricsLeft + colW + colSpacing, metricsRow1Y, captionW, metricsRowH);
labelC50Value.setBounds(metricsLeft + colW + colSpacing + captionW, metricsRow1Y, valueW, metricsRowH);
labelC80Caption.setBounds(metricsLeft, metricsRow2Y, captionW, metricsRowH);
labelC80Value.setBounds(metricsLeft + captionW, metricsRow2Y, valueW, metricsRowH);
labelEDTCaption.setBounds(metricsLeft + colW + colSpacing, metricsRow2Y, captionW, metricsRowH);
labelEDTValue.setBounds(metricsLeft + colW + colSpacing + captionW, metricsRow2Y, valueW, metricsRowH);
labelMetricsTitle.setBounds(S(metricsLeft, metricsTop, 80, 12));
labelD50Caption.setBounds(S(metricsLeft, metricsRow1Y, captionW, metricsRowH));
labelD50Value.setBounds(S(metricsLeft + captionW, metricsRow1Y, valueW, metricsRowH));
labelC50Caption.setBounds(S(metricsLeft + colW + colSpacing, metricsRow1Y, captionW, metricsRowH));
labelC50Value.setBounds(S(metricsLeft + colW + colSpacing + captionW, metricsRow1Y, valueW, metricsRowH));
labelC80Caption.setBounds(S(metricsLeft, metricsRow2Y, captionW, metricsRowH));
labelC80Value.setBounds(S(metricsLeft + captionW, metricsRow2Y, valueW, metricsRowH));
labelEDTCaption.setBounds(S(metricsLeft + colW + colSpacing, metricsRow2Y, captionW, metricsRowH));
labelEDTValue.setBounds(S(metricsLeft + colW + colSpacing + captionW, metricsRow2Y, valueW, metricsRowH));
// -- Git build-info status bar --
statusLabel.setBounds(PAD, H - STATUS_H, W - PAD * 2, STATUS_H);
statusLabel.setBounds(S(PAD, H - STATUS_H, W - PAD - 16, STATUS_H));
}
// -----------------------------------------------------------------------------
// paint
// EditorContent::paint
// -----------------------------------------------------------------------------
void FDNReverbEditor::paint(juce::Graphics& g)
void EditorContent::paint(juce::Graphics& g)
{
g.fillAll(AmbivalenceColors::Background);
g.addTransform(juce::AffineTransform::scale(uiScale));
juce::ColourGradient grad(
AmbivalenceColors::Surface.withAlpha(0.12f), 0.f, 0.f,
AmbivalenceColors::Background, 0.f, (float)H, false);
g.setGradientFill(grad);
g.fillAll();
g.setFont(juce::Font(juce::FontOptions(8.f)));
g.setColour(AmbivalenceColors::TextSecondary.withAlpha(0.35f));
g.drawText("16ch FDN | SAPF | ISM-ER | 44.1-192kHz",
PAD + 336, Y_HEADER + 10, W / 2, 12,
juce::Justification::centredLeft);
g.fillRect(0.f, 0.f, (float)W, (float)H);
g.setColour(AmbivalenceColors::Separator);
g.drawHorizontalLine(Y_SEP, (float)PAD, (float)(W - PAD));
@ -525,7 +467,7 @@ void FDNReverbEditor::paint(juce::Graphics& g)
g.drawText(t, x, y, 200, 14, juce::Justification::centredLeft);
};
if (!isProMode) {
if (!proMode) {
// -- Row 1 separator --
g.setColour(AmbivalenceColors::Separator);
g.drawVerticalLine(SEP_TF, (float)Y_SLABEL1, (float)(Y_ROW1 + UNIT_H));
@ -583,12 +525,197 @@ void FDNReverbEditor::paint(juce::Graphics& g)
g.drawHorizontalLine(H - STATUS_H - 1, (float)PAD, (float)(W - PAD));
}
// -----------------------------------------------------------------------------
// EditorContent::Zoom control
// -----------------------------------------------------------------------------
void EditorContent::showZoomMenu()
{
static constexpr float zoomLevels[] = { 0.75f, 1.0f, 1.5f, 2.0f, 2.5f, 3.0f };
juce::PopupMenu menu;
for (int i = 0; i < 6; ++i) {
const int pct = juce::roundToInt(zoomLevels[i] * 100.f);
menu.addItem(juce::String(pct) + "%", true,
juce::approximatelyEqual(zoomLevels[i], currentZoom),
[this, level = zoomLevels[i]] { setZoomLevel(level); });
}
menu.showMenuAsync(juce::PopupMenu::Options()
.withTargetComponent(&zoomButton)
.withStandardItemHeight(22));
}
void EditorContent::setZoomLevel(float scale)
{
currentZoom = juce::jlimit(0.75f, 3.0f, scale);
zoomButton.setButtonText(
juce::String(juce::roundToInt(currentZoom * 100.f)) + "%");
if (onZoomChange)
onZoomChange(currentZoom);
}
// -----------------------------------------------------------------------------
// constructor
// -----------------------------------------------------------------------------
FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
: AudioProcessorEditor(&p),
audioProcessor(p)
{
setLookAndFeel(&laf);
setSize(juce::roundToInt((float)EditorContent::W * EditorContent::kDefaultZoom),
juce::roundToInt((float)EditorContent::H * EditorContent::kDefaultZoom));
content = std::make_unique<EditorContent>(p, laf);
addAndMakeVisible(content.get());
// setSize() in the constructor runs resized() while content is still
// null, and hosts open the editor at exactly this size, so resized()
// would otherwise never run again - leaving content at 0x0 (black
// canvas). Lay it out explicitly here.
resized();
// when the user picks a zoom level, resize the (untransformed) editor so
// it exactly matches the scaled content. resized() derives the content
// scale from the actual editor width, so hosts that clamp the window
// still end up with a correct, fully-covered UI.
content->onZoomChange = [this](float z) {
setSize(juce::roundToInt((float)EditorContent::W * z),
juce::roundToInt((float)EditorContent::H * z));
};
// -- Preset UI wiring --
presetManager = std::make_unique<PresetManager>(p);
content->zoomButton.onClick = [this] { content->showZoomMenu(); };
content->presetPrevButton.setButtonText("<");
content->presetPrevButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
content->presetPrevButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextPrimary);
content->presetPrevButton.onClick = [this] {
presetManager->loadPrevPreset();
};
content->presetCombo.setLookAndFeel(&laf);
content->presetCombo.onChange = [this] {
int idx = content->presetCombo.getSelectedItemIndex() - FDNReverb::NUM_ALGORITHMS;
auto names = presetManager->getPresetNames();
if (idx >= 0 && idx < names.size())
presetManager->loadPreset(names[idx]);
};
content->presetNextButton.setButtonText(">");
content->presetNextButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
content->presetNextButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextPrimary);
content->presetNextButton.onClick = [this] {
presetManager->loadNextPreset();
};
content->presetSaveButton.setButtonText("SAVE");
content->presetSaveButton.setColour(juce::TextButton::buttonColourId,
AmbivalenceColors::Accent.withAlpha(0.75f));
content->presetSaveButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::Background);
content->presetSaveButton.onClick = [this] { savePresetWithDialog(); };
content->presetLoadButton.setButtonText("LOAD");
content->presetLoadButton.setColour(juce::TextButton::buttonColourId,
AmbivalenceColors::AccentBlue.withAlpha(0.75f));
content->presetLoadButton.setColour(juce::TextButton::textColourOffId,
AmbivalenceColors::Background);
content->presetLoadButton.onClick = [this] {
int idx = content->presetCombo.getSelectedItemIndex() - FDNReverb::NUM_ALGORITHMS;
auto names = presetManager->getPresetNames();
if (idx >= 0 && idx < names.size())
presetManager->loadPreset(names[idx]);
};
content->presetDeleteButton.setButtonText("DELETE");
content->presetDeleteButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
content->presetDeleteButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextSecondary);
content->presetDeleteButton.onClick = [this] { deleteCurrentPreset(); };
// callback setup
presetManager->onPresetListChanged = [this] { refreshPresetCombo(); };
// * fix : whenever the preset name changes Processor notify
presetManager->onPresetLoaded = [this](const juce::String& name) {
audioProcessor.setLastSavedPresetName(name);
refreshPresetCombo();
};
refreshPresetCombo();
startTimerHz(60);
}
// -----------------------------------------------------------------------------
// destructor
// -----------------------------------------------------------------------------
FDNReverbEditor::~FDNReverbEditor() {
stopTimer();
setLookAndFeel(nullptr);
content->satTypeCombo.setLookAndFeel(nullptr);
content->presetCombo.setLookAndFeel(nullptr);
}
// -----------------------------------------------------------------------------
// timerCallback
// -----------------------------------------------------------------------------
void FDNReverbEditor::timerCallback()
{
content->vuIn.setLevels(audioProcessor.getInputRMSL(),
audioProcessor.getInputRMSR());
content->vuOut.setLevels(audioProcessor.getOutputRMSL(),
audioProcessor.getOutputRMSR());
content->vuIn.repaint();
content->vuOut.repaint();
static int metricsCounter = 0;
if (++metricsCounter >= 2) {
metricsCounter = 0;
content->labelD50Value.setText(
juce::String(audioProcessor.getD50() * 100.0f, 1) + "%",
juce::dontSendNotification);
content->labelC50Value.setText(
juce::String(audioProcessor.getC50(), 1) + "dB",
juce::dontSendNotification);
content->labelC80Value.setText(
juce::String(audioProcessor.getC80(), 1) + "dB",
juce::dontSendNotification);
content->labelEDTValue.setText(
juce::String(audioProcessor.getEDT(), 2) + "s",
juce::dontSendNotification);
}
bool newProMode = (*audioProcessor.apvts.getRawParameterValue("promode") > 0.5f);
content->setProMode(newProMode);
}
// -----------------------------------------------------------------------------
// resized
// -----------------------------------------------------------------------------
void FDNReverbEditor::resized()
{
if (content == nullptr)
return;
// No setTransform here: scaling is done by EditorContent applying
// uiScale to every child bounds and to its own paint() graphics, which
// renders correctly in all hosts (some hosts, e.g. FL Studio, fail to
// composite a transformed top-level child and show a blank frame).
content->setBounds(getLocalBounds());
}
// -----------------------------------------------------------------------------
// paint
// -----------------------------------------------------------------------------
void FDNReverbEditor::paint(juce::Graphics& g)
{
g.fillAll(AmbivalenceColors::Background);
}
// -----------------------------------------------------------------------------
// Preset UI helpers
// -----------------------------------------------------------------------------
void FDNReverbEditor::refreshPresetCombo()
{
presetCombo.clear(juce::dontSendNotification);
content->presetCombo.clear(juce::dontSendNotification);
auto names = presetManager->getPresetNames();
// ---------------------------------------------------------------------
@ -606,29 +733,41 @@ void FDNReverbEditor::refreshPresetCombo()
presetManager->setCurrentPresetName(saved);
}
// -- built-in algorithm presets --
content->presetCombo.addSectionHeading("ALGORITHM");
static const char* algoNames[FDNReverb::NUM_ALGORITHMS] = {
"ROOM1", "ROOM2", "HALL1", "HALL2", "PLATE", "SPRING", "GOLDFOIL"
};
for (int i = 0; i < FDNReverb::NUM_ALGORITHMS; ++i)
content->presetCombo.addItem(algoNames[i], i + 1);
// -- saved user presets --
if (names.isEmpty()) {
presetCombo.addItem("-- No Presets --", 1);
presetCombo.setSelectedItemIndex(0, juce::dontSendNotification);
presetDeleteButton.setEnabled(false);
presetLoadButton.setEnabled(false);
presetPrevButton.setEnabled(false);
presetNextButton.setEnabled(false);
const int placeholderId = FDNReverb::NUM_ALGORITHMS + 1;
content->presetCombo.addItem("-- No Presets --", placeholderId);
content->presetCombo.setSelectedId(placeholderId, juce::dontSendNotification);
content->presetDeleteButton.setEnabled(false);
content->presetLoadButton.setEnabled(false);
content->presetPrevButton.setEnabled(false);
content->presetNextButton.setEnabled(false);
return;
}
// user-preset IDs start after the algorithm IDs so no two combo items
// share an ID (JUCE ticks every item whose ID matches the selection).
for (int i = 0; i < names.size(); ++i)
presetCombo.addItem(names[i], i + 1);
content->presetCombo.addItem(names[i], FDNReverb::NUM_ALGORITHMS + i + 1);
int idx = presetManager->getCurrentPresetIndex();
if (idx >= 0)
presetCombo.setSelectedItemIndex(idx, juce::dontSendNotification);
content->presetCombo.setSelectedItemIndex(idx, juce::dontSendNotification);
else
presetCombo.setSelectedItemIndex(0, juce::dontSendNotification);
content->presetCombo.setSelectedItemIndex(0, juce::dontSendNotification);
presetDeleteButton.setEnabled(true);
presetLoadButton.setEnabled(true);
presetPrevButton.setEnabled(names.size() > 1);
presetNextButton.setEnabled(names.size() > 1);
content->presetDeleteButton.setEnabled(true);
content->presetLoadButton.setEnabled(true);
content->presetPrevButton.setEnabled(names.size() > 1);
content->presetNextButton.setEnabled(names.size() > 1);
}
void FDNReverbEditor::savePresetWithDialog()
@ -687,4 +826,4 @@ void FDNReverbEditor::deleteCurrentPreset()
}),
true
);
}
}

View file

@ -6,31 +6,33 @@
#include "GUI/AmbivalenceUI.h"
#include "GUI/DecayCurveViz.h"
class FDNReverbEditor : public juce::AudioProcessorEditor,
private juce::Timer
// -----------------------------------------------------------------------------
// EditorContent
// -----------------------------------------------------------------------------
// Holds every UI element of the editor and is the only component that gets a
// zoom transform applied. The top-level AudioProcessorEditor stays
// untransformed and is sized to exactly fit the transformed content, so the
// host always sees a window that matches what the plugin draws (no double
// scaling, no clipped / blank margins at any zoom level).
// -----------------------------------------------------------------------------
class EditorContent : public juce::Component
{
public:
explicit FDNReverbEditor(FDNReverbAudioProcessor&);
~FDNReverbEditor() override;
explicit EditorContent(FDNReverbAudioProcessor& p, AmbivalenceLookAndFeel& laf);
void paint(juce::Graphics&) override;
void resized() override;
private:
void timerCallback() override;
void updatePanelVisibility();
void setProMode(bool pro);
// --- Preset UI helpers ---
void refreshPresetCombo();
void savePresetWithDialog();
void deleteCurrentPreset();
FDNReverbAudioProcessor& audioProcessor;
AmbivalenceLookAndFeel laf;
// --- zoom ---
static constexpr float kDefaultZoom = 1.5f;
float currentZoom{ kDefaultZoom };
float uiScale{ 1.0f }; // derived from getWidth()/W, applied to every child bounds
std::function<void(float)> onZoomChange;
AmbivalenceLookAndFeel* lookAndFeel{ nullptr };
// --- common ---
AlgorithmSelector algoSelector;
RT60Visualizer rt60Viz;
DecayCurveViz decayCurveViz;
VUMeter vuIn, vuOut;
juce::Label titleLabel;
@ -43,12 +45,13 @@ private:
juce::Label statusLabel;
juce::TextButton zoomButton;
juce::TextButton proModeButton;
juce::TextButton erSoloButton;
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> proModeAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> erSoloAttachment;
bool isProMode{ false };
bool proMode{ false };
// --- Normal Mode ---
ArcKnob kPreDelay, kRoomSize, kDecay;
@ -69,18 +72,16 @@ private:
ArcKnob kLoCutPro, kHiCutPro;
// --- preset UI ---
std::unique_ptr<PresetManager> presetManager;
juce::TextButton presetPrevButton;
juce::ComboBox presetCombo;
juce::TextButton presetNextButton;
// existing presetSaveButton / presetDeleteButton next to added
juce::TextButton presetSaveButton;
juce::TextButton presetLoadButton; // * added
juce::TextButton presetLoadButton;
juce::TextButton presetDeleteButton;
// --- layout constants ---
static constexpr int W = 900;
static constexpr int H = 540;
static constexpr int H = 450;
static constexpr int PAD = 8;
static constexpr int KNOB_W = 64;
static constexpr int KNOB_H = 72;
@ -93,5 +94,38 @@ private:
// DUCKING end (616) + gap(16) = 632
static constexpr int PRESET_PANEL_X = 632;
void updatePanelVisibility();
void showZoomMenu();
void setZoomLevel(float scale);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EditorContent)
};
// -----------------------------------------------------------------------------
// Editor
// -----------------------------------------------------------------------------
class FDNReverbEditor : public juce::AudioProcessorEditor,
private juce::Timer
{
public:
explicit FDNReverbEditor(FDNReverbAudioProcessor&);
~FDNReverbEditor() override;
void paint(juce::Graphics&) override;
void resized() override;
private:
void timerCallback() override;
// --- Preset UI helpers ---
void refreshPresetCombo();
void savePresetWithDialog();
void deleteCurrentPreset();
FDNReverbAudioProcessor& audioProcessor;
AmbivalenceLookAndFeel laf;
std::unique_ptr<EditorContent> content;
std::unique_ptr<PresetManager> presetManager;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FDNReverbEditor)
};
};

View file

@ -17,7 +17,7 @@ public:
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override { return true; }
const juce::String getName() const override { return "Ambivalence1.1"; }
const juce::String getName() const override { return "Ambivalence"; }
double getTailLengthSeconds() const override { return 20.0; }
bool acceptsMidi() const override { return false; }
bool producesMidi() const override { return false; }

View file

@ -1,12 +1,37 @@
#include "PresetManager.h"
#include "PluginProcessor.h"
#include "FactoryPresets.h"
PresetManager::PresetManager(FDNReverbAudioProcessor& p)
: processor(p)
{
seedFactoryPresets();
refreshPresetList();
}
// -----------------------------------------------------------------------------
// factory presets
// -----------------------------------------------------------------------------
// embeds Presets/*.ambpreset into the binary, then copies any that are
// missing into the user folder, so the factory set survives fresh installs.
// -----------------------------------------------------------------------------
void PresetManager::seedFactoryPresets()
{
auto folder = getPresetsFolder();
for (int i = 0; i < FactoryPresets::namedResourceListSize; ++i)
{
const auto filename = juce::String::fromUTF8(FactoryPresets::originalFilenames[i]);
auto dest = folder.getChildFile(filename);
if (dest.exists())
continue;
int size = 0;
if (auto* data = FactoryPresets::getNamedResource(FactoryPresets::namedResourceList[i], size))
dest.replaceWithData(data, size);
}
}
// -----------------------------------------------------------------------------
// filesystem
// -----------------------------------------------------------------------------

View file

@ -48,6 +48,7 @@ public:
private:
void refreshPresetList();
void seedFactoryPresets();
juce::File getPresetFile(const juce::String& name) const;
FDNReverbAudioProcessor& processor;