From 1d9ba772fe1ad450fd0e7121f488b1c94e47f492 Mon Sep 17 00:00:00 2001 From: Armin Date: Sat, 15 Aug 2026 18:31:51 +0200 Subject: [PATCH] UI changes, layout redefinition --- CMakeLists.txt | 16 +- Makefile | 36 ++- README.md | 26 +- Source/GUI/AmbivalenceUI.cpp | 218 ++----------- Source/GUI/AmbivalenceUI.h | 67 +--- Source/GUI/DecayCurveViz.cpp | 12 +- Source/GUI/DecayCurveViz.h | 2 + Source/PluginEditor.cpp | 579 ++++++++++++++++++++++------------- Source/PluginEditor.h | 78 +++-- Source/PluginProcessor.h | 2 +- Source/PresetManager.cpp | 25 ++ Source/PresetManager.h | 1 + 12 files changed, 535 insertions(+), 527 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c142d5d..c8769e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,7 +43,7 @@ juce_add_plugin(Ambivalence PLUGIN_MANUFACTURER_CODE "Otdk" PLUGIN_CODE "Ambi" FORMATS VST3 Standalone - PRODUCT_NAME "Ambivalence1.1" + PRODUCT_NAME "Ambivalence" PLUGIN_DESCRIPTION "16-channel FDN Algorithmic Reverb" IS_SYNTH FALSE NEEDS_MIDI_INPUT FALSE @@ -118,7 +118,18 @@ target_sources(Ambivalence PRIVATE Source/DSP/Saturator.h Source/DSP/OutputLimiter.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 # ============================================================================ target_include_directories(Ambivalence PRIVATE @@ -132,6 +143,7 @@ target_include_directories(Ambivalence PRIVATE # ============================================================================ target_link_libraries(Ambivalence PRIVATE + AmbivalenceFactoryPresets juce::juce_audio_basics juce::juce_audio_devices juce::juce_audio_formats diff --git a/Makefile b/Makefile index 7e17457..3f4ce34 100644 --- a/Makefile +++ b/Makefile @@ -3,25 +3,25 @@ # # Usage: # make clean Release build (VST3 + standalone) into build/ -# make install fresh build, remove stale installs, install cleanly +# make install remove stale installs, install built artifacts # make uninstall remove installed artifacts # make clean delete the build directory # make version print the current git branch + commit # make info print the resolved configuration # # Notes: -# - `make install` always performs a clean rebuild first, so it is fully -# self-sufficient. `make && make install` also works (it builds twice). +# - `make install` copies whatever is currently in build/, it does +# not build. Run `make` first if you want a fresh build. # - 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 # - Install targets (user-level, no sudo required): -# macOS: VST3: ~/Library/Audio/Plug-Ins/VST3/Ambivalence1.1.vst3 -# Standalone: ~/Applications/Ambivalence1.1.app -# Linux: VST3: ~/.local/lib/vst3/Ambivalence1.1.vst3 -# Standalone: ~/.local/bin/Ambivalence1.1 +# macOS: VST3: ~/Library/Audio/Plug-Ins/VST3/Ambivalence.vst3 +# Standalone: ~/Applications/Ambivalence.app +# Linux: VST3: ~/.local/lib/vst3/Ambivalence.vst3 +# Standalone: ~/.local/bin/Ambivalence # ============================================================================ -PRODUCT := Ambivalence1.1 +PRODUCT := Ambivalence UNAME_S := $(shell uname -s) @@ -65,6 +65,12 @@ CMAKE := cmake VST3_BUNDLE := $(VST3_DIR)/$(PRODUCT).vst3 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 OSX_FLAG := -DCMAKE_OSX_ARCHITECTURES="$(OSX_ARCH)" endif @@ -93,16 +99,20 @@ build: $(CMAKE) -S . -B "$(BUILD_DIR)" \ -DJUCE_PATH="$(JUCE_PATH)" \ -DCMAKE_BUILD_TYPE="$(CONFIG)" \ - $(OSX_FLAG) \ - -DCMAKE_INSTALL_PREFIX="$(PREFIX)" \ - -DJUCE_VST3_DIR="$(VST3_DIR)" + $(OSX_FLAG) $(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" rm -rf "$(VST3_BUNDLE)" "$(STANDALONE)" @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." @if [ -d "$(VST3_BUNDLE)" ]; then \ echo " VST3: $(VST3_BUNDLE)"; \ diff --git a/README.md b/README.md index 9d4f0e8..8e8b675 100644 --- a/README.md +++ b/README.md @@ -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: -- **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. -- **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). - **Preset Panel** - preset browser with SAVE / LOAD / DELETE controls. @@ -67,7 +67,7 @@ The UI is divided into five main zones: | 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. | | IN / OUT | Stereo VU meters for input and output signal levels. | @@ -75,7 +75,7 @@ The UI is divided into five main zones: ## 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 | | --------- | -------------------------------------------------------------- | ------------------------------ | @@ -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) @@ -197,9 +197,9 @@ Three knobs provide broad RT60 tilt control across frequency groups, applied sim | 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. | -| LO CUT (Pro) | 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. | +| 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 (EQ) | Same parameter as Normal Mode LO 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 | | -------------- | ----------- | -| 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. | | 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. | @@ -247,7 +247,7 @@ Four metrics, computed from the actual reverb output signal in real time, appear ## 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 @@ -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 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 @@ -423,7 +423,7 @@ For infinite, evolving pads with GOLDFOIL: - WIDTH 0.95, MOD AMT 0.45, MOD RATE 0.20 Hz - DIFFUSION 0.90 for maximum diffusion - 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 --- diff --git a/Source/GUI/AmbivalenceUI.cpp b/Source/GUI/AmbivalenceUI.cpp index d3f0fd9..ce4df95 100644 --- a/Source/GUI/AmbivalenceUI.cpp +++ b/Source/GUI/AmbivalenceUI.cpp @@ -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& 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(); } \ No newline at end of file diff --git a/Source/GUI/AmbivalenceUI.h b/Source/GUI/AmbivalenceUI.h index 7f1afaa..d4ab607 100644 --- a/Source/GUI/AmbivalenceUI.h +++ b/Source/GUI/AmbivalenceUI.h @@ -1,22 +1,21 @@ #pragma once #include -#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 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 buttons; - juce::AudioProcessorValueTreeState& apvts; - int currentAlgo{ 0 }; - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AlgorithmSelector) }; \ No newline at end of file diff --git a/Source/GUI/DecayCurveViz.cpp b/Source/GUI/DecayCurveViz.cpp index 1c91207..e1cac69 100644 --- a/Source/GUI/DecayCurveViz.cpp +++ b/Source/GUI/DecayCurveViz.cpp @@ -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), diff --git a/Source/GUI/DecayCurveViz.h b/Source/GUI/DecayCurveViz.h index 70c3d51..b37b465 100644 --- a/Source/GUI/DecayCurveViz.h +++ b/Source/GUI/DecayCurveViz.h @@ -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 : diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index eca22f7..c2e0379 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -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(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(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(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(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 ); -} \ No newline at end of file +} diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 5c079c8..057c54d 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -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 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 proModeAttachment; std::unique_ptr 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; 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 content; + std::unique_ptr presetManager; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FDNReverbEditor) -}; \ No newline at end of file +}; diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 654f323..ddb5b11 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -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; } diff --git a/Source/PresetManager.cpp b/Source/PresetManager.cpp index 457e72d..143ed82 100644 --- a/Source/PresetManager.cpp +++ b/Source/PresetManager.cpp @@ -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 // ----------------------------------------------------------------------------- diff --git a/Source/PresetManager.h b/Source/PresetManager.h index f05d474..7c6451e 100644 --- a/Source/PresetManager.h +++ b/Source/PresetManager.h @@ -48,6 +48,7 @@ public: private: void refreshPresetList(); + void seedFactoryPresets(); juce::File getPresetFile(const juce::String& name) const; FDNReverbAudioProcessor& processor;