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

@ -43,7 +43,7 @@ juce_add_plugin(Ambivalence
PLUGIN_MANUFACTURER_CODE "Otdk" PLUGIN_MANUFACTURER_CODE "Otdk"
PLUGIN_CODE "Ambi" PLUGIN_CODE "Ambi"
FORMATS VST3 Standalone FORMATS VST3 Standalone
PRODUCT_NAME "Ambivalence1.1" PRODUCT_NAME "Ambivalence"
PLUGIN_DESCRIPTION "16-channel FDN Algorithmic Reverb" PLUGIN_DESCRIPTION "16-channel FDN Algorithmic Reverb"
IS_SYNTH FALSE IS_SYNTH FALSE
NEEDS_MIDI_INPUT FALSE NEEDS_MIDI_INPUT FALSE
@ -118,7 +118,18 @@ target_sources(Ambivalence PRIVATE
Source/DSP/Saturator.h Source/DSP/Saturator.h
Source/DSP/OutputLimiter.h Source/DSP/OutputLimiter.h
Source/DSP/OutputEQ.h Source/DSP/OutputEQ.h
)# ============================================================================ )
# ============================================================================
# factory presets (embedded, seeded into the user folder on first run)
# ============================================================================
file(GLOB AMBIVALENCE_FACTORY_PRESETS CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/Presets/*.ambpreset")
juce_add_binary_data(AmbivalenceFactoryPresets
NAMESPACE FactoryPresets
HEADER_NAME FactoryPresets.h
SOURCES ${AMBIVALENCE_FACTORY_PRESETS}
)
# ============================================================================
# include paths # include paths
# ============================================================================ # ============================================================================
target_include_directories(Ambivalence PRIVATE target_include_directories(Ambivalence PRIVATE
@ -132,6 +143,7 @@ target_include_directories(Ambivalence PRIVATE
# ============================================================================ # ============================================================================
target_link_libraries(Ambivalence target_link_libraries(Ambivalence
PRIVATE PRIVATE
AmbivalenceFactoryPresets
juce::juce_audio_basics juce::juce_audio_basics
juce::juce_audio_devices juce::juce_audio_devices
juce::juce_audio_formats juce::juce_audio_formats

View file

@ -3,25 +3,25 @@
# #
# Usage: # Usage:
# make clean Release build (VST3 + standalone) into build/<platform> # make clean Release build (VST3 + standalone) into build/<platform>
# make install fresh build, remove stale installs, install cleanly # make install remove stale installs, install built artifacts
# make uninstall remove installed artifacts # make uninstall remove installed artifacts
# make clean delete the build directory # make clean delete the build directory
# make version print the current git branch + commit # make version print the current git branch + commit
# make info print the resolved configuration # make info print the resolved configuration
# #
# Notes: # Notes:
# - `make install` always performs a clean rebuild first, so it is fully # - `make install` copies whatever is currently in build/<platform>, it does
# self-sufficient. `make && make install` also works (it builds twice). # not build. Run `make` first if you want a fresh build.
# - JUCE is auto-fetched into ./build/JUCE on first build (shallow clone of # - JUCE is auto-fetched into ./build/JUCE on first build (shallow clone of
# the JUCE_VERSION tag). Override with: make JUCE_PATH=/path/to/JUCE # the JUCE_VERSION tag). Override with: make JUCE_PATH=/path/to/JUCE
# - Install targets (user-level, no sudo required): # - Install targets (user-level, no sudo required):
# macOS: VST3: ~/Library/Audio/Plug-Ins/VST3/Ambivalence1.1.vst3 # macOS: VST3: ~/Library/Audio/Plug-Ins/VST3/Ambivalence.vst3
# Standalone: ~/Applications/Ambivalence1.1.app # Standalone: ~/Applications/Ambivalence.app
# Linux: VST3: ~/.local/lib/vst3/Ambivalence1.1.vst3 # Linux: VST3: ~/.local/lib/vst3/Ambivalence.vst3
# Standalone: ~/.local/bin/Ambivalence1.1 # Standalone: ~/.local/bin/Ambivalence
# ============================================================================ # ============================================================================
PRODUCT := Ambivalence1.1 PRODUCT := Ambivalence
UNAME_S := $(shell uname -s) UNAME_S := $(shell uname -s)
@ -65,6 +65,12 @@ CMAKE := cmake
VST3_BUNDLE := $(VST3_DIR)/$(PRODUCT).vst3 VST3_BUNDLE := $(VST3_DIR)/$(PRODUCT).vst3
STANDALONE := $(APP_DIR)/$(PRODUCT)$(if $(filter $(UNAME_S),Linux),,.app) STANDALONE := $(APP_DIR)/$(PRODUCT)$(if $(filter $(UNAME_S),Linux),,.app)
# JUCE 8 no longer adds install() rules for plugins, so we copy the freshly
# built artefacts out of the build tree ourselves.
BUILD_ARTEFACT_DIR := $(BUILD_DIR)/Ambivalence_artefacts/$(CONFIG)
VST3_SRC := $(BUILD_ARTEFACT_DIR)/VST3/$(PRODUCT).vst3
STANDALONE_SRC := $(BUILD_ARTEFACT_DIR)/Standalone/$(PRODUCT)$(if $(filter $(UNAME_S),Linux),,.app)
ifdef OSX_ARCH ifdef OSX_ARCH
OSX_FLAG := -DCMAKE_OSX_ARCHITECTURES="$(OSX_ARCH)" OSX_FLAG := -DCMAKE_OSX_ARCHITECTURES="$(OSX_ARCH)"
endif endif
@ -93,16 +99,20 @@ build:
$(CMAKE) -S . -B "$(BUILD_DIR)" \ $(CMAKE) -S . -B "$(BUILD_DIR)" \
-DJUCE_PATH="$(JUCE_PATH)" \ -DJUCE_PATH="$(JUCE_PATH)" \
-DCMAKE_BUILD_TYPE="$(CONFIG)" \ -DCMAKE_BUILD_TYPE="$(CONFIG)" \
$(OSX_FLAG) \ $(OSX_FLAG)
-DCMAKE_INSTALL_PREFIX="$(PREFIX)" \
-DJUCE_VST3_DIR="$(VST3_DIR)"
$(CMAKE) --build "$(BUILD_DIR)" -j$(JOBS) $(CMAKE) --build "$(BUILD_DIR)" -j$(JOBS)
install: build install:
@test -d "$(VST3_SRC)" -a -d "$(STANDALONE_SRC)" || { \
echo "error: built artifacts not found in $(BUILD_DIR)"; \
echo " run 'make' first to build them, then 'make install'."; \
exit 1; }
@echo "==> Ambivalence: removing stale installs" @echo "==> Ambivalence: removing stale installs"
rm -rf "$(VST3_BUNDLE)" "$(STANDALONE)" rm -rf "$(VST3_BUNDLE)" "$(STANDALONE)"
@echo "==> Ambivalence: installing" @echo "==> Ambivalence: installing"
$(CMAKE) --install "$(BUILD_DIR)" mkdir -p "$(VST3_DIR)" "$(APP_DIR)"
cp -R "$(VST3_SRC)" "$(VST3_DIR)/"
cp -R "$(STANDALONE_SRC)" "$(APP_DIR)/"
@echo "==> Done." @echo "==> Done."
@if [ -d "$(VST3_BUNDLE)" ]; then \ @if [ -d "$(VST3_BUNDLE)" ]; then \
echo " VST3: $(VST3_BUNDLE)"; \ echo " VST3: $(VST3_BUNDLE)"; \

View file

@ -57,9 +57,9 @@ Ambivalence is a hard fork of **[Ambience 1.0.1](https://github.com/OTODESK4193/
The UI is divided into five main zones: The UI is divided into five main zones:
- **Header Bar** - plugin title, PRO button, ER SOLO button, IN/OUT VU meters, and a footer showing the technical spec (`16ch FDN | SAPF | ISM-ER | 44.1-192kHz`). - **Header Bar** - plugin title, EQ button, ER SOLO button, IN/OUT VU meters, and a footer showing the technical spec (`16ch FDN | SAPF | ISM-ER | 44.1-192kHz`).
- **Algorithm Selector** - 7 reverb type buttons. - **Algorithm Selector** - 7 reverb type buttons.
- **Parameter Panel** - Normal Mode or Pro Mode knobs (toggles with the PRO button). - **Parameter Panel** - Normal Mode or EQ Mode knobs (toggles with the EQ button).
- **Visualizer Area** - RT60 graph (top) and Decay Curve (bottom). - **Visualizer Area** - RT60 graph (top) and Decay Curve (bottom).
- **Preset Panel** - preset browser with SAVE / LOAD / DELETE controls. - **Preset Panel** - preset browser with SAVE / LOAD / DELETE controls.
@ -67,7 +67,7 @@ The UI is divided into five main zones:
| Control | Description | | Control | Description |
| ---------- | ----------- | | ---------- | ----------- |
| PRO | Toggle button: switches the Parameter Panel between Normal and Pro Mode. | | EQ | Toggle button: switches the EQ Parameter Panel between Normal and EQ Mode. |
| ER SOLO | Toggle button: mutes Late Reverb so only Early Reflections are audible. Useful for tuning ER Level and Room Size. | | ER SOLO | Toggle button: mutes Late Reverb so only Early Reflections are audible. Useful for tuning ER Level and Room Size. |
| IN / OUT | Stereo VU meters for input and output signal levels. | | IN / OUT | Stereo VU meters for input and output signal levels. |
@ -75,7 +75,7 @@ The UI is divided into five main zones:
## Algorithms ## Algorithms
Seven reverb topology buttons appear below the header. Selecting an algorithm resets HF Damping, LF Absorption, Diffusion, and all Pro Mode band multipliers to their defaults for that algorithm. Seven reverb topology buttons appear below the header. Selecting an algorithm resets HF Damping, LF Absorption, Diffusion, and all EQ Mode band multipliers to their defaults for that algorithm.
| Algorithm | Acoustic Character | Best For | | Algorithm | Acoustic Character | Best For |
| --------- | -------------------------------------------------------------- | ------------------------------ | | --------- | -------------------------------------------------------------- | ------------------------------ |
@ -160,9 +160,9 @@ Ducking reduces the wet signal level when the input exceeds the threshold, creat
--- ---
## Pro Mode ## EQ Mode
Press the **PRO** button in the header bar to reveal the Pro Mode panel. Two rows of advanced controls replace the Normal Mode rows. All Pro Mode adjustments are retained when switching back to Normal Mode - they are only reset when a different algorithm is selected. Press the **EQ** button in the header bar to reveal the EQ Mode panel. Two rows of advanced controls replace the Normal Mode rows. All EQ Mode adjustments are retained when switching back to Normal Mode - they are only reset when a different algorithm is selected.
### RT60 Per Band (Row 1) ### RT60 Per Band (Row 1)
@ -197,9 +197,9 @@ Three knobs provide broad RT60 tilt control across frequency groups, applied sim
| Control | Description | | Control | Description |
| ---------- | ----------- | | ---------- | ----------- |
| SAT TYPE | Selects the ADAA Saturator character: Warm, Tape, Tube, Hard (see [DSP Technical Reference](#dsp-technical-reference)). Only visible in Pro Mode. | | SAT TYPE | Selects the ADAA Saturator character: Warm, Tape, Tube, Hard (see [DSP Technical Reference](#dsp-technical-reference)). Only visible in EQ Mode. |
| LO CUT (Pro) | Same parameter as Normal Mode LO CUT - both knobs control the same value. | | LO CUT (EQ) | Same parameter as Normal Mode LO CUT - both knobs control the same value. |
| HI CUT (Pro) | Same parameter as Normal Mode HI CUT - both knobs control the same value. | | HI CUT (EQ) | Same parameter as Normal Mode HI CUT - both knobs control the same value. |
--- ---
@ -211,7 +211,7 @@ Displays the frequency-dependent reverberation time across 10 octave bands (31 H
| Element | Description | | Element | Description |
| -------------- | ----------- | | -------------- | ----------- |
| Gray curve | The reference RT60 of the current algorithm preset - the "ideal" before any HF Damping, LF Absorption, or Pro Mode adjustments. | | Gray curve | The reference RT60 of the current algorithm preset - the "ideal" before any HF Damping, LF Absorption, or EQ Mode adjustments. |
| Orange curve | The actual effective RT60 after all adjustments. This is what you hear. | | Orange curve | The actual effective RT60 after all adjustments. This is what you hear. |
| Dynamic Y axis | Auto-adjusts to the current maximum RT60 with smooth exponential tracking; grid lines at standard RT60 values. | | Dynamic Y axis | Auto-adjusts to the current maximum RT60 with smooth exponential tracking; grid lines at standard RT60 values. |
| Frequency labels | X-axis: 31, 63, 125, 250, 500, 1k, 2k, 4k, 8k, 16k Hz. | | Frequency labels | X-axis: 31, 63, 125, 250, 500, 1k, 2k, 4k, 8k, 16k Hz. |
@ -247,7 +247,7 @@ Four metrics, computed from the actual reverb output signal in real time, appear
## Preset Management ## Preset Management
The Preset Panel is always visible in the lower-right area of the parameter section, regardless of Normal/Pro Mode state. The Preset Panel is always visible in the lower-right area of the parameter section, regardless of Normal/EQ Mode state.
### Preset UI Controls ### Preset UI Controls
@ -397,7 +397,7 @@ The gray and orange curves overlap when HF DAMP = 0 and LF ABSORB = 0 (the defau
- Orange below gray at high frequencies = HF DAMP active. - Orange below gray at high frequencies = HF DAMP active.
- Orange below gray at low frequencies = LF ABSORB active. - Orange below gray at low frequencies = LF ABSORB active.
- Orange above gray = a Pro Mode RT band multiplier > 1.0 for those bands. - Orange above gray = a EQ Mode RT band multiplier > 1.0 for those bands.
### Plate Reverb Tips ### Plate Reverb Tips
@ -423,7 +423,7 @@ For infinite, evolving pads with GOLDFOIL:
- WIDTH 0.95, MOD AMT 0.45, MOD RATE 0.20 Hz - WIDTH 0.95, MOD AMT 0.45, MOD RATE 0.20 Hz
- DIFFUSION 0.90 for maximum diffusion - DIFFUSION 0.90 for maximum diffusion
- LO CUT 60-80 Hz to remove sub-bass buildup - LO CUT 60-80 Hz to remove sub-bass buildup
- In Pro Mode, reduce RT 8kHz and RT 16kHz to 0.6 for a warmer, less harsh tail - In EQ Mode, reduce RT 8kHz and RT 16kHz to 0.6 for a warmer, less harsh tail
--- ---

View file

@ -32,9 +32,7 @@ void AmbivalenceLookAndFeel::drawRotarySlider(juce::Graphics& g,
float angle = startAngle + sliderPos * (endAngle - startAngle); float angle = startAngle + sliderPos * (endAngle - startAngle);
juce::Path fill; juce::Path fill;
fill.addCentredArc(cx, cy, r, r, 0.f, startAngle, angle, true); fill.addCentredArc(cx, cy, r, r, 0.f, startAngle, angle, true);
juce::ColourGradient grad(AmbivalenceColors::AccentBlue, cx - r, cy, g.setColour(AmbivalenceColors::ArcFill);
AmbivalenceColors::Accent, cx + r, cy, false);
g.setGradientFill(grad);
g.strokePath(fill, juce::PathStrokeType(th, g.strokePath(fill, juce::PathStrokeType(th,
juce::PathStrokeType::curved, juce::PathStrokeType::rounded)); juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
@ -83,8 +81,8 @@ void AmbivalenceLookAndFeel::positionComboBoxText(juce::ComboBox& box, juce::Lab
label.setFont(getComboBoxFont(box)); label.setFont(getComboBoxFont(box));
} }
juce::Font AmbivalenceLookAndFeel::getLabelFont(juce::Label&) { return mainFont.withHeight(10.f); } juce::Font AmbivalenceLookAndFeel::getLabelFont(juce::Label& label) { return label.getFont(); }
juce::Font AmbivalenceLookAndFeel::getComboBoxFont(juce::ComboBox&) { return mainFont.withHeight(11.f); } juce::Font AmbivalenceLookAndFeel::getComboBoxFont(juce::ComboBox&) { return mainFont.withHeight(11.f * zoomScale); }
void AmbivalenceLookAndFeel::drawGroupComponentOutline(juce::Graphics& g, void AmbivalenceLookAndFeel::drawGroupComponentOutline(juce::Graphics& g,
int w, int h, const juce::String& text, int w, int h, const juce::String& text,
@ -111,130 +109,6 @@ void AmbivalenceLookAndFeel::drawGroupComponentOutline(juce::Graphics& g,
juce::Justification::centredLeft); 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::VUMeter(const juce::String& lbl, Side s) : label(lbl), side(s) {} 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.setColour(AmbivalenceColors::Surface);
g.fillRoundedRectangle(b, 3.f); 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) { auto bar = [&](float y, float level) {
float n = juce::jlimit(0.f, 1.f, juce::jmap( float n = juce::jlimit(0.f, 1.f, juce::jmap(
juce::Decibels::gainToDecibels(level + 1e-9f), -60.f, 0.f, 0.f, 1.f)); juce::Decibels::gainToDecibels(level + 1e-9f), -60.f, 0.f, 0.f, 1.f));
g.setColour(AmbivalenceColors::ArcTrack); 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, juce::ColourGradient gr(AmbivalenceColors::AccentBlue, bx, y,
AmbivalenceColors::Accent, bx + bw, y, false); AmbivalenceColors::Accent, bx + bw, y, false);
g.setGradientFill(gr); 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(startY, levelL);
bar(b.getY() + 11.f, levelR); bar(startY + barH + gap, levelR);
g.setColour(AmbivalenceColors::TextSecondary); g.setColour(AmbivalenceColors::TextSecondary);
g.setFont(8.f); g.setFont(8.f * zoomScale);
g.drawText(label, (int)b.getX(), (int)b.getY(), 20, (int)b.getHeight(), g.drawText(label, (int)(b.getX() + inset), (int)b.getY(), (int)labelW,
juce::Justification::centredLeft); (int)b.getHeight(), juce::Justification::centredLeft);
} }
// --- ArcKnob --------------------------------------------------------- // --- ArcKnob ---------------------------------------------------------
@ -290,65 +173,4 @@ void ArcKnob::build(juce::AudioProcessorValueTreeState& apvts,
attachment.reset( attachment.reset(
new juce::AudioProcessorValueTreeState::SliderAttachment( new juce::AudioProcessorValueTreeState::SliderAttachment(
apvts, paramID, slider)); 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 #pragma once
#include <JuceHeader.h> #include <JuceHeader.h>
#include "../AlgorithmPresets.h"
class FDNReverbAudioProcessor; class FDNReverbAudioProcessor;
// --- Ambivalence Design System ------------------------------------------ // --- Ambivalence Design System ------------------------------------------
namespace AmbivalenceColors { namespace AmbivalenceColors {
const juce::Colour Background{ 0xFF1A1A1A }; const juce::Colour Background{ 0xFF181819 }; // palette 0
const juce::Colour Surface{ 0xFF242424 }; const juce::Colour Surface{ 0xFF2C2E34 }; // theme background
const juce::Colour Panel{ 0xFF2C2C2C }; const juce::Colour Panel{ 0xFF2C2E34 }; // theme background
const juce::Colour Border{ 0xFF3C3C3C }; const juce::Colour Border{ 0xFF3B3E48 }; // selection background
const juce::Colour Accent{ 0xFFFF6B00 }; const juce::Colour Accent{ 0xFFF39660 }; // palette 6/14
const juce::Colour AccentBlue{ 0xFF4090FF }; const juce::Colour AccentBlue{ 0xFF76CCE0 }; // palette 4/12
const juce::Colour TextPrimary{ 0xFFE8E8E8 }; const juce::Colour TextPrimary{ 0xFFE2E2E3 }; // foreground
const juce::Colour TextSecondary{ 0xFF888888 }; const juce::Colour TextSecondary{ 0xFF7F8490 }; // palette 8
const juce::Colour ArcTrack{ 0xFF3A3A3A }; const juce::Colour ArcTrack{ 0xFF3B3E48 }; // selection background
const juce::Colour ArcFill{ 0xFFFF6B00 }; const juce::Colour ArcFill{ 0xFF76CCE0 }; // plain blue arc
const juce::Colour Separator{ 0xFF383838 }; const juce::Colour Separator{ 0xFF2C2E34 }; // theme background
} }
// --- Ambivalence LookAndFeel -------------------------------------------- // --- Ambivalence LookAndFeel --------------------------------------------
@ -38,32 +37,12 @@ public:
void drawGroupComponentOutline(juce::Graphics&, int w, int h, void drawGroupComponentOutline(juce::Graphics&, int w, int h,
const juce::String&, const juce::Justification&, const juce::String&, const juce::Justification&,
juce::GroupComponent&) override; juce::GroupComponent&) override;
float zoomScale{ 1.0f }; // pushed by the editor so fonts scale with zoom
private: private:
juce::Font mainFont; 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 -------------------------------------------------------- // --- VU Meter --------------------------------------------------------
class VUMeter : public juce::Component class VUMeter : public juce::Component
{ {
@ -72,10 +51,12 @@ public:
VUMeter(const juce::String& label, Side side); VUMeter(const juce::String& label, Side side);
void paint(juce::Graphics&) override; void paint(juce::Graphics&) override;
void setLevels(float l, float r) noexcept { levelL = l; levelR = r; } void setLevels(float l, float r) noexcept { levelL = l; levelR = r; }
void setZoomScale(float s) noexcept { zoomScale = s; repaint(); }
private: private:
juce::String label; juce::String label;
Side side; Side side;
float levelL{ 0.f }, levelR{ 0.f }; float levelL{ 0.f }, levelR{ 0.f };
float zoomScale{ 1.0f };
}; };
// --- Labelled Arc Knob ----------------------------------------------- // --- Labelled Arc Knob -----------------------------------------------
@ -89,22 +70,4 @@ struct ArcKnob {
const juce::String& labelText, const juce::String& labelText,
juce::Component* parent, juce::Component* parent,
AmbivalenceLookAndFeel& laf); 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 --- // --- ER zone background ---
{ {
const float erZoneW = plotW * splitRatio; const float erZoneW = plotW * splitRatio;
g.setColour(juce::Colour(0xFF1A2535)); g.setColour(AmbivalenceColors::Surface);
g.fillRect(plotX, plotY, erZoneW, plotH); g.fillRect(plotX, plotY, erZoneW, plotH);
} }
@ -122,7 +122,7 @@ void DecayCurveViz::paint(juce::Graphics& g)
} }
// --- axis label --- // --- 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)); g.setColour(AmbivalenceColors::TextSecondary.withAlpha(0.6f));
for (float db = 0.0f; db >= -60.0f; db -= 20.0f) { 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 // : wide 5px + marker + envelope fill
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
if (!cachedERBypassed && cachedERTapCount > 0) { if (!cachedERBypassed && cachedERTapCount > 0) {
const juce::Colour blueColor = juce::Colour::fromRGB(80, 160, 230); const juce::Colour blueColor = AmbivalenceColors::AccentBlue;
// -- ER envelope fill region -- // -- ER envelope fill region --
if (cachedERTapCount >= 2) { if (cachedERTapCount >= 2) {
@ -300,9 +300,9 @@ void DecayCurveViz::paint(juce::Graphics& g)
// --- zone label --- // --- zone label ---
g.setFont(juce::Font(juce::FontOptions( 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", g.drawText("ER",
(int)(plotX + 4), (int)(plotY + 2), (int)(plotX + 4), (int)(plotY + 2),
30, 12, juce::Justification::centredLeft); 30, 12, juce::Justification::centredLeft);
@ -315,7 +315,7 @@ void DecayCurveViz::paint(juce::Graphics& g)
40, 12, juce::Justification::centredLeft); 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.setColour(AmbivalenceColors::TextSecondary.withAlpha(0.4f));
g.drawText("0-200ms (x2)", g.drawText("0-200ms (x2)",
(int)(plotX + 2), (int)(plotY + plotH - 14), (int)(plotX + 2), (int)(plotY + plotH - 14),

View file

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

View file

@ -3,13 +3,12 @@
#include "BuildInfo.h" #include "BuildInfo.h"
static constexpr int Y_HEADER = 8; static constexpr int Y_HEADER = 8;
static constexpr int Y_ALGO = 48; static constexpr int Y_SLABEL1 = 46;
static constexpr int Y_SLABEL1 = 86; static constexpr int Y_ROW1 = 64;
static constexpr int Y_ROW1 = 104; static constexpr int Y_SLABEL2 = 164;
static constexpr int Y_SLABEL2 = 204; static constexpr int Y_ROW2 = 182;
static constexpr int Y_ROW2 = 222; static constexpr int Y_SEP = 282;
static constexpr int Y_SEP = 322; static constexpr int Y_VIZ = 286;
static constexpr int Y_VIZ = 326;
static constexpr int SEC_TIME = 8; static constexpr int SEC_TIME = 8;
static constexpr int SEC_FREQUENCY = 254; 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_DS = 655;
static constexpr int SEP_SC = 737; static constexpr int SEP_SC = 737;
// -----------------------------------------------------------------------------
// EditorContent
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// constructor // constructor
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p) EditorContent::EditorContent(FDNReverbAudioProcessor& p, AmbivalenceLookAndFeel& laf)
: AudioProcessorEditor(&p), : vuIn("IN", VUMeter::Side::Input),
audioProcessor(p),
algoSelector(p.apvts),
vuIn("IN", VUMeter::Side::Input),
vuOut("OUT", VUMeter::Side::Output) vuOut("OUT", VUMeter::Side::Output)
{ {
setLookAndFeel(&laf); setOpaque(true);
setSize(W, H); lookAndFeel = &laf;
// -- Title -- // -- Title --
titleLabel.setText("AMBIVALENCE 1.1", juce::dontSendNotification); titleLabel.setText("AMBIVALENCE", juce::dontSendNotification);
titleLabel.setFont(juce::Font(juce::FontOptions( 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.setColour(juce::Label::textColourId, AmbivalenceColors::TextPrimary);
titleLabel.setJustificationType(juce::Justification::centredLeft);
addAndMakeVisible(titleLabel); addAndMakeVisible(titleLabel);
addAndMakeVisible(algoSelector);
auto BK = [&](ArcKnob& k, const char* id, const char* lbl) { auto BK = [&](ArcKnob& k, const char* id, const char* lbl) {
k.build(p.apvts, id, lbl, this, laf); k.build(p.apvts, id, lbl, this, laf);
}; };
@ -68,7 +65,7 @@ FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
BK(kHiCutNorm, "hicut", "HI CUT"); BK(kHiCutNorm, "hicut", "HI CUT");
// -- ProMode button -- // -- ProMode button --
proModeButton.setButtonText("PRO"); proModeButton.setButtonText("EQ");
proModeButton.setClickingTogglesState(true); proModeButton.setClickingTogglesState(true);
proModeButton.setColour(juce::TextButton::buttonOnColourId, AmbivalenceColors::Accent); proModeButton.setColour(juce::TextButton::buttonOnColourId, AmbivalenceColors::Accent);
proModeButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface); proModeButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
@ -79,8 +76,16 @@ FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
new juce::AudioProcessorValueTreeState::ButtonAttachment( new juce::AudioProcessorValueTreeState::ButtonAttachment(
p.apvts, "promode", proModeButton)); 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 -- // -- ER SOLO button --
erSoloButton.setButtonText("ER SOLO"); erSoloButton.setButtonText("ER ONLY");
erSoloButton.setClickingTogglesState(true); erSoloButton.setClickingTogglesState(true);
erSoloButton.setColour(juce::TextButton::buttonOnColourId, AmbivalenceColors::AccentBlue); erSoloButton.setColour(juce::TextButton::buttonOnColourId, AmbivalenceColors::AccentBlue);
erSoloButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface); erSoloButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
@ -124,82 +129,8 @@ FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
BK(kLoCutPro, "locut", "LO CUT"); BK(kLoCutPro, "locut", "LO CUT");
BK(kHiCutPro, "hicut", "HI 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 -- // -- Visualizers --
rt60Viz.setProcessor(&p);
decayCurveViz.setProcessor(&p); decayCurveViz.setProcessor(&p);
addAndMakeVisible(rt60Viz);
addAndMakeVisible(decayCurveViz); addAndMakeVisible(decayCurveViz);
addAndMakeVisible(vuIn); addAndMakeVisible(vuIn);
@ -254,75 +185,32 @@ FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
"Helvetica Neue", 8.5f, juce::Font::plain))); "Helvetica Neue", 8.5f, juce::Font::plain)));
statusLabel.setColour(juce::Label::textColourId, statusLabel.setColour(juce::Label::textColourId,
AmbivalenceColors::TextSecondary.withAlpha(0.6f)); AmbivalenceColors::TextSecondary.withAlpha(0.6f));
statusLabel.setJustificationType(juce::Justification::centred); statusLabel.setJustificationType(juce::Justification::centredRight);
addAndMakeVisible(statusLabel); addAndMakeVisible(statusLabel);
// --- after changes --- // -- Preset UI (wiring is done by the editor) --
refreshPresetCombo(); // added: initialize the combo at startup addAndMakeVisible(presetPrevButton);
addAndMakeVisible(presetCombo);
addAndMakeVisible(presetNextButton);
addAndMakeVisible(presetSaveButton);
addAndMakeVisible(presetLoadButton);
addAndMakeVisible(presetDeleteButton);
updatePanelVisibility(); updatePanelVisibility();
startTimerHz(60);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// destructor // EditorContent::updatePanelVisibility
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
FDNReverbEditor::~FDNReverbEditor() { void EditorContent::updatePanelVisibility()
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()
{ {
auto setKnob = [](ArcKnob& k, bool vis) { auto setKnob = [](ArcKnob& k, bool vis) {
k.slider.setVisible(vis); k.slider.setVisible(vis);
k.label.setVisible(vis); k.label.setVisible(vis);
}; };
const bool showNormal = !isProMode; const bool showNormal = !proMode;
const bool showPro = isProMode; const bool showPro = proMode;
setKnob(kPreDelay, showNormal); setKnob(kPreDelay, showNormal);
setKnob(kRoomSize, showNormal); setKnob(kRoomSize, showNormal);
@ -357,37 +245,100 @@ void FDNReverbEditor::updatePanelVisibility()
presetPrevButton.setVisible(true); presetPrevButton.setVisible(true);
presetCombo.setVisible(true); presetCombo.setVisible(true);
presetNextButton.setVisible(true); presetNextButton.setVisible(true);
// --- after changes ---
presetSaveButton.setVisible(true); presetSaveButton.setVisible(true);
presetLoadButton.setVisible(true); // * added presetLoadButton.setVisible(true);
presetDeleteButton.setVisible(true); presetDeleteButton.setVisible(true);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// resized // EditorContent::setProMode
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void FDNReverbEditor::resized() void EditorContent::setProMode(bool pro)
{ {
titleLabel.setBounds(PAD, Y_HEADER, 180, 32); if (proMode == pro)
proModeButton.setBounds(196, Y_HEADER + 5, 52, 22); return;
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);
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) { auto place1 = [&](ArcKnob& k, int& x, int y) {
k.label.setBounds(x, y, KNOB_W, KNOB_LBL_H); k.label.setBounds(S(x, y, KNOB_W, KNOB_LBL_H));
k.slider.setBounds(x, y + KNOB_LBL_H, KNOB_W, KNOB_H); k.slider.setBounds(S(x, y + KNOB_LBL_H, KNOB_W, KNOB_H));
x += KNOB_W + ROW1_GAP; x += KNOB_W + ROW1_GAP;
}; };
auto place2 = [&](ArcKnob& k, int& x, int y) { auto place2 = [&](ArcKnob& k, int& x, int y) {
k.label.setBounds(x, y, KNOB_W, KNOB_LBL_H); k.label.setBounds(S(x, y, KNOB_W, KNOB_LBL_H));
k.slider.setBounds(x, y + KNOB_LBL_H, KNOB_W, KNOB_H); k.slider.setBounds(S(x, y + KNOB_LBL_H, KNOB_W, KNOB_H));
x += KNOB_W + PAD; x += KNOB_W + PAD;
}; };
if (!isProMode) { if (!proMode) {
// -- Row 1 -- // -- Row 1 --
int kx = PAD; int kx = PAD;
place1(kPreDelay, kx, Y_ROW1); place1(kPreDelay, kx, Y_ROW1);
@ -424,8 +375,8 @@ void FDNReverbEditor::resized()
// -- ProMode row 2 -- // -- ProMode row 2 --
int kx2 = PAD; int kx2 = PAD;
satTypeLabel.setBounds(kx2, Y_SLABEL2, KNOB_W, KNOB_LBL_H); satTypeLabel.setBounds(S(kx2, Y_SLABEL2, KNOB_W, KNOB_LBL_H));
satTypeCombo.setBounds(kx2, Y_SLABEL2 + KNOB_LBL_H + 2, KNOB_W + PAD, 24); satTypeCombo.setBounds(S(kx2, Y_SLABEL2 + KNOB_LBL_H + 2, KNOB_W + PAD, 24));
kx2 += KNOB_W + PAD + PAD + 8; kx2 += KNOB_W + PAD + PAD + 8;
place2(kTiltLow, kx2, Y_ROW2); place2(kTiltLow, kx2, Y_ROW2);
place2(kTiltMid, kx2, Y_ROW2); place2(kTiltMid, kx2, Y_ROW2);
@ -439,10 +390,10 @@ void FDNReverbEditor::resized()
// preset UI (always, mode-independent) // preset UI (always, mode-independent)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// layout (PRESET_PANEL_X=632 starting from ): // layout (PRESET_PANEL_X=632 starting from ):
// //
// top row Y=Y_ROW2: [<(26)] gap4 [combo(154)] gap4 [>(26)] -> right edge 848 // 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 // bottom row Y=Y_ROW2+34:[SAVE(104)] gap8 [DELETE(104)] -> right edge 848
// //
// separator vertical line : PRESET_PANEL_X - 9 = 623 // separator vertical line : PRESET_PANEL_X - 9 = 623
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
{ {
@ -450,31 +401,26 @@ void FDNReverbEditor::resized()
const int btnH = 26; const int btnH = 26;
// top row // top row
presetPrevButton.setBounds(px, Y_ROW2, 26, btnH); presetPrevButton.setBounds(S(px, Y_ROW2, 26, btnH));
presetCombo.setBounds(px + 30, Y_ROW2, 154, btnH); presetCombo.setBounds(S(px + 30, Y_ROW2, 154, btnH));
presetNextButton.setBounds(px + 188, Y_ROW2, 26, btnH); presetNextButton.setBounds(S(px + 188, Y_ROW2, 26, btnH));
// --- after changes --- // bottom row : [SAVE(68)] [LOAD(68)] [DELETE(68)]
// bottom row : [SAVE(68)] [LOAD(68)] [DELETE(68)] presetSaveButton.setBounds(S(px, Y_ROW2 + 34, 68, btnH));
presetSaveButton.setBounds(px, Y_ROW2 + 34, 68, btnH); presetLoadButton.setBounds(S(px + 72, Y_ROW2 + 34, 68, btnH));
presetLoadButton.setBounds(px + 72, Y_ROW2 + 34, 68, btnH); presetDeleteButton.setBounds(S(px + 144, Y_ROW2 + 34, 68, btnH));
presetDeleteButton.setBounds(px + 144, Y_ROW2 + 34, 68, btnH);
} }
// -- Visualizers -- // -- Visualizer --
const int vizTotalH = H - Y_VIZ - STATUS_H - PAD; const int decayHeight = 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;
rt60Viz.setBounds(PAD, Y_VIZ, W - PAD * 2, rt60Height); decayCurveViz.setBounds(S(PAD, Y_VIZ, W - PAD * 2, decayHeight));
decayCurveViz.setBounds(PAD, decayY, W - PAD * 2, decayHeight);
// -- AcousticMetrics -- // -- AcousticMetrics --
const int metricsRight = W - PAD - 8; const int metricsRight = W - PAD - 8;
const int metricsW = 280; const int metricsW = 280;
const int metricsLeft = metricsRight - metricsW; const int metricsLeft = metricsRight - metricsW;
const int metricsTop = decayY + 6; const int metricsTop = Y_VIZ + 6;
const int metricsRowH = 14; const int metricsRowH = 14;
const int metricsRow1Y = metricsTop + 14; const int metricsRow1Y = metricsTop + 14;
const int metricsRow2Y = metricsRow1Y + metricsRowH; const int metricsRow2Y = metricsRow1Y + metricsRowH;
@ -483,37 +429,33 @@ void FDNReverbEditor::resized()
const int colSpacing = 8; const int colSpacing = 8;
const int colW = captionW + valueW; const int colW = captionW + valueW;
labelMetricsTitle.setBounds(metricsLeft, metricsTop, 80, 12); labelMetricsTitle.setBounds(S(metricsLeft, metricsTop, 80, 12));
labelD50Caption.setBounds(metricsLeft, metricsRow1Y, captionW, metricsRowH); labelD50Caption.setBounds(S(metricsLeft, metricsRow1Y, captionW, metricsRowH));
labelD50Value.setBounds(metricsLeft + captionW, metricsRow1Y, valueW, metricsRowH); labelD50Value.setBounds(S(metricsLeft + captionW, metricsRow1Y, valueW, metricsRowH));
labelC50Caption.setBounds(metricsLeft + colW + colSpacing, metricsRow1Y, captionW, metricsRowH); labelC50Caption.setBounds(S(metricsLeft + colW + colSpacing, metricsRow1Y, captionW, metricsRowH));
labelC50Value.setBounds(metricsLeft + colW + colSpacing + captionW, metricsRow1Y, valueW, metricsRowH); labelC50Value.setBounds(S(metricsLeft + colW + colSpacing + captionW, metricsRow1Y, valueW, metricsRowH));
labelC80Caption.setBounds(metricsLeft, metricsRow2Y, captionW, metricsRowH); labelC80Caption.setBounds(S(metricsLeft, metricsRow2Y, captionW, metricsRowH));
labelC80Value.setBounds(metricsLeft + captionW, metricsRow2Y, valueW, metricsRowH); labelC80Value.setBounds(S(metricsLeft + captionW, metricsRow2Y, valueW, metricsRowH));
labelEDTCaption.setBounds(metricsLeft + colW + colSpacing, metricsRow2Y, captionW, metricsRowH); labelEDTCaption.setBounds(S(metricsLeft + colW + colSpacing, metricsRow2Y, captionW, metricsRowH));
labelEDTValue.setBounds(metricsLeft + colW + colSpacing + captionW, metricsRow2Y, valueW, metricsRowH); labelEDTValue.setBounds(S(metricsLeft + colW + colSpacing + captionW, metricsRow2Y, valueW, metricsRowH));
// -- Git build-info status bar -- // -- 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.fillAll(AmbivalenceColors::Background);
g.addTransform(juce::AffineTransform::scale(uiScale));
juce::ColourGradient grad( juce::ColourGradient grad(
AmbivalenceColors::Surface.withAlpha(0.12f), 0.f, 0.f, AmbivalenceColors::Surface.withAlpha(0.12f), 0.f, 0.f,
AmbivalenceColors::Background, 0.f, (float)H, false); AmbivalenceColors::Background, 0.f, (float)H, false);
g.setGradientFill(grad); g.setGradientFill(grad);
g.fillAll(); g.fillRect(0.f, 0.f, (float)W, (float)H);
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.setColour(AmbivalenceColors::Separator); g.setColour(AmbivalenceColors::Separator);
g.drawHorizontalLine(Y_SEP, (float)PAD, (float)(W - PAD)); 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); g.drawText(t, x, y, 200, 14, juce::Justification::centredLeft);
}; };
if (!isProMode) { if (!proMode) {
// -- Row 1 separator -- // -- Row 1 separator --
g.setColour(AmbivalenceColors::Separator); g.setColour(AmbivalenceColors::Separator);
g.drawVerticalLine(SEP_TF, (float)Y_SLABEL1, (float)(Y_ROW1 + UNIT_H)); 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)); 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 // Preset UI helpers
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void FDNReverbEditor::refreshPresetCombo() void FDNReverbEditor::refreshPresetCombo()
{ {
presetCombo.clear(juce::dontSendNotification); content->presetCombo.clear(juce::dontSendNotification);
auto names = presetManager->getPresetNames(); auto names = presetManager->getPresetNames();
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
@ -606,29 +733,41 @@ void FDNReverbEditor::refreshPresetCombo()
presetManager->setCurrentPresetName(saved); 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()) { if (names.isEmpty()) {
presetCombo.addItem("-- No Presets --", 1); const int placeholderId = FDNReverb::NUM_ALGORITHMS + 1;
presetCombo.setSelectedItemIndex(0, juce::dontSendNotification); content->presetCombo.addItem("-- No Presets --", placeholderId);
presetDeleteButton.setEnabled(false); content->presetCombo.setSelectedId(placeholderId, juce::dontSendNotification);
presetLoadButton.setEnabled(false); content->presetDeleteButton.setEnabled(false);
presetPrevButton.setEnabled(false); content->presetLoadButton.setEnabled(false);
presetNextButton.setEnabled(false); content->presetPrevButton.setEnabled(false);
content->presetNextButton.setEnabled(false);
return; 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) 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(); int idx = presetManager->getCurrentPresetIndex();
if (idx >= 0) if (idx >= 0)
presetCombo.setSelectedItemIndex(idx, juce::dontSendNotification); content->presetCombo.setSelectedItemIndex(idx, juce::dontSendNotification);
else else
presetCombo.setSelectedItemIndex(0, juce::dontSendNotification); content->presetCombo.setSelectedItemIndex(0, juce::dontSendNotification);
presetDeleteButton.setEnabled(true); content->presetDeleteButton.setEnabled(true);
presetLoadButton.setEnabled(true); content->presetLoadButton.setEnabled(true);
presetPrevButton.setEnabled(names.size() > 1); content->presetPrevButton.setEnabled(names.size() > 1);
presetNextButton.setEnabled(names.size() > 1); content->presetNextButton.setEnabled(names.size() > 1);
} }
void FDNReverbEditor::savePresetWithDialog() void FDNReverbEditor::savePresetWithDialog()
@ -687,4 +826,4 @@ void FDNReverbEditor::deleteCurrentPreset()
}), }),
true true
); );
} }

View file

@ -6,31 +6,33 @@
#include "GUI/AmbivalenceUI.h" #include "GUI/AmbivalenceUI.h"
#include "GUI/DecayCurveViz.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: public:
explicit FDNReverbEditor(FDNReverbAudioProcessor&); explicit EditorContent(FDNReverbAudioProcessor& p, AmbivalenceLookAndFeel& laf);
~FDNReverbEditor() override;
void paint(juce::Graphics&) override; void paint(juce::Graphics&) override;
void resized() override; void resized() override;
private: void setProMode(bool pro);
void timerCallback() override;
void updatePanelVisibility();
// --- Preset UI helpers --- // --- zoom ---
void refreshPresetCombo(); static constexpr float kDefaultZoom = 1.5f;
void savePresetWithDialog(); float currentZoom{ kDefaultZoom };
void deleteCurrentPreset(); float uiScale{ 1.0f }; // derived from getWidth()/W, applied to every child bounds
std::function<void(float)> onZoomChange;
FDNReverbAudioProcessor& audioProcessor; AmbivalenceLookAndFeel* lookAndFeel{ nullptr };
AmbivalenceLookAndFeel laf;
// --- common --- // --- common ---
AlgorithmSelector algoSelector;
RT60Visualizer rt60Viz;
DecayCurveViz decayCurveViz; DecayCurveViz decayCurveViz;
VUMeter vuIn, vuOut; VUMeter vuIn, vuOut;
juce::Label titleLabel; juce::Label titleLabel;
@ -43,12 +45,13 @@ private:
juce::Label statusLabel; juce::Label statusLabel;
juce::TextButton zoomButton;
juce::TextButton proModeButton; juce::TextButton proModeButton;
juce::TextButton erSoloButton; juce::TextButton erSoloButton;
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> proModeAttachment; std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> proModeAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> erSoloAttachment; std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> erSoloAttachment;
bool isProMode{ false }; bool proMode{ false };
// --- Normal Mode --- // --- Normal Mode ---
ArcKnob kPreDelay, kRoomSize, kDecay; ArcKnob kPreDelay, kRoomSize, kDecay;
@ -69,18 +72,16 @@ private:
ArcKnob kLoCutPro, kHiCutPro; ArcKnob kLoCutPro, kHiCutPro;
// --- preset UI --- // --- preset UI ---
std::unique_ptr<PresetManager> presetManager;
juce::TextButton presetPrevButton; juce::TextButton presetPrevButton;
juce::ComboBox presetCombo; juce::ComboBox presetCombo;
juce::TextButton presetNextButton; juce::TextButton presetNextButton;
// existing presetSaveButton / presetDeleteButton next to added
juce::TextButton presetSaveButton; juce::TextButton presetSaveButton;
juce::TextButton presetLoadButton; // * added juce::TextButton presetLoadButton;
juce::TextButton presetDeleteButton; juce::TextButton presetDeleteButton;
// --- layout constants --- // --- layout constants ---
static constexpr int W = 900; static constexpr int W = 900;
static constexpr int H = 540; static constexpr int H = 450;
static constexpr int PAD = 8; static constexpr int PAD = 8;
static constexpr int KNOB_W = 64; static constexpr int KNOB_W = 64;
static constexpr int KNOB_H = 72; static constexpr int KNOB_H = 72;
@ -93,5 +94,38 @@ private:
// DUCKING end (616) + gap(16) = 632 // DUCKING end (616) + gap(16) = 632
static constexpr int PRESET_PANEL_X = 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) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FDNReverbEditor)
}; };

View file

@ -17,7 +17,7 @@ public:
juce::AudioProcessorEditor* createEditor() override; juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override { return true; } 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; } double getTailLengthSeconds() const override { return 20.0; }
bool acceptsMidi() const override { return false; } bool acceptsMidi() const override { return false; }
bool producesMidi() const override { return false; } bool producesMidi() const override { return false; }

View file

@ -1,12 +1,37 @@
#include "PresetManager.h" #include "PresetManager.h"
#include "PluginProcessor.h" #include "PluginProcessor.h"
#include "FactoryPresets.h"
PresetManager::PresetManager(FDNReverbAudioProcessor& p) PresetManager::PresetManager(FDNReverbAudioProcessor& p)
: processor(p) : processor(p)
{ {
seedFactoryPresets();
refreshPresetList(); 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 // filesystem
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------

View file

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