diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 356777d..90bad33 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -59,7 +59,7 @@ jobs:
if: runner.os == 'Linux'
run: |
sudo apt-get update
- sudo apt-get install -y ninja-build pkg-config xvfb \
+ sudo apt-get install -y ninja-build pkg-config xvfb openbox x11-utils gdb \
libasound2-dev libx11-dev libxext-dev libxinerama-dev libxrandr-dev \
libxcursor-dev libgl1-mesa-dev libfreetype6-dev libfontconfig1-dev
@@ -92,12 +92,42 @@ jobs:
run: |
mkdir -p ui-captures
if [ "${{ runner.os }}" = "Windows" ]; then
- export SUPPRESSOR_UI_CAPTURE_DIR="$(pwd -W)/ui-captures"
+ SUPPRESSOR_UI_CAPTURE_DIR="$(pwd -W)/ui-captures"
else
- export SUPPRESSOR_UI_CAPTURE_DIR="$PWD/ui-captures"
+ SUPPRESSOR_UI_CAPTURE_DIR="$PWD/ui-captures"
fi
+ export SUPPRESSOR_UI_CAPTURE_DIR
if [ "${{ runner.os }}" = "Linux" ]; then
- xvfb-run -a ctest --test-dir build --output-on-failure -C Release -R suppressor-ui-unit
+ # Native JUCE peers need WM_PROTOCOLS and other window-manager atoms,
+ # which a bare Xvfb server does not supply. Variables below belong to
+ # the inner shell running on that display.
+ # shellcheck disable=SC2016
+ xvfb-run -a sh -c '
+ openbox --sm-disable >/tmp/suppressor-openbox.log 2>&1 &
+ wm_pid=$!
+ trap "kill $wm_pid 2>/dev/null || true" EXIT
+ ready=false
+ for attempt in $(seq 1 50); do
+ if xprop -root _NET_SUPPORTING_WM_CHECK | grep -q "window id"; then
+ ready=true
+ break
+ fi
+ sleep 0.1
+ done
+ if [ "$ready" != true ]; then
+ echo "Window manager did not start"
+ exit 1
+ fi
+ if ! ctest --test-dir build --output-on-failure -C Release -R suppressor-ui-unit; then
+ # Native X11 errors terminate the process rather than fail an
+ # assertion. Collect a stack while keeping the test gate failed.
+ export SUPPRESSOR_VST3_PATH="$PWD/build/Source/Suppressor_artefacts/Release/VST3/suppressor.vst3"
+ export SUPPRESSOR_CLAP_PATH="$PWD/build/Source/Suppressor_artefacts/Release/CLAP/suppressor.clap"
+ gdb --batch -ex "set breakpoint pending on" -ex "break _XDefaultError" \
+ -ex run -ex bt --args build/Source/SuppressorUITests --success || true
+ exit 1
+ fi
+ '
else
ctest --test-dir build --output-on-failure -C Release -R suppressor-ui-unit
fi
@@ -233,7 +263,7 @@ jobs:
for d in *.vst3; do [ -e "$d" ] && zip -qr "../suppressor-${short}-vst3.zip" "$d"; done && \
for f in *.clap; do [ -e "$f" ] && zip -qr "../suppressor-${short}-clap.zip" "$f"; done)
done
- ls -la *.zip
+ ls -la -- *.zip
- uses: actions/upload-artifact@v4
with:
diff --git a/README.md b/README.md
index 5dd4f69..1eee6e5 100644
--- a/README.md
+++ b/README.md
@@ -5,10 +5,10 @@ high-gain amp-sim and reamping chains. It sits on the raw DI *before* the amp
sim, strips pickup-borne hiss/buzz/EMI between notes, and leaves the guitar
body alone. Formats: **VST3** and **CLAP** (Windows, macOS, Linux).
-[](https://github.com/user1303836/suppressor/actions/workflows/ci.yml)
+[](https://github.com/affinedsp/suppressor/actions/workflows/ci.yml)
-
+
## What it does
@@ -30,14 +30,28 @@ biquads plus an envelope per channel).
## Controls
-| Section | Controls |
+| Control | What it does |
|---|---|
-| **Core** | Strength (crossover 8 kHz → 1.4 kHz), Threshold, Release (2–30 ms, exponential) |
-| **Gate** | Reduction / Tight Gate mode, Depth floor, Hysteresis, Hold, Adaptive Release |
-| **Detection** | Transient Cue, internal/external detector source, 0/1/2 ms lookahead |
-| **Hum Removal** | Learn-and-lock resonant dips on the 50/60 Hz family, strength, harmonic count |
-| **Multiband** | Optional 4/6-band learned-threshold mode (advanced; one-split is the default) |
-| **Output** | Delta (removed-signal) audition, output gain, detector/GR metering |
+| **Threshold** | Sets when highs are reduced. Raise it until noise between notes is suppressed. |
+| **Strength** | Sets how much of the high-frequency range is treated (8 kHz → 1.4 kHz). Zero is not bypass. |
+| **Release** | Sets how quickly suppression returns after a note (2–30 ms). Increase it to preserve tails. |
+| **Listen removed** | Auditions input minus processed audio; switch it off to hear the processed signal. This difference includes filter phase effects, not just noise. |
+
+Input and output meters show **peak dBFS**. The large readout shows
+**high-band gain reduction**, not whole-signal loudness loss; in multiband
+sessions it shows the deepest band reduction. Clip warnings, bypass, silence,
+learning, and stopped audio are labelled explicitly. Stopped meters clear
+instead of holding stale readings.
+
+Drag a dial to adjust; **Shift-drag** for fine control, **double-click / Return**
+for exact entry, and **Alt/Option-click / Home** to reset. Tab and arrow keys
+work too. Use your host's bypass to compare with the dry input.
+
+Existing sessions and automation remain compatible. The additional engine
+settings remain available in the host's parameter view, not in the simplified
+editor. **Host settings active** indicates non-default legacy settings; hover
+for details. In multiband mode, Threshold and Strength are inactive because
+that mode uses its own splits and learned thresholds.
## Building
@@ -58,6 +72,17 @@ cmake -S . -B build-tests -G Ninja -DSUPPRESSOR_BUILD_PLUGIN=OFF -DSUPPRESSOR_BU
cmake --build build-tests && ctest --test-dir build-tests --output-on-failure
```
+### UI and wrapper tests
+
+```bash
+cmake --build build --target SuppressorUITests
+ctest --test-dir build --output-on-failure
+```
+
+These also exercise native VST3/CLAP editor embedding. Linux needs a display
+with a window manager (CI uses Xvfb + Openbox). Set `SUPPRESSOR_UI_CAPTURE_DIR` to an absolute directory to
+save real editor captures at 100–200% scaling.
+
### CI
`.github/workflows/ci.yml` gates every push/PR on:
diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt
index 2602190..a0c2352 100644
--- a/Source/CMakeLists.txt
+++ b/Source/CMakeLists.txt
@@ -8,7 +8,7 @@ juce_add_binary_data(SuppressorAssets
juce_add_plugin(Suppressor
VERSION 0.1.0
COMPANY_NAME "Suppressor Audio"
- COMPANY_WEBSITE "https://github.com/user1303836/suppressor"
+ COMPANY_WEBSITE "https://github.com/affinedsp/suppressor"
COMPANY_EMAIL "dev@suppressor.audio"
PLUGIN_MANUFACTURER_CODE SpAu
PLUGIN_CODE Supr
@@ -56,6 +56,7 @@ if (SUPPRESSOR_BUILD_TESTS)
add_executable(SuppressorUITests)
target_sources(SuppressorUITests PRIVATE
+ ../Tests/test_ui_main.cpp
../Tests/test_ui.cpp)
target_include_directories(SuppressorUITests PRIVATE
@@ -63,14 +64,33 @@ if (SUPPRESSOR_BUILD_TESTS)
target_link_libraries(SuppressorUITests PRIVATE
Suppressor
- doctest::doctest_with_main
+ doctest::doctest
juce::juce_audio_utils
juce::juce_recommended_config_flags
juce::juce_recommended_warning_flags)
target_compile_definitions(SuppressorUITests PRIVATE
JUCE_WEB_BROWSER=0
- JUCE_USE_CURL=0)
+ JUCE_USE_CURL=0
+ JUCE_MODAL_LOOPS_PERMITTED=1
+ JUCE_PLUGINHOST_VST3=1
+ $<$:_CONSOLE=1>)
- add_test(NAME suppressor-ui-unit COMMAND SuppressorUITests)
+ # Keep assertion context in CTest's failure log, including native-window exits.
+ add_test(NAME suppressor-ui-unit COMMAND SuppressorUITests --success)
+ set_tests_properties(suppressor-ui-unit PROPERTIES ENVIRONMENT
+ "SUPPRESSOR_VST3_PATH=$>")
+ add_dependencies(SuppressorUITests Suppressor_VST3)
+ if (SUPPRESSOR_ENABLE_CLAP)
+ target_sources(SuppressorUITests PRIVATE ../Tests/test_clap_gui.cpp)
+ target_link_libraries(SuppressorUITests PRIVATE clap-core)
+ add_dependencies(SuppressorUITests Suppressor_CLAP)
+ if (APPLE)
+ set(clap_test_path "$")
+ else()
+ set(clap_test_path "$")
+ endif()
+ set_property(TEST suppressor-ui-unit APPEND PROPERTY ENVIRONMENT
+ "SUPPRESSOR_CLAP_PATH=${clap_test_path}")
+ endif()
endif()
diff --git a/Source/Metering.h b/Source/Metering.h
new file mode 100644
index 0000000..319dc54
--- /dev/null
+++ b/Source/Metering.h
@@ -0,0 +1,86 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+namespace suppressor
+{
+struct MeterSnapshot
+{
+ enum Flag : uint32_t { active = 1, bypassed = 2, removed = 4, multiband = 8,
+ learning = 16, inputClip = 32, outputClip = 64 };
+ float input = 0.0f, output = 0.0f, reduction = 0.0f;
+ uint32_t flags = 0, sequence = 0;
+};
+
+// Single audio writer, message-thread readers. Atomic fields make even a
+// rejected read data-race-free. The writer never waits for the UI.
+class Metering
+{
+public:
+ static_assert (std::atomic::is_always_lock_free);
+ static_assert (std::atomic::is_always_lock_free);
+
+ void reset() noexcept
+ {
+ in = out = reduction = inClipTime = outClipTime = 0.0f;
+ previousRoute = 0;
+ publish (0);
+ }
+
+ void push (float inputPeak, float outputPeak, float reductionDb,
+ int samples, double sampleRate, uint32_t flags) noexcept
+ {
+ if (samples <= 0 || sampleRate <= 0.0) return;
+ const auto route = flags & (MeterSnapshot::bypassed | MeterSnapshot::removed
+ | MeterSnapshot::multiband | MeterSnapshot::learning);
+ if (route != previousRoute)
+ in = out = reduction = inClipTime = outClipTime = 0.0f;
+ previousRoute = route;
+ const auto seconds = static_cast (samples / sampleRate);
+ const auto decay = std::exp (-seconds / 0.3f);
+ in = std::max (clean (inputPeak), in * decay);
+ out = std::max (clean (outputPeak), out * decay);
+ // Cap the display envelope, not just the drawing. A zero-gain startup
+ // sample must not leave a >100 dB peak hanging above the real depth.
+ reduction = (flags & (MeterSnapshot::bypassed | MeterSnapshot::learning)) != 0
+ ? 0.0f : std::max (std::clamp (clean (reductionDb), 0.0f, 60.0f),
+ reduction * std::exp (-seconds / 0.12f));
+ inClipTime = inputPeak >= 1.0f ? 1.0f : std::max (0.0f, inClipTime - seconds);
+ outClipTime = outputPeak >= 1.0f ? 1.0f : std::max (0.0f, outClipTime - seconds);
+ if (inClipTime > 0.0f) flags |= MeterSnapshot::inputClip;
+ if (outClipTime > 0.0f) flags |= MeterSnapshot::outputClip;
+ publish (flags | MeterSnapshot::active);
+ }
+
+ bool read (MeterSnapshot& result) const noexcept
+ {
+ // seq_cst across the atomic fields gives a simple portable coherent
+ // snapshot, not a seqlock around non-atomic (racy) storage.
+ for (int attempt = 0; attempt < 3; ++attempt)
+ {
+ const auto before = sequence.load();
+ if ((before & 1u) != 0) continue;
+ MeterSnapshot s { input.load(), output.load(), gr.load(), status.load(), before };
+ if (before == sequence.load()) { result = s; return true; }
+ }
+ return false;
+ }
+
+private:
+ static float clean (float v) noexcept { return std::isfinite (v) ? std::max (0.0f, v) : 0.0f; }
+ void publish (uint32_t flags) noexcept
+ {
+ sequence.fetch_add (1);
+ input.store (in); output.store (out); gr.store (reduction); status.store (flags);
+ sequence.fetch_add (1);
+ }
+
+ float in = 0.0f, out = 0.0f, reduction = 0.0f, inClipTime = 0.0f, outClipTime = 0.0f;
+ uint32_t previousRoute = 0;
+ std::atomic input { 0 }, output { 0 }, gr { 0 };
+ std::atomic status { 0 }, sequence { 0 };
+};
+} // namespace suppressor
diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp
index ee04536..d4b96b4 100644
--- a/Source/PluginEditor.cpp
+++ b/Source/PluginEditor.cpp
@@ -1,668 +1,349 @@
#include "PluginEditor.h"
+#include
-#include
-
-class ActivityDisplay::ValueInterface final : public juce::AccessibilityValueInterface
+namespace
{
-public:
- explicit ValueInterface (ActivityDisplay& displayToUse) : display (displayToUse) {}
-
- bool isReadOnly() const override { return true; }
- double getCurrentValue() const override { return display.reduction; }
- juce::String getCurrentValueAsString() const override
- {
- return display.accessibleValueText();
- }
- void setValue (double) override {}
- void setValueAsString (const juce::String&) override {}
- AccessibleValueRange getRange() const override { return {}; }
+using namespace SuppressorTheme;
+const auto bg = canvas, panel = surface, line = divider, text = primaryText,
+ secondary = secondaryText, neutral = controlOutline;
-private:
- ActivityDisplay& display;
-};
-
-void ActivityDisplay::setState (float detectorDb, float gainReductionDb,
- float thresholdDb, int bandMode, bool deltaAudition,
- bool multibandLearning)
+void label (juce::Graphics& g, const juce::String& s, juce::Rectangle r,
+ float size, juce::Colour colour, juce::Justification align = juce::Justification::left)
{
- const auto previousStatus = semanticStatus();
- const auto newReduction = juce::jmax (0.0f, -gainReductionDb);
- const auto changed = juce::roundToInt (detector * 10.0f)
- != juce::roundToInt (detectorDb * 10.0f)
- || juce::roundToInt (reduction * 10.0f)
- != juce::roundToInt (newReduction * 10.0f)
- || juce::roundToInt (threshold * 10.0f)
- != juce::roundToInt (thresholdDb * 10.0f)
- || mode != bandMode || delta != deltaAudition
- || learning != multibandLearning;
-
- if (! changed)
- return;
-
- detector = detectorDb;
- reduction = newReduction;
- threshold = thresholdDb;
- mode = bandMode;
- delta = deltaAudition;
- learning = multibandLearning;
- setDescription (accessibleValueText());
- repaint();
-
- if (semanticStatus() != previousStatus)
- if (auto* handler = getAccessibilityHandler())
- handler->notifyAccessibilityEvent (juce::AccessibilityEvent::valueChanged);
+ g.setColour (colour);
+ g.setFont (SuppressorTheme::makeFont (size, size >= 20.0f));
+ g.drawText (s, r, align);
}
+float db (float peak) { return juce::Decibels::gainToDecibels (peak, -100.0f); }
+juce::String levelText (float peak) { return peak < 0.00001f ? "-inf" : juce::String (db (peak), 1); }
+} // namespace
-juce::String ActivityDisplay::semanticStatus() const
+SuppressorDial::SuppressorDial (juce::AudioProcessorValueTreeState& state, const juce::String& id,
+ const juce::String& help)
+ : parameter (*state.getParameter (id)), attachment (state, id, *this)
{
- if (delta)
- return "OUTPUT: REMOVED SIGNAL";
- if (learning)
- return "LEARN ARMED";
- if (mode > 0)
- return "MULTIBAND MODE";
- if (detector <= -120.0f)
- return "NO SIGNAL";
- return reduction > 0.5f ? "REDUCTION OBSERVED" : "NO REDUCTION";
+ setComponentID (id);
+ setName (parameter.getName (64));
+ setTitle (getName());
+ setDescription (help);
+ setTooltip (help + " Double-click or Return to type. Shift-drag for fine adjustment. "
+ "Alt/Option-click to reset; right-click for the menu.");
+ setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
+ setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0);
+ setTextValueSuffix (" " + parameter.getLabel());
+ setRotaryParameters (juce::MathConstants::pi * 1.25f,
+ juce::MathConstants::pi * 2.75f, true);
+ setScrollWheelEnabled (false);
+ setWantsKeyboardFocus (true);
+ setMouseClickGrabsKeyboardFocus (true);
+ setDoubleClickReturnValue (false, 0.0);
+ addChildComponent (entry);
+ entry.setName (getName() + " exact value");
+ entry.setJustification (juce::Justification::centred);
+ entry.setFont (SuppressorTheme::makeFont (18.0f));
+ entry.onReturnKey = [this] { finishEntry (true); };
+ entry.onEscapeKey = [this] { finishEntry (false); };
+ entry.onFocusLost = [this] { finishEntry (false); };
}
-juce::String ActivityDisplay::accessibleValueText() const
+SuppressorDial::~SuppressorDial() { cancelInteraction(); }
+void SuppressorDial::resized()
{
- juce::String detectorText;
- if (mode > 0)
- detectorText = "per-band detector; global detector unavailable";
- else if (detector <= -120.0f)
- detectorText = "no input detected";
- else
- detectorText = "detector " + juce::String (detector, 1) + " decibels";
-
- const auto reductionText = learning
- ? juce::String ("gain reduction unavailable while multiband learning is armed; suppression passes while processing")
- : reduction >= 60.0f
- ? juce::String ("at least 60 decibels gain reduction")
- : juce::String (reduction, 1) + " decibels gain reduction";
- return "Last processing snapshot: " + semanticStatus().toLowerCase()
- + ", " + detectorText + ", " + reductionText;
+ juce::Slider::resized();
+ entry.setBounds (getLocalBounds().withSizeKeepingCentre (100, 32));
}
-
-std::unique_ptr ActivityDisplay::createAccessibilityHandler()
+void SuppressorDial::setWithGesture (double v)
{
- return std::make_unique (
- *this, juce::AccessibilityRole::staticText, juce::AccessibilityActions {},
- juce::AccessibilityHandler::Interfaces { std::make_unique (*this) });
+ juce::Slider::ScopedDragNotification gesture (*this);
+ setValue (v, juce::sendNotificationSync);
}
-
-void ActivityDisplay::paint (juce::Graphics& graphics)
+void SuppressorDial::mouseDown (const juce::MouseEvent& e)
{
- auto bounds = getLocalBounds();
- auto statusBounds = bounds.removeFromTop (54);
- const auto status = semanticStatus();
- const auto statusColour = status == "NO SIGNAL" || status == "LEARN ARMED"
- || status == "OUTPUT: REMOVED SIGNAL"
- ? SuppressorTheme::warning
- : SuppressorTheme::accent;
-
- graphics.setColour (statusColour);
- graphics.fillRoundedRectangle (static_cast (statusBounds.getX()),
- static_cast (statusBounds.getY() + 8),
- 4.0f, 31.0f, 2.0f);
- graphics.setColour (SuppressorTheme::primaryText);
- graphics.setFont (SuppressorTheme::makeFont (27.0f, true, 0.02f));
- graphics.drawFittedText (status, statusBounds.withTrimmedLeft (14),
- juce::Justification::centredLeft, 1);
-
- auto drawCaption = [&graphics] (juce::Rectangle row, const juce::String& title,
- const juce::String& value)
+ if (! isEnabled()) return;
+ grabKeyboardFocus();
+ if (e.mods.isPopupMenu()) { showMenu(); return; }
+ if (e.mods.isAltDown())
{
- graphics.setColour (SuppressorTheme::secondaryText);
- graphics.setFont (SuppressorTheme::makeFont (11.5f, true, 0.09f));
- graphics.drawText (title, row.removeFromLeft (row.getWidth() * 2 / 3),
- juce::Justification::centredLeft);
- graphics.setColour (SuppressorTheme::primaryText);
- graphics.setFont (SuppressorTheme::makeFont (14.0f, true));
- graphics.drawText (value, row, juce::Justification::centredRight);
- };
-
- auto drawMeter = [&graphics] (juce::Rectangle meterBounds, float proportion)
- {
- auto track = meterBounds.toFloat();
- graphics.setColour (SuppressorTheme::divider);
- graphics.fillRoundedRectangle (track, 2.5f);
- track.setWidth (track.getWidth() * juce::jlimit (0.0f, 1.0f, proportion));
- graphics.setColour (SuppressorTheme::accent);
- graphics.fillRoundedRectangle (track, 2.5f);
- };
-
- bounds.removeFromTop (4);
- auto detectorLabel = bounds.removeFromTop (22);
- if (mode > 0)
- {
- drawCaption (detectorLabel, "DETECTOR", "PER-BAND");
- auto message = bounds.removeFromTop (25);
- graphics.setColour (SuppressorTheme::secondaryText);
- graphics.setFont (SuppressorTheme::makeFont (13.0f));
- graphics.drawFittedText ("GLOBAL LEVEL NOT EXPOSED", message,
- juce::Justification::centredLeft, 1);
- bounds.removeFromTop (13);
- }
- else
- {
- const auto detectorText = detector <= -120.0f
- ? juce::String ("NO SIGNAL")
- : juce::String (detector, 1) + " dB";
- drawCaption (detectorLabel, "DETECTOR", detectorText);
- auto meter = bounds.removeFromTop (7).reduced (0, 1);
- drawMeter (meter, juce::jmap (juce::jlimit (-80.0f, 0.0f, detector),
- -80.0f, 0.0f, 0.0f, 1.0f));
-
- const auto thresholdPosition = juce::jmap (juce::jlimit (-80.0f, 0.0f, threshold),
- -80.0f, 0.0f, 0.0f, 1.0f);
- const auto thresholdX = static_cast (meter.getX())
- + static_cast (meter.getWidth()) * thresholdPosition;
- graphics.setColour (SuppressorTheme::primaryText);
- graphics.drawVerticalLine (juce::roundToInt (thresholdX),
- static_cast (meter.getY() - 2),
- static_cast (meter.getBottom() + 2));
- bounds.removeFromTop (25);
- }
-
- auto reductionLabel = bounds.removeFromTop (22);
- if (learning)
- {
- drawCaption (reductionLabel, "GAIN REDUCTION", "UNAVAILABLE");
- graphics.setColour (SuppressorTheme::secondaryText);
- graphics.setFont (SuppressorTheme::makeFont (13.0f));
- graphics.drawFittedText ("SUPPRESSION PASSES WHILE PROCESSING",
- bounds.removeFromTop (25),
- juce::Justification::centredLeft, 1);
- }
- else
- {
- const auto reductionText = reduction >= 60.0f
- ? juce::String ("60+ dB")
- : juce::String (reduction, 1) + " dB";
- drawCaption (reductionLabel, "GAIN REDUCTION", reductionText);
- auto reductionMeter = bounds.removeFromTop (7).reduced (0, 1);
- drawMeter (reductionMeter, juce::jlimit (0.0f, 1.0f, reduction / 60.0f));
+ setWithGesture (parameter.convertFrom0to1 (parameter.getDefaultValue()));
+ return;
}
-
- bounds.removeFromTop (14);
- graphics.setColour (SuppressorTheme::secondaryText);
- graphics.setFont (SuppressorTheme::makeFont (12.5f));
- graphics.drawFittedText ("UPDATES WHILE PROCESSING", bounds.removeFromTop (18),
- juce::Justification::centredLeft, 1);
+ lastDrag = e.position;
+ dragProportion = valueToProportionOfLength (getValue());
+ drag = std::make_unique (*this);
}
-
-SuppressorEditor::SuppressorEditor (SuppressorProcessor& processorToUse)
- : juce::AudioProcessorEditor (&processorToUse), proc (processorToUse)
+void SuppressorDial::mouseDrag (const juce::MouseEvent& e)
{
- setLookAndFeel (&lookAndFeel);
- setOpaque (true);
-
- activityDisplay.setName ("Suppression activity");
- activityDisplay.setTitle ("Suppression activity");
- activityDisplay.setExplicitFocusOrder (nextFocusOrder++);
- addAndMakeVisible (activityDisplay);
-
- addCombo ("gateMode", "Gate Mode");
- addToggle ("adaptiveRelease", "Adaptive Release");
- auto& suppressionStrength = addKnob ("strength", "Strength");
- suppressionStrength.setName ("Suppression Strength");
- suppressionStrength.setTitle ("Suppression Strength");
- addKnob ("threshold", "Threshold");
- addKnob ("release", "Release");
- addKnob ("depth", "Depth");
- addKnob ("hysteresis", "Hysteresis");
- addKnob ("hold", "Hold");
-
- addKnob ("cue", "Transient Cue");
- addCombo ("sidechain", "Detector Source");
- addCombo ("lookahead", "Lookahead");
-
- addToggle ("humEnable", "Hum Removal");
- addCombo ("humBase", "Mains Family");
- addKnob ("humHarmonics", "Harmonics");
- addKnob ("humStrength", "Hum Strength");
- addAction ("humLearn", humLearnButton, "Learn hum profile");
-
- addCombo ("bandMode", "Band Mode");
- addAction ("bandsLearn", bandsLearnButton, "Learn multiband thresholds");
- addKnob ("outputGain", "Output Gain");
- addToggle ("deltaAudition", "Delta Audition");
-
- auto setupStatus = [this] (juce::Label& label, const juce::String& name)
- {
- label.setName (name);
- label.setTitle (name);
- label.setFont (SuppressorTheme::makeFont (13.0f, true, 0.03f));
- label.setColour (juce::Label::textColourId, SuppressorTheme::secondaryText);
- label.setJustificationType (juce::Justification::centredLeft);
- label.setMinimumHorizontalScale (0.72f);
- addAndMakeVisible (label);
- };
- setupStatus (humStatus, "Hum learning status");
- setupStatus (bandsStatus, "Multiband learning status");
-
- setSize (1000, 640);
- updateModePresentation();
- timerCallback();
- updateTimerState();
+ if (drag == nullptr) return;
+ const auto delta = e.position - lastDrag;
+ const double sensitivity = e.mods.isShiftDown() ? 2400.0 : 240.0;
+ // Accumulate sub-step movement so fine drags work on the 0.1 dB grid.
+ dragProportion = juce::jlimit (0.0, 1.0, dragProportion + (delta.x - delta.y) / sensitivity);
+ setValue (proportionOfLengthToValue (dragProportion), juce::sendNotificationSync);
+ lastDrag = e.position;
}
-
-SuppressorEditor::~SuppressorEditor()
+void SuppressorDial::mouseUp (const juce::MouseEvent&) { drag.reset(); }
+void SuppressorDial::mouseDoubleClick (const juce::MouseEvent& e)
{
- stopTimer();
- for (const auto& slider : sliders)
- slider->endActiveGesture();
- entries.clear();
- setLookAndFeel (nullptr);
+ if (! e.mods.isAltDown() && ! e.mods.isPopupMenu()) beginEntry();
}
-
-ParameterSlider& SuppressorEditor::addKnob (const juce::String& parameterId,
- const juce::String& labelText)
+void SuppressorDial::cancelInteraction()
{
- sliders.push_back (std::make_unique());
- auto* slider = sliders.back().get();
- addAndMakeVisible (*slider);
- slider->setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
- slider->setRotaryParameters (juce::MathConstants::pi * 1.25f,
- juce::MathConstants::pi * 2.75f, true);
- slider->setTextBoxStyle (juce::Slider::TextBoxBelow, false, 88, 24);
- slider->setMouseDragSensitivity (220);
- slider->setVelocityBasedMode (false);
- slider->setVelocityModeParameters (0.24, 1, 0.0, true,
- juce::ModifierKeys::shiftModifier);
- slider->setScrollWheelEnabled (false);
- slider->setWantsKeyboardFocus (true);
- slider->setExplicitFocusOrder (nextFocusOrder++);
- slider->setComponentID (parameterId);
- slider->setName (labelText);
- slider->setTitle (labelText);
- slider->setDescription ("Drag to adjust. Shift-drag for fine control. Double-click or press Return for exact entry. Alt/Option-click resets to default.");
- slider->setTooltip ("Return/double-click: exact. Shift-drag: fine. Alt/Option-click: reset.");
-
- auto entry = std::make_unique();
- entry->id = parameterId;
- entry->slider = slider;
- entry->label = std::make_unique();
- entry->label->setText (labelText.toUpperCase(), juce::dontSendNotification);
- entry->label->setFont (SuppressorTheme::makeFont (11.5f, true, 0.08f));
- entry->label->setColour (juce::Label::textColourId, SuppressorTheme::secondaryText);
- entry->label->setJustificationType (juce::Justification::centred);
- entry->label->setInterceptsMouseClicks (false, false);
- entry->label->setAccessible (false);
- addAndMakeVisible (*entry->label);
- entry->sliderAttachment = std::make_unique<
- juce::AudioProcessorValueTreeState::SliderAttachment> (proc.apvts, parameterId, *slider);
- if (auto* parameter = proc.apvts.getParameter (parameterId))
- {
- slider->setDoubleClickReturnValue (
- true, parameter->convertFrom0to1 (parameter->getDefaultValue()),
- juce::ModifierKeys::altModifier);
- slider->setKeyboardGestureCallbacks (
- [parameter] { parameter->beginChangeGesture(); },
- [parameter] { parameter->endChangeGesture(); });
- if (parameter->getLabel().isNotEmpty())
- slider->setTextValueSuffix (" " + parameter->getLabel());
- }
- entries.push_back (std::move (entry));
- return *slider;
+ drag.reset();
+ finishEntry (false);
}
-
-juce::ComboBox& SuppressorEditor::addCombo (const juce::String& parameterId,
- const juce::String& labelText)
+void SuppressorDial::enablementChanged()
{
- comboBoxes.push_back (std::make_unique());
- auto* combo = comboBoxes.back().get();
- if (auto* parameter = dynamic_cast (
- proc.apvts.getParameter (parameterId)))
- combo->addItemList (parameter->choices, 1);
- combo->setJustificationType (juce::Justification::centredLeft);
- combo->setWantsKeyboardFocus (true);
- combo->setExplicitFocusOrder (nextFocusOrder++);
- combo->setComponentID (parameterId);
- combo->setName (labelText);
- combo->setTitle (labelText);
- combo->setDescription ("Choose " + labelText.toLowerCase());
- combo->setTooltip (combo->getDescription());
- addAndMakeVisible (*combo);
-
- auto entry = std::make_unique();
- entry->id = parameterId;
- entry->combo = combo;
- entry->label = std::make_unique();
- entry->label->setText (labelText.toUpperCase(), juce::dontSendNotification);
- entry->label->setFont (SuppressorTheme::makeFont (11.5f, true, 0.08f));
- entry->label->setColour (juce::Label::textColourId, SuppressorTheme::secondaryText);
- entry->label->setJustificationType (juce::Justification::centredLeft);
- entry->label->setInterceptsMouseClicks (false, false);
- entry->label->setAccessible (false);
- addAndMakeVisible (*entry->label);
- entry->comboAttachment = std::make_unique<
- juce::AudioProcessorValueTreeState::ComboBoxAttachment> (proc.apvts, parameterId, *combo);
- entries.push_back (std::move (entry));
-
- if (parameterId == "gateMode" || parameterId == "bandMode")
- combo->onChange = [this]
- {
- updateModePresentation();
- timerCallback();
- };
-
- return *combo;
+ if (! isEnabled()) cancelInteraction();
+ repaint();
}
-
-juce::ToggleButton& SuppressorEditor::addToggle (const juce::String& parameterId,
- const juce::String& labelText)
+bool SuppressorDial::keyPressed (const juce::KeyPress& key)
{
- toggleButtons.push_back (std::make_unique (labelText.toUpperCase()));
- auto* button = toggleButtons.back().get();
- button->setWantsKeyboardFocus (true);
- button->setExplicitFocusOrder (nextFocusOrder++);
- button->setComponentID (parameterId);
- button->setName (labelText);
- button->setTitle (labelText);
- button->setTooltip ("Toggle " + labelText.toLowerCase());
- addAndMakeVisible (*button);
-
- auto entry = std::make_unique();
- entry->id = parameterId;
- entry->button = button;
- entry->buttonAttachment = std::make_unique<
- juce::AudioProcessorValueTreeState::ButtonAttachment> (proc.apvts, parameterId, *button);
- entries.push_back (std::move (entry));
-
- if (parameterId == "humEnable")
- button->onClick = [this] { updateModePresentation(); };
-
- return *button;
+ if (! isEnabled()) return false;
+ if (key == juce::KeyPress::returnKey) { beginEntry(); return true; }
+ const int code = key.getKeyCode();
+ if (code == juce::KeyPress::upKey || code == juce::KeyPress::rightKey
+ || code == juce::KeyPress::downKey || code == juce::KeyPress::leftKey)
+ {
+ const double step = getComponentID() == "strength" ? 0.01 : 0.1;
+ const int direction = code == juce::KeyPress::upKey || code == juce::KeyPress::rightKey ? 1 : -1;
+ const auto fine = key.getModifiers().isShiftDown() && getInterval() <= 0.0 ? 0.1 : 1.0;
+ setWithGesture (getValue() + direction * step * fine);
+ return true;
+ }
+ if (code == juce::KeyPress::homeKey)
+ {
+ setWithGesture (parameter.convertFrom0to1 (parameter.getDefaultValue()));
+ return true;
+ }
+ return juce::Slider::keyPressed (key);
}
-
-void SuppressorEditor::addAction (const juce::String& parameterId,
- juce::TextButton& button,
- const juce::String& accessibleName)
+void SuppressorDial::beginEntry()
{
- button.setClickingTogglesState (true);
- button.setWantsKeyboardFocus (true);
- button.setExplicitFocusOrder (nextFocusOrder++);
- button.setComponentID (parameterId);
- button.setName (accessibleName);
- button.setTitle (accessibleName);
- button.setTooltip (accessibleName);
- addAndMakeVisible (button);
-
- auto entry = std::make_unique();
- entry->id = parameterId;
- entry->button = &button;
- entry->buttonAttachment = std::make_unique<
- juce::AudioProcessorValueTreeState::ButtonAttachment> (proc.apvts, parameterId, button);
- entries.push_back (std::move (entry));
+ if (! isEnabled()) return;
+ drag.reset();
+ entry.setText (getTextFromValue (getValue()), false);
+ entry.setVisible (true);
+ entry.grabKeyboardFocus();
+ entry.selectAll();
}
-
-SuppressorEditor::Entry* SuppressorEditor::findEntry (const juce::String& parameterId) const
+void SuppressorDial::finishEntry (bool commit)
{
- for (const auto& entry : entries)
- if (entry->id == parameterId)
- return entry.get();
- return nullptr;
+ if (! entry.isVisible()) return;
+ const auto typed = entry.getText().trim();
+ entry.setVisible (false);
+ if (commit)
+ {
+ // Accept one finite number plus an optional matching unit. Do not let
+ // the parameter's permissive parser turn malformed input into zero.
+ auto number = typed;
+ const auto unit = parameter.getLabel();
+ if (number.endsWithIgnoreCase (unit)) number = number.dropLastCharacters (unit.length()).trimEnd();
+ const auto utf8 = number.toRawUTF8();
+ char* end = nullptr;
+ const auto value = std::strtod (utf8, &end);
+ if (number.containsOnly ("0123456789.+-") && end != utf8 && *end == '\0' && std::isfinite (value))
+ {
+ const double plain = getComponentID() == "strength" ? value / 100.0 : value;
+ setWithGesture (plain);
+ }
+ grabKeyboardFocus();
+ }
+ repaint();
}
-
-void SuppressorEditor::placeKnob (const juce::String& parameterId,
- juce::Rectangle bounds)
+void SuppressorDial::showMenu()
{
- if (auto* entry = findEntry (parameterId))
+ juce::PopupMenu menu;
+ const juce::Component::SafePointer safe (this);
+ menu.addItem ("Enter value...", [safe] { if (safe != nullptr) safe->beginEntry(); });
+ menu.addItem ("Reset to default", [safe]
{
- entry->label->setBounds (bounds.removeFromTop (18));
- entry->slider->setBounds (bounds);
- }
+ if (safe != nullptr)
+ safe->setWithGesture (safe->parameter.convertFrom0to1 (safe->parameter.getDefaultValue()));
+ });
+ if (auto* editor = findParentComponentOfClass())
+ if (auto* host = editor->getHostContext())
+ if (auto hostMenu = host->getContextMenuForParameter (¶meter))
+ menu.addSubMenu ("Host", hostMenu->getEquivalentPopupMenu());
+ menu.showMenuAsync (juce::PopupMenu::Options().withTargetComponent (this));
}
-void SuppressorEditor::placeCombo (const juce::String& parameterId,
- juce::Rectangle bounds)
+SuppressorEditor::SuppressorEditor (SuppressorProcessor& p)
+ : juce::AudioProcessorEditor (&p), proc (p),
+ threshold (p.apvts, "threshold", "Highs below this level are reduced. Raise it until noise between notes is suppressed."),
+ strength (p.apvts, "strength", "Higher strength suppresses a wider high-frequency range. Zero is not bypass."),
+ release (p.apvts, "release", "How quickly suppression returns after a note. Increase to preserve longer tails."),
+ listenAttachment (p.apvts, "deltaAudition", listen)
{
- if (auto* entry = findEntry (parameterId))
+ setLookAndFeel (&theme);
+ int order = 1;
+ for (auto* dial : { &threshold, &strength, &release })
+ {
+ addAndMakeVisible (*dial);
+ dial->setExplicitFocusOrder (order++);
+ }
+ listen.setClickingTogglesState (true);
+ listen.setComponentID ("deltaAudition");
+ listen.setTitle ("Listen removed");
+ listen.setTooltip ("Audition input minus processed audio, including filter phase differences; not isolated noise. Turn off to hear the processed signal.");
+ listen.setExplicitFocusOrder (4);
+ addAndMakeVisible (listen);
+ for (auto* l : { &status, &meterSummary, &hostSettings })
{
- entry->label->setBounds (bounds.getX(), bounds.getY() - 17,
- bounds.getWidth(), 15);
- entry->combo->setBounds (bounds);
+ addAndMakeVisible (*l);
+ l->setFont (SuppressorTheme::makeFont (16.0f));
+ l->setBorderSize (juce::BorderSize (0));
}
+ status.setJustificationType (juce::Justification::centredRight);
+ status.setName ("Processing status");
+ meterSummary.setName ("Signal meters");
+ meterSummary.setTooltip ("Input and output: peak across channels, dBFS. Reduction: deepest gate attenuation, not overall loudness loss. 300 ms peak decay; clips held for one second.");
+ hostSettings.setJustificationType (juce::Justification::centredRight);
+ hostSettings.setName ("Host settings");
+ setSize (640, 460);
+ // A reopened editor must see a NEW audio block before calling data live.
+ proc.readMeters (meter);
+ lastSequence = meter.sequence;
+ lastUpdateMs = juce::Time::getMillisecondCounterHiRes() - 1000.0;
+ status.setText ("No audio", juce::dontSendNotification);
+ timerCallback();
+ startTimerHz (30);
}
-
-void SuppressorEditor::placeButton (const juce::String& parameterId,
- juce::Rectangle bounds)
+SuppressorEditor::~SuppressorEditor()
{
- if (auto* entry = findEntry (parameterId))
- entry->button->setBounds (bounds);
+ stopTimer();
+ setLookAndFeel (nullptr);
}
-
void SuppressorEditor::resized()
{
- coreBounds = { 18, 82, 650, 250 };
- activityBounds = { 680, 82, 302, 250 };
- detectionBounds = { 18, 344, 262, 278 };
- humBounds = { 292, 344, 392, 278 };
- outputBounds = { 696, 344, 286, 278 };
-
- placeCombo ("gateMode", { coreBounds.getX() + 14, coreBounds.getY() + 51, 168, 34 });
- placeButton ("adaptiveRelease",
- { coreBounds.getX() + 196, coreBounds.getY() + 51, 178, 34 });
-
- const auto knobWidth = (coreBounds.getWidth() - 28) / 6;
- const juce::StringArray coreKnobs { "strength", "threshold", "release",
- "depth", "hysteresis", "hold" };
- for (int index = 0; index < coreKnobs.size(); ++index)
- placeKnob (coreKnobs[index],
- { coreBounds.getX() + 14 + index * knobWidth,
- coreBounds.getY() + 94, knobWidth, 142 });
-
- auto activityContent = activityBounds.reduced (14);
- activityContent.removeFromTop (28);
- activityDisplay.setBounds (activityContent);
-
- placeKnob ("cue", { detectionBounds.getX() + 14, detectionBounds.getY() + 41,
- 110, 218 });
- placeCombo ("sidechain",
- { detectionBounds.getX() + 138, detectionBounds.getY() + 69, 110, 34 });
- placeCombo ("lookahead",
- { detectionBounds.getX() + 138, detectionBounds.getY() + 145, 110, 34 });
-
- placeButton ("humEnable",
- { humBounds.getX() + 14, humBounds.getY() + 48, 132, 34 });
- placeCombo ("humBase",
- { humBounds.getX() + 250, humBounds.getY() + 48, 128, 34 });
- placeKnob ("humHarmonics",
- { humBounds.getX() + 14, humBounds.getY() + 96, 108, 164 });
- placeKnob ("humStrength",
- { humBounds.getX() + 126, humBounds.getY() + 96, 108, 164 });
- placeButton ("humLearn",
- { humBounds.getX() + 250, humBounds.getY() + 111, 128, 36 });
- humStatus.setBounds (humBounds.getX() + 250, humBounds.getY() + 154, 128, 62);
-
- placeCombo ("bandMode",
- { outputBounds.getX() + 14, outputBounds.getY() + 50, 122, 34 });
- placeButton ("bandsLearn",
- { outputBounds.getX() + 150, outputBounds.getY() + 50, 122, 34 });
- bandsStatus.setBounds (outputBounds.getX() + 14, outputBounds.getY() + 91, 258, 25);
- placeKnob ("outputGain",
- { outputBounds.getX() + 14, outputBounds.getY() + 139, 110, 124 });
- placeButton ("deltaAudition",
- { outputBounds.getX() + 140, outputBounds.getY() + 175, 132, 38 });
-}
-
-void SuppressorEditor::updateModePresentation()
-{
- const auto bandMode = findEntry ("bandMode")->combo->getSelectedItemIndex();
- const auto gateMode = findEntry ("gateMode")->combo->getSelectedItemIndex();
- const auto humEnabled = findEntry ("humEnable")->button->getToggleState();
-
- auto setEmphasis = [this] (const juce::String& parameterId, bool active)
- {
- if (auto* entry = findEntry (parameterId))
- if (entry->slider != nullptr)
- {
- entry->slider->setColour (juce::Slider::rotarySliderFillColourId,
- active ? SuppressorTheme::accent
- : SuppressorTheme::inactiveControl);
- entry->slider->repaint();
- }
- };
-
- setEmphasis ("strength", bandMode <= 0);
- setEmphasis ("threshold", bandMode <= 0);
- setEmphasis ("depth", gateMode <= 0);
- setEmphasis ("humHarmonics", humEnabled);
- setEmphasis ("humStrength", humEnabled);
-
- auto setApplicability = [this] (const juce::String& parameterId, bool active,
- const juce::String& inactiveReason)
- {
- if (auto* entry = findEntry (parameterId))
- if (entry->slider != nullptr)
- {
- const auto interaction = " Drag to adjust; Shift-drag is fine control; double-click or Return opens exact entry; Alt/Option-click resets.";
- const auto description = active
- ? entry->slider->getName() + "." + interaction
- : "Inactive but editable. " + inactiveReason + interaction;
- entry->slider->setDescription (description);
- entry->slider->setTooltip (active
- ? "Return/double-click: exact. Shift-drag: fine. Alt/Option-click: reset."
- : inactiveReason + " Still editable.");
- }
- };
-
- setApplicability ("strength", bandMode <= 0, "Applies only in One-Split mode.");
- setApplicability ("threshold", bandMode <= 0, "Global Threshold applies only in One-Split mode.");
- setApplicability ("depth", gateMode <= 0, "Tight Gate uses a hard-zero floor; this Reduction depth is retained for automation and preconfiguration.");
- setApplicability ("humHarmonics", humEnabled, "Hum Removal is off; this setting is retained.");
- setApplicability ("humStrength", humEnabled, "Hum Removal is off; this setting is retained.");
-}
-
-void SuppressorEditor::timerCallback()
-{
- const auto desiredIntervalMs = isShowing() ? 50 : 500;
- if (getTimerInterval() != desiredIntervalMs)
- startTimer (desiredIntervalMs);
-
- const auto bandMode = juce::jmax (0, findEntry ("bandMode")->combo->getSelectedItemIndex());
- const auto threshold = static_cast (findEntry ("threshold")->slider->getValue());
- const auto deltaAudition = findEntry ("deltaAudition")->button->getToggleState();
- const auto bandsLearning = bandsLearnButton.getToggleState();
- activityDisplay.setState (proc.detectorDb(), proc.gainReductionDb(), threshold,
- bandMode, deltaAudition, bandsLearning && bandMode > 0);
-
- const auto humLearning = humLearnButton.getToggleState();
- humLearnButton.setButtonText (humLearning ? "STOP LEARNING" : "LEARN");
- humLearnButton.setName (humLearning ? "Stop hum learning" : "Learn hum profile");
- humLearnButton.setTitle (humLearnButton.getName());
- humLearnButton.setTooltip (humLearning
- ? "Stop the armed request. Capture and hum pass-through occur while processing."
- : "Arm hum-profile learning; capture begins while processing.");
- humStatus.setText (humLearning ? "ARMED / PASSES IN PROCESS"
- : "READY TO LEARN",
- juce::dontSendNotification);
-
- const auto multibandMode = bandMode > 0;
- bandsLearnButton.setButtonText (bandsLearning ? "STOP LEARNING"
- : multibandMode ? "LEARN"
- : "ARM LEARNING");
- bandsLearnButton.setName (bandsLearning ? "Stop multiband learning"
- : multibandMode ? "Learn multiband thresholds"
- : "Arm multiband learning");
- bandsLearnButton.setTitle (bandsLearnButton.getName());
- bandsLearnButton.setTooltip (
- bandsLearning
- ? multibandMode
- ? "Stop the armed request. Suppression passes while learning during processing."
- : "Stop the armed request; it is waiting for 4/6-Band."
- : multibandMode
- ? "Arm multiband learning; suppression passes while processing."
- : "Arm multiband learning now; capture waits for 4/6-Band processing.");
- if (bandsLearning)
- bandsStatus.setText (multibandMode ? "ARMED / PASSES WHILE PROCESSING"
- : "ARMED / WAITING FOR 4/6-BAND",
- juce::dontSendNotification);
- else
- bandsStatus.setText (multibandMode ? "READY TO LEARN"
- : "CAN ARM FOR 4/6-BAND",
- juce::dontSendNotification);
-
- updateModePresentation();
+ status.setBounds (365, 24, 251, 28);
+ meterSummary.setBounds (36, 224, 355, 18);
+ threshold.setBounds (59, 277, 122, 116);
+ strength.setBounds (259, 277, 122, 116);
+ release.setBounds (459, 277, 122, 116);
+ listen.setBounds (24, 412, 160, 32);
+ hostSettings.setBounds (464, 419, 152, 20);
}
-
void SuppressorEditor::visibilityChanged()
{
- updateTimerState();
+ if (! isShowing())
+ for (auto* dial : { &threshold, &strength, &release }) dial->cancelInteraction();
}
-
-void SuppressorEditor::updateTimerState()
+int SuppressorEditor::getControlParameterIndex (juce::Component& c)
{
- startTimer (isShowing() ? 50 : 500);
+ for (auto* dial : { &threshold, &strength, &release })
+ if (&c == dial || dial->isParentOf (&c)) return dial->parameter.getParameterIndex();
+ if (&c == &listen || listen.isParentOf (&c))
+ return proc.apvts.getParameter ("deltaAudition")->getParameterIndex();
+ return -1;
}
-
-int SuppressorEditor::getControlParameterIndex (juce::Component& component)
+void SuppressorEditor::timerCallback()
{
- for (const auto& entry : entries)
+ const auto now = juce::Time::getMillisecondCounterHiRes();
+ suppressor::MeterSnapshot next;
+ if (proc.readMeters (next) && next.sequence != lastSequence)
{
- auto* control = entry->slider != nullptr
- ? static_cast (entry->slider)
- : (entry->combo != nullptr
- ? static_cast (entry->combo)
- : static_cast (entry->button));
- if (control == &component || control->isParentOf (&component))
- return proc.getParameters().indexOf (proc.apvts.getParameter (entry->id));
+ meter = next;
+ lastSequence = next.sequence;
+ lastUpdateMs = now;
}
-
- return -1;
+ fresh = (meter.flags & suppressor::MeterSnapshot::active) != 0 && now - lastUpdateMs < 500.0;
+ // A parameter change may precede the next audio block (or happen while
+ // stopped). Do not relabel old output/reduction as the new routing mode.
+ if ((meter.flags & suppressor::MeterSnapshot::bypassed) == 0)
+ fresh = fresh
+ && (((meter.flags & suppressor::MeterSnapshot::removed) != 0) == listen.getToggleState())
+ && (((meter.flags & suppressor::MeterSnapshot::multiband) != 0)
+ == (proc.apvts.getRawParameterValue ("bandMode")->load() > 0.5f));
+ if (! isShowing()) return;
+
+ using M = suppressor::MeterSnapshot;
+ const bool bypass = fresh && (meter.flags & M::bypassed) != 0;
+ const bool learning = fresh && (meter.flags & M::learning) != 0;
+ const bool audition = listen.getToggleState() && ! bypass;
+ listen.setButtonText (listen.getToggleState() ? "Stop listening" : "Listen removed");
+ juce::String state = ! fresh ? "No audio" : bypass ? "Bypassed" : learning ? "Learning bands"
+ : meter.input < 0.00001f ? "No input" : audition ? "Listening to difference"
+ : meter.reduction > 0.5f ? "Suppressing" : "Passing signal";
+ status.setText (state, juce::dontSendNotification);
+ status.setColour (juce::Label::textColourId, audition ? warning : text);
+ const bool multi = proc.apvts.getRawParameterValue ("bandMode")->load() > 0.5f;
+ threshold.setEnabled (! multi);
+ strength.setEnabled (! multi);
+
+ juce::StringArray changed;
+ for (auto* param : proc.getParameters())
+ if (auto* ranged = dynamic_cast (param))
+ if (ranged->paramID != "threshold" && ranged->paramID != "strength"
+ && ranged->paramID != "release" && ranged->paramID != "deltaAudition"
+ && std::abs (ranged->getValue() - ranged->getDefaultValue()) > 0.00001f)
+ changed.add (ranged->getName (64) + ": " + ranged->getCurrentValueAsText());
+ hostSettings.setText (changed.isEmpty() ? "" : "Host settings active", juce::dontSendNotification);
+ hostSettings.setTooltip ("Legacy settings are preserved. Edit them in your host's parameter view.\n" + changed.joinIntoString ("\n"));
+ hostSettings.setDescription (hostSettings.getTooltip());
+ meterSummary.setText ("PEAK LEVEL / dBFS", juce::dontSendNotification);
+ meterSummary.setDescription (! fresh ? "No current audio readings" :
+ "Input " + levelText (meter.input) + " dBFS, " + (audition ? "removed " : "output ")
+ + levelText (meter.output) + " dBFS. " + (bypass ? "Bypassed." : learning ? "Learning; reduction unavailable."
+ : meter.input < 0.00001f ? "No input; reduction unavailable."
+ : (multi ? "Maximum band reduction " : "High-band reduction ")
+ + (meter.reduction >= 60.0f ? "at least 60" : juce::String (meter.reduction, 1)) + " dB."));
+ if (fresh)
+ {
+ auto description = meterSummary.getDescription();
+ if ((meter.flags & M::inputClip) != 0) description += " Input clipped.";
+ if ((meter.flags & M::outputClip) != 0) description += " Output clipped.";
+ meterSummary.setDescription (description);
+ }
+ repaint();
}
-
-void SuppressorEditor::paintPanel (juce::Graphics& graphics,
- juce::Rectangle bounds,
- const juce::String& title) const
+void SuppressorEditor::drawLevel (juce::Graphics& g, int y, const juce::String& name, float peak, bool clip)
{
- const auto panel = bounds.toFloat();
- graphics.setColour (SuppressorTheme::surface);
- graphics.fillRoundedRectangle (panel, SuppressorTheme::cornerRadius);
- graphics.setColour (SuppressorTheme::divider);
- graphics.drawRoundedRectangle (panel.reduced (0.5f), SuppressorTheme::cornerRadius, 1.0f);
-
- graphics.setColour (SuppressorTheme::secondaryText);
- graphics.setFont (SuppressorTheme::makeFont (11.5f, true, 0.10f));
- graphics.drawText (title, bounds.getX() + 14, bounds.getY() + 8,
- bounds.getWidth() - 28, 15, juce::Justification::centredLeft);
- graphics.setColour (SuppressorTheme::divider);
- graphics.drawHorizontalLine (bounds.getY() + 29,
- static_cast (bounds.getX() + 14),
- static_cast (bounds.getRight() - 14));
+ label (g, name, { 36, y, 66, 20 }, 16.0f, secondary);
+ const auto track = juce::Rectangle (106.0f, (float) y + 6.0f, 205.0f, 8.0f);
+ g.setColour (line); g.fillRoundedRectangle (track, 2.0f);
+ if (fresh)
+ {
+ g.setColour (name == "Input" ? secondary : accent);
+ if (name == "Removed") g.setColour (warning);
+ g.fillRoundedRectangle (track.withWidth (205.0f * juce::jlimit (0.0f, 1.0f, (db (peak) + 60.0f) / 60.0f)), 2.0f);
+ }
+ label (g, fresh ? levelText (peak) : "--", { 322, y - 1, 66, 22 }, 19.0f, text, juce::Justification::right);
+ if (fresh && clip) label (g, "CLIP", { 319, y + 19, 69, 15 }, 14.0f, error, juce::Justification::right);
}
-
-void SuppressorEditor::paint (juce::Graphics& graphics)
+void SuppressorEditor::paint (juce::Graphics& g)
{
- graphics.fillAll (SuppressorTheme::canvas);
- graphics.setColour (SuppressorTheme::header);
- graphics.fillRect (0, 0, getWidth(), 64);
- graphics.setColour (SuppressorTheme::divider);
- graphics.drawHorizontalLine (63, 0.0f, static_cast (getWidth()));
-
- graphics.setColour (SuppressorTheme::accent);
- graphics.fillRoundedRectangle (18.0f, 16.0f, 5.0f, 32.0f, 2.5f);
- graphics.setColour (SuppressorTheme::primaryText);
- graphics.setFont (SuppressorTheme::makeFont (24.0f, true, 0.03f));
- graphics.drawText ("SUPPRESSOR", 36, 8, 220, 29, juce::Justification::centredLeft);
- graphics.setColour (SuppressorTheme::secondaryText);
- graphics.setFont (SuppressorTheme::makeFont (11.0f, true, 0.11f));
- graphics.drawText ("FREQUENCY-SPLIT NOISE CONTROL", 36, 35, 300, 15,
- juce::Justification::centredLeft);
- graphics.drawText ("DYNAMIC HIGH-BAND PROCESSOR", getWidth() - 300, 24, 282, 16,
- juce::Justification::centredRight);
-
- paintPanel (graphics, coreBounds, "SUPPRESSION CORE");
- paintPanel (graphics, activityBounds, "ACTIVITY / LAST PROCESSING SNAPSHOT");
- paintPanel (graphics, detectionBounds, "DETECTION");
- paintPanel (graphics, humBounds, "HUM FILTER");
- paintPanel (graphics, outputBounds, "MULTIBAND / OUTPUT");
-
- graphics.setColour (SuppressorTheme::divider);
- graphics.drawHorizontalLine (outputBounds.getY() + 128,
- static_cast (outputBounds.getX() + 14),
- static_cast (outputBounds.getRight() - 14));
+ using M = suppressor::MeterSnapshot;
+ const bool bypass = fresh && (meter.flags & M::bypassed) != 0;
+ const bool multi = proc.apvts.getRawParameterValue ("bandMode")->load() > 0.5f;
+ const bool learning = fresh && (meter.flags & M::learning) != 0;
+ const bool audition = listen.getToggleState() && ! bypass;
+ const bool hasReduction = fresh && ! learning && (bypass || meter.input >= 0.00001f);
+ g.fillAll (bg);
+ label (g, "suppressor", { 24, 19, 270, 32 }, 30.0f, text);
+ label (g, "GUITAR DI / NOISE SUPPRESSOR", { 25, 54, 340, 16 }, 14.0f, secondary);
+ g.setColour (panel); g.fillRoundedRectangle (24.0f, 86.0f, 592.0f, 162.0f, 8.0f);
+ drawLevel (g, 112, "Input", meter.input, (meter.flags & M::inputClip) != 0);
+ drawLevel (g, 164, audition ? "Removed" : "Output", meter.output, (meter.flags & M::outputClip) != 0);
+ for (int value : { -60, -36, -12, 0 })
+ {
+ const int x = 106 + (value + 60) * 205 / 60;
+ g.setColour (neutral); g.drawVerticalLine (x, 195.0f, 199.0f);
+ label (g, juce::String (value), { x - 14, 202, 28, 16 }, 14.0f, secondary, juce::Justification::centred);
+ }
+ g.setColour (line); g.drawVerticalLine (406, 106.0f, 230.0f);
+ label (g, multi ? "MAX BAND REDUCTION" : "HIGH-BAND REDUCTION", { 423, 108, 178, 18 }, 14.0f, secondary);
+ label (g, hasReduction ? (meter.reduction >= 60.0f ? "60+" : juce::String (meter.reduction, 1)) : "--", { 422, 134, 131, 56 }, 52.0f,
+ hasReduction && meter.reduction > 0.5f ? accent : text);
+ label (g, "dB", { 548, 158, 40, 24 }, 20.0f, secondary);
+ const float amount = hasReduction ? juce::jlimit (0.0f, 1.0f, meter.reduction / 60.0f) : 0.0f;
+ g.setColour (line); g.fillRect (424.0f, 202.0f, 172.0f, 6.0f);
+ g.setColour (accent); g.fillRect (424.0f, 202.0f, 172.0f * amount, 6.0f);
+ for (int value : { 0, 20, 40, 60 })
+ label (g, juce::String (value), { 416 + value * 172 / 60, 215, 22, 17 }, 14.0f, secondary, juce::Justification::centred);
+
+ label (g, "Threshold", { 24, 259, 192, 22 }, 18.0f, text, juce::Justification::centred);
+ label (g, "Strength", { 224, 259, 192, 22 }, 18.0f, text, juce::Justification::centred);
+ label (g, "Release", { 424, 259, 192, 22 }, 18.0f, text, juce::Justification::centred);
+ label (g, bypass ? "Monitor: dry input" : audition ? "Monitor: input minus processed" : "Monitor: processed signal",
+ { 196, 419, 265, 20 }, 15.0f, audition ? warning : secondary);
}
diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h
index aa6e659..9fed4bb 100644
--- a/Source/PluginEditor.h
+++ b/Source/PluginEditor.h
@@ -1,91 +1,77 @@
#pragma once
-#include "PluginLookAndFeel.h"
#include "PluginProcessor.h"
-#include
+#include "PluginLookAndFeel.h"
-class ActivityDisplay final : public juce::Component
+class SuppressorDial final : public juce::Slider
{
public:
- void setState (float detectorDb, float gainReductionDb, float thresholdDb,
- int bandMode, bool deltaAudition, bool multibandLearning);
- void paint (juce::Graphics&) override;
- std::unique_ptr createAccessibilityHandler() override;
+ SuppressorDial (juce::AudioProcessorValueTreeState&, const juce::String& id,
+ const juce::String& help);
+ ~SuppressorDial() override;
+ void resized() override;
+ void mouseDown (const juce::MouseEvent&) override;
+ void mouseDrag (const juce::MouseEvent&) override;
+ void mouseUp (const juce::MouseEvent&) override;
+ void mouseDoubleClick (const juce::MouseEvent&) override;
+ bool keyPressed (const juce::KeyPress&) override;
+ void focusGained (FocusChangeType) override { repaint(); }
+ void focusLost (FocusChangeType) override { repaint(); }
+ void enablementChanged() override;
+ void beginEntry();
+ void cancelInteraction();
+ juce::RangedAudioParameter& parameter;
private:
- class ValueInterface;
- juce::String accessibleValueText() const;
- juce::String semanticStatus() const;
+ void finishEntry (bool commit);
+ void setWithGesture (double);
+ void showMenu();
+ juce::TextEditor entry;
+ juce::Point lastDrag;
+ double dragProportion = 0.0;
+ std::unique_ptr drag;
+ juce::AudioProcessorValueTreeState::SliderAttachment attachment;
+};
- float detector = -180.0f;
- float reduction = 0.0f;
- float threshold = -40.0f;
- int mode = 0;
- bool delta = false;
- bool learning = false;
+class ListenButton final : public juce::TextButton
+{
+public:
+ ListenButton() : juce::TextButton ("Listen removed") {}
+ bool keyPressed (const juce::KeyPress& key) override
+ {
+ if (isEnabled() && key.isKeyCode (' '))
+ {
+ setToggleState (! getToggleState(), juce::sendNotificationSync);
+ return true;
+ }
+ return juce::TextButton::keyPressed (key);
+ }
};
-class SuppressorEditor final : public juce::AudioProcessorEditor,
- private juce::Timer
+class SuppressorEditor final : public juce::AudioProcessorEditor, private juce::Timer
{
public:
explicit SuppressorEditor (SuppressorProcessor&);
~SuppressorEditor() override;
-
void paint (juce::Graphics&) override;
void resized() override;
void visibilityChanged() override;
int getControlParameterIndex (juce::Component&) override;
private:
- struct Entry
- {
- juce::String id;
- ParameterSlider* slider = nullptr;
- juce::ComboBox* combo = nullptr;
- juce::Button* button = nullptr;
- std::unique_ptr label;
- std::unique_ptr sliderAttachment;
- std::unique_ptr comboAttachment;
- std::unique_ptr buttonAttachment;
- };
-
void timerCallback() override;
- ParameterSlider& addKnob (const juce::String& parameterId, const juce::String& label);
- juce::ComboBox& addCombo (const juce::String& parameterId, const juce::String& label);
- juce::ToggleButton& addToggle (const juce::String& parameterId, const juce::String& label);
- void addAction (const juce::String& parameterId, juce::TextButton& button,
- const juce::String& accessibleName);
- Entry* findEntry (const juce::String& parameterId) const;
- void placeKnob (const juce::String& parameterId, juce::Rectangle bounds);
- void placeCombo (const juce::String& parameterId, juce::Rectangle bounds);
- void placeButton (const juce::String& parameterId, juce::Rectangle bounds);
- void updateModePresentation();
- void updateTimerState();
- void paintPanel (juce::Graphics&, juce::Rectangle, const juce::String&) const;
-
+ void drawLevel (juce::Graphics&, int y, const juce::String&, float peak, bool clip);
SuppressorProcessor& proc;
- SuppressorLookAndFeel lookAndFeel;
- ActivityDisplay activityDisplay;
- juce::TooltipWindow tooltipWindow { this, 1200 };
-
- std::vector> sliders;
- std::vector> comboBoxes;
- std::vector> toggleButtons;
- std::vector> entries;
-
- ParameterTextButton humLearnButton { "LEARN" };
- ParameterTextButton bandsLearnButton { "LEARN" };
- juce::Label humStatus;
- juce::Label bandsStatus;
-
- juce::Rectangle coreBounds;
- juce::Rectangle activityBounds;
- juce::Rectangle detectionBounds;
- juce::Rectangle humBounds;
- juce::Rectangle outputBounds;
-
- int nextFocusOrder = 1;
+ SuppressorLookAndFeel theme;
+ SuppressorDial threshold, strength, release;
+ ListenButton listen;
+ juce::AudioProcessorValueTreeState::ButtonAttachment listenAttachment;
+ juce::Label status, meterSummary, hostSettings;
+ juce::TooltipWindow tooltips { this, 700 };
+ suppressor::MeterSnapshot meter;
+ uint32_t lastSequence = 0;
+ double lastUpdateMs = 0.0;
+ bool fresh = false;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SuppressorEditor)
};
diff --git a/Source/PluginLookAndFeel.cpp b/Source/PluginLookAndFeel.cpp
index 62409af..c529105 100644
--- a/Source/PluginLookAndFeel.cpp
+++ b/Source/PluginLookAndFeel.cpp
@@ -1,455 +1,85 @@
#include "PluginLookAndFeel.h"
#include "SuppressorAssets.h"
-#include
-
-namespace
-{
-const juce::Typeface::Ptr& regularTypeface()
-{
- static auto typeface = juce::Typeface::createSystemTypefaceFor (
- SuppressorAssets::BarlowCondensedRegular_ttf,
- SuppressorAssets::BarlowCondensedRegular_ttfSize);
- return typeface;
-}
-
-const juce::Typeface::Ptr& semiboldTypeface()
-{
- static auto typeface = juce::Typeface::createSystemTypefaceFor (
- SuppressorAssets::BarlowCondensedSemiBold_ttf,
- SuppressorAssets::BarlowCondensedSemiBold_ttfSize);
- return typeface;
-}
-
-juce::TextLayout makeTooltipLayout (const juce::String& text, juce::Colour colour,
- float maximumWidth)
-{
- juce::AttributedString attributed;
- attributed.setJustification (juce::Justification::centred);
- attributed.setWordWrap (juce::AttributedString::byWord);
- attributed.append (text, SuppressorTheme::makeFont (14.0f), colour);
- juce::TextLayout layout;
- layout.createLayout (attributed, maximumWidth);
- return layout;
-}
-}
-
-juce::Font SuppressorTheme::makeFont (float size, bool semibold, float tracking)
-{
- auto typeface = semibold ? semiboldTypeface() : regularTypeface();
- return juce::Font (juce::FontOptions (typeface)
- .withHeight (size)
- .withKerningFactor (tracking));
-}
-
-void ParameterSlider::mouseDoubleClick (const juce::MouseEvent& event)
-{
- if (isEnabled() && isTextBoxEditable() && ! event.mods.isPopupMenu())
- showTextBox();
-}
-
-bool ParameterSlider::keyPressed (const juce::KeyPress& key)
-{
- if (key == juce::KeyPress::returnKey && isEnabled() && isTextBoxEditable())
- {
- showTextBox();
- return true;
- }
-
- const auto isAdjustmentKey = ! key.getModifiers().isAnyModifierKeyDown()
- && (key.isKeyCode (juce::KeyPress::leftKey)
- || key.isKeyCode (juce::KeyPress::rightKey)
- || key.isKeyCode (juce::KeyPress::upKey)
- || key.isKeyCode (juce::KeyPress::downKey));
- if (! isEnabled() || ! isAdjustmentKey || getThumbBeingDragged() >= 0)
- return juce::Slider::keyPressed (key);
-
- if (beginKeyboardGesture != nullptr)
- beginKeyboardGesture();
- const auto handled = juce::Slider::keyPressed (key);
- if (endKeyboardGesture != nullptr)
- endKeyboardGesture();
- return handled;
-}
-
-void ParameterSlider::setKeyboardGestureCallbacks (GestureCallback begin,
- GestureCallback end)
-{
- beginKeyboardGesture = std::move (begin);
- endKeyboardGesture = std::move (end);
-}
-
-void ParameterSlider::endActiveGesture()
-{
- if (getThumbBeingDragged() < 0)
- return;
-
- const auto now = juce::Time::getCurrentTime();
- const auto position = getLocalBounds().getCentre().toFloat();
- juce::Slider::mouseUp (
- { juce::Desktop::getInstance().getMainMouseSource(), position, {},
- 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, this, this, now, position, now, 1, true });
-}
-
-double ParameterSlider::getValueFromText (const juce::String& text)
-{
- auto numericText = text.trim();
- const auto suffix = getTextValueSuffix().trim();
- if (suffix.isNotEmpty() && numericText.endsWithIgnoreCase (suffix))
- numericText = numericText.dropLastCharacters (suffix.length()).trimEnd();
-
- bool hasDigit = false;
- bool hasDecimalPoint = false;
- for (int index = 0; index < numericText.length(); ++index)
- {
- const auto character = numericText[index];
- if (character >= '0' && character <= '9')
- {
- hasDigit = true;
- continue;
- }
- if (character == '.' && ! hasDecimalPoint)
- {
- hasDecimalPoint = true;
- continue;
- }
- if (index == 0 && (character == '+' || character == '-'))
- continue;
- return getValue();
- }
-
- if (! hasDigit)
- return getValue();
-
- const auto parsed = numericText.getDoubleValue();
- return std::isfinite (parsed) ? juce::Slider::getValueFromText (numericText)
- : getValue();
-}
-
-bool ParameterToggleButton::keyPressed (const juce::KeyPress& key)
+juce::Font SuppressorTheme::makeFont (float size, bool semibold)
{
- if (isEnabled() && key.isKeyCode (' '))
- {
- setToggleState (! getToggleState(), juce::sendNotificationSync);
- return true;
- }
- return juce::ToggleButton::keyPressed (key);
-}
-
-bool ParameterTextButton::keyPressed (const juce::KeyPress& key)
-{
- if (isEnabled() && key.isKeyCode (' '))
- {
- setToggleState (! getToggleState(), juce::sendNotificationSync);
- return true;
- }
- return juce::TextButton::keyPressed (key);
+ static auto regular = juce::Typeface::createSystemTypefaceFor (
+ SuppressorAssets::BarlowCondensedRegular_ttf, SuppressorAssets::BarlowCondensedRegular_ttfSize);
+ static auto bold = juce::Typeface::createSystemTypefaceFor (
+ SuppressorAssets::BarlowCondensedSemiBold_ttf, SuppressorAssets::BarlowCondensedSemiBold_ttfSize);
+ return juce::Font (juce::FontOptions (semibold ? bold : regular).withHeight (size));
}
SuppressorLookAndFeel::SuppressorLookAndFeel()
{
- setColour (juce::Slider::rotarySliderFillColourId, SuppressorTheme::accent);
- setColour (juce::Slider::rotarySliderOutlineColourId, SuppressorTheme::controlOutline);
- setColour (juce::Slider::textBoxTextColourId, SuppressorTheme::primaryText);
- setColour (juce::Slider::textBoxBackgroundColourId, juce::Colours::transparentBlack);
- setColour (juce::Slider::textBoxOutlineColourId, juce::Colours::transparentBlack);
- setColour (juce::Slider::textBoxHighlightColourId, SuppressorTheme::accent.withAlpha (0.28f));
- setColour (juce::ComboBox::textColourId, SuppressorTheme::primaryText);
- setColour (juce::ComboBox::backgroundColourId, SuppressorTheme::surfaceRaised);
- setColour (juce::ComboBox::outlineColourId, SuppressorTheme::controlOutline);
- setColour (juce::ComboBox::arrowColourId, SuppressorTheme::secondaryText);
- setColour (juce::ComboBox::focusedOutlineColourId, SuppressorTheme::accent);
- setColour (juce::PopupMenu::backgroundColourId, SuppressorTheme::surfaceRaised);
- setColour (juce::PopupMenu::textColourId, SuppressorTheme::primaryText);
- setColour (juce::PopupMenu::highlightedBackgroundColourId, SuppressorTheme::accent);
- setColour (juce::PopupMenu::highlightedTextColourId, SuppressorTheme::canvas);
- setColour (juce::Label::textColourId, SuppressorTheme::primaryText);
- setColour (juce::Label::textWhenEditingColourId, SuppressorTheme::primaryText);
- setColour (juce::Label::backgroundWhenEditingColourId, SuppressorTheme::surfaceRaised);
- setColour (juce::Label::outlineWhenEditingColourId, SuppressorTheme::accent);
- setColour (juce::TextEditor::textColourId, SuppressorTheme::primaryText);
- setColour (juce::TextEditor::backgroundColourId, SuppressorTheme::surfaceRaised);
- setColour (juce::TextEditor::highlightColourId, SuppressorTheme::accent.withAlpha (0.35f));
- setColour (juce::TextEditor::highlightedTextColourId, SuppressorTheme::primaryText);
- setColour (juce::CaretComponent::caretColourId, SuppressorTheme::accent);
- setColour (juce::TooltipWindow::backgroundColourId, SuppressorTheme::surfaceRaised);
- setColour (juce::TooltipWindow::textColourId, SuppressorTheme::primaryText);
- setColour (juce::TooltipWindow::outlineColourId, SuppressorTheme::controlOutline);
-}
-
-void SuppressorLookAndFeel::drawRotarySlider (juce::Graphics& graphics, int x, int y,
- int width, int height, float sliderPosition,
- float startAngle, float endAngle,
- juce::Slider& slider)
-{
- const auto size = static_cast (juce::jmin (width, height));
- const auto radius = size * 0.5f - 8.0f;
- const auto centre = juce::Point (
- static_cast (x) + static_cast (width) * 0.5f,
- static_cast (y) + static_cast (height) * 0.5f);
- const auto angle = juce::jmap (sliderPosition, 0.0f, 1.0f, startAngle, endAngle);
- const auto alpha = slider.isEnabled() ? 1.0f : 0.38f;
-
- juce::Path track;
- track.addCentredArc (centre.x, centre.y, radius, radius, 0.0f,
- startAngle, endAngle, true);
- graphics.setColour (slider.findColour (juce::Slider::rotarySliderOutlineColourId)
- .withMultipliedAlpha (alpha));
- graphics.strokePath (track, juce::PathStrokeType (4.0f,
- juce::PathStrokeType::curved,
- juce::PathStrokeType::rounded));
-
- if (sliderPosition > 0.0f)
+ using namespace SuppressorTheme;
+ setColour (juce::Label::textColourId, secondaryText);
+ setColour (juce::TextEditor::backgroundColourId, surfaceRaised);
+ setColour (juce::TextEditor::textColourId, primaryText);
+ setColour (juce::TextEditor::outlineColourId, controlOutline);
+ setColour (juce::TextEditor::focusedOutlineColourId, accent);
+ setColour (juce::TextEditor::highlightColourId, accent.withAlpha (0.3f));
+ setColour (juce::CaretComponent::caretColourId, primaryText);
+ setColour (juce::TextButton::textColourOffId, primaryText);
+ setColour (juce::TextButton::textColourOnId, warning);
+ setColour (juce::PopupMenu::backgroundColourId, surface);
+ setColour (juce::PopupMenu::textColourId, primaryText);
+ setColour (juce::PopupMenu::highlightedBackgroundColourId, surfaceRaised);
+ setColour (juce::PopupMenu::highlightedTextColourId, accent);
+ setColour (juce::TooltipWindow::backgroundColourId, surfaceRaised);
+ setColour (juce::TooltipWindow::textColourId, primaryText);
+ setColour (juce::TooltipWindow::outlineColourId, controlOutline);
+}
+
+void SuppressorLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int w, int h,
+ float position, float start, float end, juce::Slider& s)
+{
+ using namespace SuppressorTheme;
+ const auto bounds = juce::Rectangle (x, y, w, h).toFloat().reduced (8.0f);
+ const auto centre = bounds.getCentre();
+ const auto radius = juce::jmin (bounds.getWidth(), bounds.getHeight()) * 0.5f - 4.0f;
+ g.setColour (surfaceRaised);
+ g.fillEllipse (bounds.reduced (7.0f));
+ juce::Path track, value;
+ track.addCentredArc (centre.x, centre.y, radius, radius, 0.0f, start, end, true);
+ value.addCentredArc (centre.x, centre.y, radius, radius, 0.0f, start,
+ start + position * (end - start), true);
+ g.setColour (controlOutline);
+ g.strokePath (track, juce::PathStrokeType (3.0f));
+ g.setColour (s.isEnabled() ? accent : controlOutline);
+ g.strokePath (value, juce::PathStrokeType (3.0f));
+ const auto angle = start + position * (end - start);
+ const auto point = centre + juce::Point (std::sin (angle), -std::cos (angle)) * radius;
+ g.fillEllipse (point.x - 3.0f, point.y - 3.0f, 6.0f, 6.0f);
+ g.setColour (s.isEnabled() ? primaryText : secondaryText);
+ g.setFont (makeFont (21.0f, true));
+ g.drawText (s.getTextFromValue (s.getValue()), bounds, juce::Justification::centred);
+ if (s.hasKeyboardFocus (true))
{
- juce::Path value;
- value.addCentredArc (centre.x, centre.y, radius, radius, 0.0f,
- startAngle, angle, true);
- graphics.setColour (slider.findColour (juce::Slider::rotarySliderFillColourId)
- .withMultipliedAlpha (alpha));
- graphics.strokePath (value, juce::PathStrokeType (4.0f,
- juce::PathStrokeType::curved,
- juce::PathStrokeType::rounded));
+ g.setColour (accent);
+ g.drawRoundedRectangle (s.getLocalBounds().toFloat().reduced (1.0f), 6.0f, 1.5f);
}
-
- const auto innerRadius = radius - 10.0f;
- graphics.setColour (SuppressorTheme::surfaceRaised.withMultipliedAlpha (alpha));
- graphics.fillEllipse (centre.x - innerRadius, centre.y - innerRadius,
- innerRadius * 2.0f, innerRadius * 2.0f);
-
- auto innerOutline = slider.isMouseOverOrDragging()
- ? SuppressorTheme::secondaryText
- : SuppressorTheme::divider;
- graphics.setColour (innerOutline.withMultipliedAlpha (alpha));
- graphics.drawEllipse (centre.x - innerRadius, centre.y - innerRadius,
- innerRadius * 2.0f, innerRadius * 2.0f, 1.0f);
-
- const auto markerStart = centre.getPointOnCircumference (innerRadius - 9.0f, angle);
- const auto markerEnd = centre.getPointOnCircumference (innerRadius - 3.0f, angle);
- graphics.setColour (slider.findColour (juce::Slider::rotarySliderFillColourId)
- .withMultipliedAlpha (alpha));
- graphics.drawLine ({ markerStart, markerEnd }, 2.5f);
-
- if (slider.hasKeyboardFocus (true))
- {
- graphics.setColour (SuppressorTheme::accent);
- graphics.drawEllipse (centre.x - radius - 4.0f, centre.y - radius - 4.0f,
- (radius + 4.0f) * 2.0f, (radius + 4.0f) * 2.0f, 2.0f);
- }
-}
-
-juce::Label* SuppressorLookAndFeel::createSliderTextBox (juce::Slider& slider)
-{
- auto* label = juce::LookAndFeel_V4::createSliderTextBox (slider);
- label->setFont (SuppressorTheme::makeFont (15.0f, true));
- label->setJustificationType (juce::Justification::centred);
- label->onEditorShow = [label]
- {
- if (auto* editor = label->getCurrentTextEditor())
- {
- editor->setFont (label->getFont());
- editor->setJustification (juce::Justification::centred);
- }
- };
- label->setColour (juce::Label::backgroundColourId, juce::Colours::transparentBlack);
- label->setColour (juce::Label::backgroundWhenEditingColourId, juce::Colours::transparentBlack);
- label->setColour (juce::Label::outlineColourId, juce::Colours::transparentBlack);
- label->setColour (juce::Label::outlineWhenEditingColourId, juce::Colours::transparentBlack);
- label->setColour (juce::TextEditor::backgroundColourId, juce::Colours::transparentBlack);
- label->setColour (juce::TextEditor::outlineColourId, juce::Colours::transparentBlack);
- label->setColour (juce::TextEditor::focusedOutlineColourId, juce::Colours::transparentBlack);
- label->setColour (juce::TextEditor::shadowColourId, juce::Colours::transparentBlack);
- label->setInterceptsMouseClicks (false, true);
- return label;
-}
-
-juce::Slider::SliderLayout SuppressorLookAndFeel::getSliderLayout (juce::Slider& slider)
-{
- juce::Slider::SliderLayout layout;
- const auto bounds = slider.getLocalBounds();
- layout.sliderBounds = bounds;
-
- if (slider.getTextBoxPosition() != juce::Slider::NoTextBox)
- {
- const auto width = juce::jmin (slider.getTextBoxWidth(), bounds.getWidth() - 4);
- const auto height = juce::jmin (slider.getTextBoxHeight(), bounds.getHeight() - 4);
- layout.textBoxBounds = juce::Rectangle (width, height).withCentre (bounds.getCentre());
- }
-
- return layout;
-}
-
-void SuppressorLookAndFeel::drawComboBox (juce::Graphics& graphics, int width, int height,
- bool isButtonDown, int, int, int, int,
- juce::ComboBox& box)
-{
- auto bounds = juce::Rectangle (0.5f, 0.5f,
- static_cast (width) - 1.0f,
- static_cast (height) - 1.0f);
- graphics.setColour (SuppressorTheme::surfaceRaised);
- graphics.fillRoundedRectangle (bounds, 6.0f);
-
- const auto focused = box.hasKeyboardFocus (false);
- const auto outline = focused || isButtonDown
- ? SuppressorTheme::accent
- : (box.isMouseOver() ? SuppressorTheme::secondaryText
- : SuppressorTheme::controlOutline);
- graphics.setColour (outline);
- graphics.drawRoundedRectangle (bounds, 6.0f, focused ? 2.0f : 1.0f);
-
- const auto arrowCentreX = static_cast (width - 17);
- const auto arrowCentreY = static_cast (height) * 0.5f;
- juce::Path arrow;
- arrow.startNewSubPath (arrowCentreX - 4.0f, arrowCentreY - 2.0f);
- arrow.lineTo (arrowCentreX, arrowCentreY + 2.0f);
- arrow.lineTo (arrowCentreX + 4.0f, arrowCentreY - 2.0f);
- graphics.setColour (focused ? SuppressorTheme::accent : SuppressorTheme::secondaryText);
- graphics.strokePath (arrow, juce::PathStrokeType (1.7f,
- juce::PathStrokeType::curved,
- juce::PathStrokeType::rounded));
-}
-
-juce::Font SuppressorLookAndFeel::getComboBoxFont (juce::ComboBox&)
-{
- return SuppressorTheme::makeFont (15.0f);
-}
-
-juce::Font SuppressorLookAndFeel::getPopupMenuFont()
-{
- return SuppressorTheme::makeFont (15.0f);
-}
-
-juce::Rectangle SuppressorLookAndFeel::getTooltipBounds (
- const juce::String& text, juce::Point screenPosition,
- juce::Rectangle parentArea)
-{
- const auto layout = makeTooltipLayout (text, SuppressorTheme::primaryText, 300.0f);
- const auto width = juce::roundToInt (std::ceil (layout.getWidth())) + 16;
- const auto height = juce::roundToInt (std::ceil (layout.getHeight())) + 10;
- return juce::Rectangle (
- screenPosition.x > parentArea.getCentreX() ? screenPosition.x - width - 12
- : screenPosition.x + 24,
- screenPosition.y > parentArea.getCentreY() ? screenPosition.y - height - 6
- : screenPosition.y + 6,
- width, height)
- .constrainedWithin (parentArea);
-}
-
-void SuppressorLookAndFeel::drawTooltip (juce::Graphics& graphics,
- const juce::String& text,
- int width, int height)
-{
- const auto bounds = juce::Rectangle (width, height).toFloat();
- graphics.setColour (SuppressorTheme::surfaceRaised);
- graphics.fillRoundedRectangle (bounds, 5.0f);
- graphics.setColour (SuppressorTheme::controlOutline);
- graphics.drawRoundedRectangle (bounds.reduced (0.5f), 5.0f, 1.0f);
-
- const auto textBounds = bounds.reduced (8.0f, 5.0f);
- auto layout = makeTooltipLayout (text, SuppressorTheme::primaryText,
- textBounds.getWidth());
- layout.draw (graphics, textBounds);
-}
-
-void SuppressorLookAndFeel::positionComboBoxText (juce::ComboBox& box, juce::Label& label)
-{
- label.setBounds (12, 1, box.getWidth() - 38, box.getHeight() - 2);
- label.setFont (getComboBoxFont (box));
-}
-
-void SuppressorLookAndFeel::drawToggleButton (juce::Graphics& graphics,
- juce::ToggleButton& button,
- bool isMouseOver, bool isButtonDown)
-{
- const auto alpha = button.isEnabled() ? 1.0f : 0.42f;
- auto bounds = button.getLocalBounds().toFloat().reduced (0.5f);
- auto fill = SuppressorTheme::surfaceRaised;
- if (isButtonDown)
- fill = fill.brighter (0.08f);
- graphics.setColour (fill.withMultipliedAlpha (alpha));
- graphics.fillRoundedRectangle (bounds, 6.0f);
-
- const auto focused = button.hasKeyboardFocus (false);
- const auto outline = focused ? SuppressorTheme::accent
- : (isMouseOver ? SuppressorTheme::secondaryText
- : SuppressorTheme::controlOutline);
- graphics.setColour (outline.withMultipliedAlpha (alpha));
- graphics.drawRoundedRectangle (bounds, 6.0f, focused ? 2.0f : 1.0f);
-
- auto indicator = juce::Rectangle (12.0f,
- static_cast (button.getHeight()) * 0.5f - 8.0f,
- 16.0f, 16.0f);
- graphics.setColour ((button.getToggleState() ? SuppressorTheme::accent
- : SuppressorTheme::surface)
- .withMultipliedAlpha (alpha));
- graphics.fillRoundedRectangle (indicator, 3.0f);
- graphics.setColour ((button.getToggleState() ? SuppressorTheme::accent
- : SuppressorTheme::controlOutline)
- .withMultipliedAlpha (alpha));
- graphics.drawRoundedRectangle (indicator, 3.0f, 1.0f);
-
- if (button.getToggleState())
- {
- juce::Path check;
- check.startNewSubPath (indicator.getX() + 3.5f, indicator.getCentreY());
- check.lineTo (indicator.getX() + 7.0f, indicator.getBottom() - 4.0f);
- check.lineTo (indicator.getRight() - 3.0f, indicator.getY() + 4.0f);
- graphics.setColour (SuppressorTheme::canvas.withMultipliedAlpha (alpha));
- graphics.strokePath (check, juce::PathStrokeType (2.0f,
- juce::PathStrokeType::curved,
- juce::PathStrokeType::rounded));
- }
-
- graphics.setColour (SuppressorTheme::primaryText.withMultipliedAlpha (alpha));
- graphics.setFont (SuppressorTheme::makeFont (14.5f, true, 0.02f));
- graphics.drawFittedText (button.getButtonText(), 38, 0, button.getWidth() - 48,
- button.getHeight(), juce::Justification::centredLeft, 1);
-}
-
-void SuppressorLookAndFeel::drawButtonBackground (juce::Graphics& graphics,
- juce::Button& button,
- const juce::Colour&,
- bool isMouseOver, bool isButtonDown)
-{
- const auto alpha = button.isEnabled() ? 1.0f : 0.42f;
- const auto active = button.getToggleState();
- auto bounds = button.getLocalBounds().toFloat().reduced (0.5f);
- auto fill = active ? SuppressorTheme::accent : SuppressorTheme::surfaceRaised;
- if (isButtonDown)
- fill = fill.brighter (active ? 0.04f : 0.08f);
- graphics.setColour (fill.withMultipliedAlpha (alpha));
- graphics.fillRoundedRectangle (bounds, 6.0f);
-
- const auto focused = button.hasKeyboardFocus (false);
- const auto outline = active ? SuppressorTheme::accent
- : (isMouseOver ? SuppressorTheme::secondaryText
- : SuppressorTheme::controlOutline);
- graphics.setColour ((focused ? (active ? SuppressorTheme::primaryText
- : SuppressorTheme::accent)
- : outline)
- .withMultipliedAlpha (alpha));
- graphics.drawRoundedRectangle (bounds, 6.0f, focused ? 2.0f : 1.0f);
- if (focused && active)
+ else if (s.isMouseOver() && s.isEnabled())
{
- graphics.setColour (SuppressorTheme::canvas.withMultipliedAlpha (alpha));
- graphics.drawRoundedRectangle (bounds.reduced (3.0f), 4.0f, 1.5f);
+ g.setColour (controlOutline);
+ g.drawRoundedRectangle (s.getLocalBounds().toFloat().reduced (1.0f), 6.0f, 1.0f);
}
}
-void SuppressorLookAndFeel::drawButtonText (juce::Graphics& graphics,
- juce::TextButton& button,
- bool, bool)
+void SuppressorLookAndFeel::drawButtonBackground (juce::Graphics& g, juce::Button& b,
+ const juce::Colour&, bool hover, bool down)
{
- const auto alpha = button.isEnabled() ? 1.0f : 0.42f;
- graphics.setColour ((button.getToggleState() ? SuppressorTheme::canvas
- : SuppressorTheme::primaryText)
- .withMultipliedAlpha (alpha));
- graphics.setFont (getTextButtonFont (button, button.getHeight()));
- graphics.drawFittedText (button.getButtonText(), button.getLocalBounds().reduced (8, 2),
- juce::Justification::centred, 1);
+ using namespace SuppressorTheme;
+ const auto r = b.getLocalBounds().toFloat().reduced (1.0f);
+ g.setColour (down ? divider : surfaceRaised);
+ g.fillRoundedRectangle (r, 5.0f);
+ g.setColour (b.getToggleState() ? warning : (hover ? primaryText : controlOutline));
+ g.drawRoundedRectangle (r, 5.0f, b.hasKeyboardFocus (true) ? 2.0f : 1.0f);
+ if (b.hasKeyboardFocus (true)) g.drawRoundedRectangle (r.reduced (4.0f), 3.0f, 1.0f);
}
-juce::Font SuppressorLookAndFeel::getTextButtonFont (juce::TextButton&, int buttonHeight)
+juce::Font SuppressorLookAndFeel::getTextButtonFont (juce::TextButton&, int)
{
- return SuppressorTheme::makeFont (juce::jmin (15.0f, static_cast (buttonHeight) * 0.5f),
- true, 0.04f);
+ return SuppressorTheme::makeFont (17.0f, true);
}
+juce::Font SuppressorLookAndFeel::getPopupMenuFont() { return SuppressorTheme::makeFont (17.0f); }
diff --git a/Source/PluginLookAndFeel.h b/Source/PluginLookAndFeel.h
index a9088b3..a5448be 100644
--- a/Source/PluginLookAndFeel.h
+++ b/Source/PluginLookAndFeel.h
@@ -2,82 +2,23 @@
#include
-#include
-#include
-
namespace SuppressorTheme
{
-inline const juce::Colour canvas { 0xff0a0e14 };
-inline const juce::Colour header { 0xff0d141e };
-inline const juce::Colour surface { 0xff111a24 };
-inline const juce::Colour surfaceRaised { 0xff182432 };
-inline const juce::Colour divider { 0xff2a3a4b };
-inline const juce::Colour controlOutline { 0xff66798c };
-inline const juce::Colour primaryText { 0xfff1f5f8 };
-inline const juce::Colour secondaryText { 0xffa6b2be };
-inline const juce::Colour accent { 0xff8ea7ff };
-inline const juce::Colour warning { 0xfff2b84b };
-inline const juce::Colour error { 0xffff6f7d };
-inline const juce::Colour inactiveControl { 0xff6c7c8f };
-inline constexpr float cornerRadius = 9.0f;
-
-juce::Font makeFont (float size, bool semibold = false, float tracking = 0.0f);
+inline const juce::Colour canvas { 0xff0b0e11 }, surface { 0xff12171c },
+ surfaceRaised { 0xff171d23 }, divider { 0xff2a333c }, primaryText { 0xffe8edf1 },
+ secondaryText { 0xff89939d }, controlOutline { 0xff66717b }, accent { 0xffb8f24a },
+ warning { 0xffe7a84b }, error { 0xffff8080 };
+juce::Font makeFont (float size, bool semibold = false);
}
-class ParameterSlider final : public juce::Slider
-{
-public:
- using GestureCallback = std::function;
-
- void mouseDoubleClick (const juce::MouseEvent&) override;
- bool keyPressed (const juce::KeyPress&) override;
- double getValueFromText (const juce::String&) override;
- void setKeyboardGestureCallbacks (GestureCallback begin, GestureCallback end);
- void endActiveGesture();
-
-private:
- GestureCallback beginKeyboardGesture;
- GestureCallback endKeyboardGesture;
-};
-
-class ParameterToggleButton final : public juce::ToggleButton
-{
-public:
- using juce::ToggleButton::ToggleButton;
- bool keyPressed (const juce::KeyPress&) override;
-};
-
-class ParameterTextButton final : public juce::TextButton
-{
-public:
- using juce::TextButton::TextButton;
- bool keyPressed (const juce::KeyPress&) override;
-};
-
class SuppressorLookAndFeel final : public juce::LookAndFeel_V4
{
public:
SuppressorLookAndFeel();
-
- void drawRotarySlider (juce::Graphics&, int x, int y, int width, int height,
- float sliderPosition, float startAngle, float endAngle,
+ void drawRotarySlider (juce::Graphics&, int, int, int, int, float, float, float,
juce::Slider&) override;
- juce::Label* createSliderTextBox (juce::Slider&) override;
- juce::Slider::SliderLayout getSliderLayout (juce::Slider&) override;
- void drawComboBox (juce::Graphics&, int width, int height, bool isButtonDown,
- int buttonX, int buttonY, int buttonWidth, int buttonHeight,
- juce::ComboBox&) override;
- juce::Font getComboBoxFont (juce::ComboBox&) override;
- juce::Font getPopupMenuFont() override;
- juce::Rectangle getTooltipBounds (const juce::String&, juce::Point,
- juce::Rectangle) override;
- void drawTooltip (juce::Graphics&, const juce::String&, int width, int height) override;
- void positionComboBoxText (juce::ComboBox&, juce::Label&) override;
- void drawToggleButton (juce::Graphics&, juce::ToggleButton&, bool isMouseOver,
- bool isButtonDown) override;
void drawButtonBackground (juce::Graphics&, juce::Button&, const juce::Colour&,
- bool isMouseOver, bool isButtonDown) override;
- void drawButtonText (juce::Graphics&, juce::TextButton&, bool isMouseOver,
- bool isButtonDown) override;
- juce::Font getTextButtonFont (juce::TextButton&, int buttonHeight) override;
+ bool, bool) override;
+ juce::Font getTextButtonFont (juce::TextButton&, int) override;
+ juce::Font getPopupMenuFont() override;
};
diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp
index b3698ae..3519036 100644
--- a/Source/PluginProcessor.cpp
+++ b/Source/PluginProcessor.cpp
@@ -126,9 +126,10 @@ void SuppressorProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
setLatencySamples (currentLatency);
engine.setTargets (readTargets (sampleRate));
engine.reset();
+ meters.reset();
}
-void SuppressorProcessor::releaseResources() {}
+void SuppressorProcessor::releaseResources() { meters.reset(); }
bool SuppressorProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
@@ -244,15 +245,24 @@ void SuppressorProcessor::processBlock (juce::AudioBuffer& buffer, juce::
if (numSc > 1) sc[1] = scBus.getReadPointer (1);
}
+ const float inputPeak = mainBus.getMagnitude (0, mainBus.getNumSamples());
engine.processBlock (io, sc, numSc, buffer.getNumSamples(), numMain);
- detectorDbAtomic.store (engine.detectorLevelDb());
- grDbAtomic.store (engine.gainReductionDb());
+ using M = suppressor::MeterSnapshot;
+ uint32_t flags = targets.deltaAudition ? M::removed : 0u;
+ if (targets.bandMode != 0) flags |= M::multiband;
+ if (targets.bandMode != 0 && engine.multi (0).learning()) flags |= M::learning;
+ meters.push (inputPeak, mainBus.getMagnitude (0, mainBus.getNumSamples()),
+ -engine.gainReductionDb(), buffer.getNumSamples(), fs, flags);
}
-void SuppressorProcessor::processBlockBypassed (juce::AudioBuffer&, juce::MidiBuffer& midi)
+void SuppressorProcessor::processBlockBypassed (juce::AudioBuffer& buffer, juce::MidiBuffer& midi)
{
midi.clear();
+ auto mainBus = getBusBuffer (buffer, true, 0);
+ const float peak = mainBus.getMagnitude (0, mainBus.getNumSamples());
+ meters.push (peak, peak, 0.0f, buffer.getNumSamples(), getSampleRate(),
+ suppressor::MeterSnapshot::bypassed);
}
void SuppressorProcessor::handleAsyncUpdate()
@@ -264,30 +274,6 @@ void SuppressorProcessor::handleAsyncUpdate()
prm->setValueNotifyingHost (0.0f);
}
-// ------------------------------------------------------------------------
-bool SuppressorProcessor::humLearning() const noexcept { return humLearningNow; }
-bool SuppressorProcessor::humLocked() const noexcept
-{
- if (engine.numChannels() == 0) return false;
- return const_cast (engine).hum (0).locked();
-}
-bool SuppressorProcessor::bandsLearning() const noexcept { return bandsLearningNow; }
-bool SuppressorProcessor::bandsLocked() const noexcept
-{
- if (engine.numChannels() == 0) return false;
- return const_cast (engine).multi (0).locked();
-}
-int SuppressorProcessor::humActiveDips() const noexcept
-{
- if (engine.numChannels() == 0) return 0;
- return const_cast (engine).hum (0).activeHarmonicCount();
-}
-double SuppressorProcessor::humDetectedBaseHz() const noexcept
-{
- if (engine.numChannels() == 0) return 60.0;
- return const_cast (engine).hum (0).baseHz();
-}
-
// ------------------------------------------------------------------------
juce::AudioProcessorEditor* SuppressorProcessor::createEditor()
{
diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h
index 9571752..242e1d4 100644
--- a/Source/PluginProcessor.h
+++ b/Source/PluginProcessor.h
@@ -2,6 +2,7 @@
#include
#include "DSP/DenoiserEngine.h"
+#include "Metering.h"
class SuppressorProcessor : public juce::AudioProcessor,
private juce::AsyncUpdater
@@ -38,17 +39,8 @@ class SuppressorProcessor : public juce::AudioProcessor,
// ------------------------------------------------------------------
juce::AudioProcessorValueTreeState apvts;
- // Metering / status for the editor (read on the message thread)
- float detectorDb() const noexcept { return detectorDbAtomic.load(); }
- float gainReductionDb() const noexcept { return grDbAtomic.load(); }
- bool humLearning() const noexcept;
- bool humLocked() const noexcept;
- bool bandsLearning() const noexcept;
- bool bandsLocked() const noexcept;
- int humActiveDips() const noexcept;
- double humDetectedBaseHz() const noexcept;
- void requestHumLearnStop() { humLearnRequest = false; }
- void requestBandsLearnStop() { bandsLearnRequest = false; }
+ // Bounded, race-free telemetry; never expose mutable DSP state to the editor.
+ bool readMeters (suppressor::MeterSnapshot& result) const noexcept { return meters.read (result); }
static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
@@ -58,13 +50,12 @@ class SuppressorProcessor : public juce::AudioProcessor,
suppressor::DenoiserEngine engine;
- std::atomic detectorDbAtomic { -120.0f };
- std::atomic grDbAtomic { 0.0f };
+ suppressor::Metering meters;
std::atomic latencyDirty { false };
std::atomic humAutoFinished { false };
- bool humLearnRequest = false, humLearningNow = false;
- bool bandsLearnRequest = false, bandsLearningNow = false;
+ bool humLearningNow = false;
+ bool bandsLearningNow = false;
int currentLatency = 0;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SuppressorProcessor)
diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt
index bf6ebe9..c6af2d2 100644
--- a/Tests/CMakeLists.txt
+++ b/Tests/CMakeLists.txt
@@ -12,9 +12,11 @@ add_executable(suppressor-tests
test_engine.cpp
test_hum.cpp
test_multiband.cpp
- test_cue.cpp)
+ test_cue.cpp
+ test_metering.cpp)
-target_link_libraries(suppressor-tests PRIVATE suppressor-dsp doctest::doctest)
+find_package(Threads REQUIRED)
+target_link_libraries(suppressor-tests PRIVATE suppressor-dsp doctest::doctest Threads::Threads)
target_include_directories(suppressor-tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
add_test(NAME suppressor-dsp-unit COMMAND suppressor-tests)
diff --git a/Tests/test_clap_gui.cpp b/Tests/test_clap_gui.cpp
new file mode 100644
index 0000000..5dddd1a
--- /dev/null
+++ b/Tests/test_clap_gui.cpp
@@ -0,0 +1,100 @@
+#include
+#include
+#include
+
+namespace
+{
+struct ClapHost
+{
+ juce::DynamicLibrary library;
+ const clap_plugin_entry_t* entry = nullptr;
+ const clap_plugin_t* plugin = nullptr;
+ const clap_plugin_gui_t* gui = nullptr;
+ bool initialised = false, created = false;
+ std::atomic callbackRequested { false };
+ clap_host_t host { CLAP_VERSION, this, "Suppressor UI tests", "Suppressor Audio",
+ "https://github.com/affinedsp/suppressor", "1.0",
+ [] (const clap_host_t*, const char*) -> const void* { return nullptr; },
+ [] (const clap_host_t*) {}, [] (const clap_host_t*) {},
+ [] (const clap_host_t* h) { static_cast (h->host_data)->callbackRequested = true; } };
+ void pump()
+ {
+ if (callbackRequested.exchange (false) && plugin != nullptr)
+ {
+ plugin->on_main_thread (plugin);
+ }
+ juce::MessageManager::getInstance()->runDispatchLoopUntil (40);
+ }
+ ~ClapHost()
+ {
+ if (created) gui->destroy (plugin);
+ if (plugin != nullptr) plugin->destroy (plugin);
+ if (initialised) entry->deinit();
+ }
+};
+}
+
+TEST_CASE ("CLAP embeds, hides, reopens and destroys the actual native editor")
+{
+ const auto path = juce::SystemStats::getEnvironmentVariable ("SUPPRESSOR_CLAP_PATH", {});
+ if (path.isEmpty()) return;
+ juce::ScopedJuceInitialiser_GUI initialiser;
+ juce::Component parent;
+ parent.setBounds (80, 80, 640, 460);
+ parent.addToDesktop (0);
+ parent.setVisible (true);
+ ClapHost h;
+ auto binary = juce::File (path);
+ #if JUCE_MAC
+ binary = binary.getChildFile ("Contents/MacOS/suppressor");
+ #endif
+ REQUIRE (h.library.open (binary.getFullPathName()));
+ h.entry = static_cast (h.library.getFunction ("clap_entry"));
+ REQUIRE (h.entry != nullptr);
+ h.initialised = h.entry->init (path.toRawUTF8());
+ REQUIRE (h.initialised);
+ auto* factory = static_cast (h.entry->get_factory (CLAP_PLUGIN_FACTORY_ID));
+ REQUIRE (factory != nullptr);
+ REQUIRE (factory->get_plugin_count (factory) == 1);
+ h.plugin = factory->create_plugin (factory, &h.host, factory->get_plugin_descriptor (factory, 0)->id);
+ REQUIRE (h.plugin != nullptr);
+ REQUIRE (h.plugin->init (h.plugin));
+ h.gui = static_cast (h.plugin->get_extension (h.plugin, CLAP_EXT_GUI));
+ REQUIRE (h.gui != nullptr);
+ clap_window_t window {};
+ #if JUCE_MAC
+ window.api = CLAP_WINDOW_API_COCOA;
+ window.cocoa = parent.getPeer()->getNativeHandle();
+ #elif JUCE_WINDOWS
+ window.api = CLAP_WINDOW_API_WIN32;
+ window.win32 = parent.getPeer()->getNativeHandle();
+ #else
+ window.api = CLAP_WINDOW_API_X11;
+ window.x11 = reinterpret_cast (parent.getPeer()->getNativeHandle());
+ #endif
+ REQUIRE (h.gui->is_api_supported (h.plugin, window.api, false));
+ for (int reopen = 0; reopen < 3; ++reopen)
+ {
+ h.created = h.gui->create (h.plugin, window.api, false);
+ REQUIRE (h.created);
+ uint32_t width = 0, height = 0;
+ REQUIRE (h.gui->get_size (h.plugin, &width, &height));
+ CHECK (width == 640);
+ CHECK (height == 460);
+ CHECK_FALSE (h.gui->can_resize (h.plugin));
+ REQUIRE (h.gui->set_parent (h.plugin, &window));
+ REQUIRE (h.gui->show (h.plugin));
+ h.pump();
+ // The pinned wrapper returns false from hide(). Embedded hosts can
+ // hide their containing window instead; exercise that fallback too.
+ h.gui->hide (h.plugin);
+ parent.setVisible (false);
+ h.pump();
+ parent.setVisible (true);
+ REQUIRE (h.gui->show (h.plugin));
+ h.pump();
+ h.gui->destroy (h.plugin);
+ h.created = false;
+ h.pump();
+ }
+}
diff --git a/Tests/test_metering.cpp b/Tests/test_metering.cpp
new file mode 100644
index 0000000..5a25dbc
--- /dev/null
+++ b/Tests/test_metering.cpp
@@ -0,0 +1,89 @@
+#include
+#include "Metering.h"
+#include
+#include
+
+using suppressor::Metering;
+using M = suppressor::MeterSnapshot;
+
+TEST_CASE ("meter snapshots start empty and reset on release")
+{
+ Metering meter;
+ M s;
+ REQUIRE (meter.read (s));
+ CHECK (s.flags == 0);
+ meter.push (0.5f, 0.25f, 20.0f, 480, 48000, 0);
+ REQUIRE (meter.read (s));
+ CHECK (s.input == 0.5f);
+ CHECK (s.output == 0.25f);
+ CHECK (s.reduction == 20.0f);
+ CHECK ((s.flags & M::active) != 0);
+ const auto sequence = s.sequence;
+ meter.push (1, 1, 1, 0, 48000, 0);
+ meter.push (1, 1, 1, 480, 0, 0);
+ REQUIRE (meter.read (s));
+ CHECK (s.sequence == sequence);
+ meter.reset();
+ REQUIRE (meter.read (s));
+ CHECK (s.flags == 0);
+ CHECK (s.input == 0);
+ CHECK (s.reduction == 0);
+}
+
+TEST_CASE ("peak decay is time-based and clips are held for one second")
+{
+ for (int block : { 48, 480, 4800 })
+ {
+ Metering meter;
+ M s;
+ meter.push (1.1f, 0.4f, 40.0f, block, 48000, 0);
+ for (int n = 0; n < 14400 / block; ++n) meter.push (0, 0, 0, block, 48000, 0);
+ REQUIRE (meter.read (s));
+ CHECK (s.input == doctest::Approx (1.1f * std::exp (-1.0f)).epsilon (0.0001));
+ CHECK (s.reduction == doctest::Approx (40.0f * std::exp (-2.5f)).epsilon (0.0001));
+ CHECK ((s.flags & M::inputClip) != 0);
+ CHECK ((s.flags & M::outputClip) == 0);
+ for (int n = 0; n < 48000 / block; ++n) meter.push (0, 0, 0, block, 48000, 0);
+ REQUIRE (meter.read (s));
+ CHECK ((s.flags & M::inputClip) == 0);
+ }
+}
+
+TEST_CASE ("bypass and learning never claim suppression, nonfinite input cannot poison meters")
+{
+ Metering meter;
+ M s;
+ meter.push (0.5f, 0.1f, 40, 480, 48000, 0);
+ for (auto flags : { M::bypassed, M::learning })
+ {
+ meter.push (0.5f, 0.5f, 40, 480, 48000, flags);
+ REQUIRE (meter.read (s));
+ CHECK (s.reduction == 0);
+ }
+ meter.push (std::numeric_limits::quiet_NaN(),
+ std::numeric_limits::infinity(), -2, 480, 48000, 0);
+ REQUIRE (meter.read (s));
+ CHECK (std::isfinite (s.input));
+ CHECK (std::isfinite (s.output));
+ CHECK (s.reduction >= 0);
+}
+
+TEST_CASE ("concurrent meter reads are coherent and never block the writer")
+{
+ Metering meter;
+ std::atomic done { false };
+ std::thread writer ([&]
+ {
+ for (int n = 1; n <= 100000; ++n)
+ meter.push (static_cast (n), static_cast (n * 2), 0, 48, 48000, 0);
+ done.store (true);
+ });
+ bool coherent = true;
+ do
+ {
+ M s;
+ if (meter.read (s)) coherent = coherent && s.output == s.input * 2 && (s.sequence & 1u) == 0;
+ } while (! done.load());
+ writer.join();
+ CHECK (coherent);
+}
diff --git a/Tests/test_ui.cpp b/Tests/test_ui.cpp
index 666a473..dbb5458 100644
--- a/Tests/test_ui.cpp
+++ b/Tests/test_ui.cpp
@@ -1,429 +1,445 @@
#include
-
#include "PluginEditor.h"
-#include "PluginLookAndFeel.h"
-
-#include
#include
-#include
#include
namespace
{
-float relativeLuminance (juce::Colour colour)
+void pump (int ms = 50)
+{
+ juce::MessageManager::getInstance()->runDispatchLoopUntil (ms);
+ juce::Timer::callPendingTimersSynchronously();
+}
+template
+bool waitFor (Predicate predicate)
{
- auto linearise = [] (float value)
+ // Message/timer delivery is asynchronous, especially on loaded CI hosts.
+ // Wait for the actual state with a deadline, not an assumed 50 ms tick.
+ const auto deadline = juce::Time::getMillisecondCounterHiRes() + 2000.0;
+ do
{
- return value <= 0.04045f ? value / 12.92f
- : std::pow ((value + 0.055f) / 1.055f, 2.4f);
- };
-
- return 0.2126f * linearise (colour.getFloatRed())
- + 0.7152f * linearise (colour.getFloatGreen())
- + 0.0722f * linearise (colour.getFloatBlue());
+ pump (20);
+ if (predicate()) return true;
+ } while (juce::Time::getMillisecondCounterHiRes() < deadline);
+ return false;
}
-
-float contrastRatio (juce::Colour first, juce::Colour second)
+void expectStatus (juce::Label& status, const juce::String& expected)
{
- const auto lighter = std::max (relativeLuminance (first), relativeLuminance (second));
- const auto darker = std::min (relativeLuminance (first), relativeLuminance (second));
- return (lighter + 0.05f) / (darker + 0.05f);
+ const bool matched = waitFor ([&] { return status.getText() == expected; });
+ CHECK_MESSAGE (matched, "Expected ", expected, "; got ", status.getText());
}
-
-int countPixelsNear (juce::Image& image, juce::Colour target)
+void setParameter (SuppressorProcessor& p, const char* id, float plain)
{
- int count = 0;
- for (int y = 0; y < image.getHeight(); ++y)
- for (int x = 0; x < image.getWidth(); ++x)
- {
- const auto pixel = image.getPixelAt (x, y);
- const auto distance = std::abs (pixel.getFloatRed() - target.getFloatRed())
- + std::abs (pixel.getFloatGreen() - target.getFloatGreen())
- + std::abs (pixel.getFloatBlue() - target.getFloatBlue());
- if (distance < 0.08f)
- ++count;
- }
- return count;
+ auto* parameter = p.apvts.getParameter (id);
+ parameter->setValueNotifyingHost (parameter->convertTo0to1 (plain));
}
-
-juce::MouseEvent makeMouseEvent (juce::Component& target, juce::ModifierKeys modifiers,
- int clicks)
+void prepare (juce::AudioProcessor& p)
{
- const auto now = juce::Time::getCurrentTime();
- return { juce::Desktop::getInstance().getMainMouseSource(), { 60.0f, 70.0f },
- modifiers, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, &target, &target, now,
- { 60.0f, 70.0f }, now, clicks, false };
+ p.setRateAndBufferSizeDetails (48000, 256);
+ p.prepareToPlay (48000, 256);
}
-
-struct ParameterGestureCounter final : juce::AudioProcessorParameter::Listener
+void feed (juce::AudioProcessor& p, int blocks = 120, bool silence = false, bool bypass = false)
{
- void parameterValueChanged (int, float) override {}
- void parameterGestureChanged (int, bool starting) override
+ juce::AudioBuffer buffer (2, 256);
+ juce::MidiBuffer midi;
+ for (int b = 0; b < blocks; ++b)
{
- starting ? ++starts : ++ends;
- }
-
- void reset()
- {
- starts = 0;
- ends = 0;
+ for (int n = 0; n < 256; ++n)
+ {
+ const auto phase = juce::MathConstants::twoPi * (b * 256 + n) / 48000.0;
+ const auto sample = silence ? 0.0f : static_cast (0.08 * std::sin (220.0 * phase)
+ + 0.003 * std::sin (11000.0 * phase));
+ buffer.setSample (0, n, sample);
+ buffer.setSample (1, n, sample * 0.7f);
+ }
+ if (bypass) p.processBlockBypassed (buffer, midi); else p.processBlock (buffer, midi);
}
-
- int starts = 0;
- int ends = 0;
-};
-
}
-
-TEST_CASE ("theme colours meet text and essential-control contrast targets")
+juce::MouseEvent event (juce::Component& c, juce::Point pos = { 60, 60 }, int modifiers = juce::ModifierKeys::leftButtonModifier)
{
- CHECK (contrastRatio (SuppressorTheme::primaryText, SuppressorTheme::canvas) >= 4.5f);
- CHECK (contrastRatio (SuppressorTheme::secondaryText, SuppressorTheme::surface) >= 4.5f);
- CHECK (contrastRatio (SuppressorTheme::accent, SuppressorTheme::canvas) >= 4.5f);
- CHECK (contrastRatio (SuppressorTheme::warning, SuppressorTheme::surface) >= 4.5f);
- CHECK (contrastRatio (SuppressorTheme::error, SuppressorTheme::surface) >= 4.5f);
- CHECK (contrastRatio (SuppressorTheme::controlOutline,
- SuppressorTheme::surfaceRaised) >= 3.0f);
+ const auto now = juce::Time::getCurrentTime();
+ return { juce::Desktop::getInstance().getMainMouseSource(), pos, juce::ModifierKeys (modifiers),
+ 1, 0, 0, 0, 0, &c, &c, now, { 60, 60 }, now, 1, true };
}
-
-TEST_CASE ("rotary value is represented by a proportional accent arc")
+struct Gestures final : juce::AudioProcessorParameter::Listener
{
- SuppressorLookAndFeel lookAndFeel;
- juce::Slider slider;
- slider.setLookAndFeel (&lookAndFeel);
-
- auto render = [&] (float position)
- {
- juce::Image image (juce::Image::RGB, 120, 120, true);
- juce::Graphics graphics (image);
- lookAndFeel.drawRotarySlider (graphics, 10, 10, 100, 100, position,
- juce::MathConstants::pi * 1.25f,
- juce::MathConstants::pi * 2.75f,
- slider);
- return image;
- };
-
- auto minimum = render (0.0f);
- auto maximum = render (1.0f);
- CHECK (countPixelsNear (maximum, SuppressorTheme::accent)
- > countPixelsNear (minimum, SuppressorTheme::accent) + 100);
-}
-
-TEST_CASE ("rotary value field is centred and leaves the complete dial draggable")
+ void parameterValueChanged (int, float) override { ++changes; }
+ void parameterGestureChanged (int, bool start) override { start ? ++starts : ++ends; }
+ int starts = 0, ends = 0, changes = 0;
+};
+juce::Label* namedLabel (juce::Component& editor, const juce::String& name)
{
- juce::ScopedJuceInitialiser_GUI gui;
- SuppressorLookAndFeel lookAndFeel;
- juce::Component parent;
- ParameterSlider slider;
- parent.setLookAndFeel (&lookAndFeel);
- parent.setBounds (0, 0, 120, 140);
- parent.addAndMakeVisible (slider);
- slider.setBounds (parent.getLocalBounds());
- slider.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
- slider.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 80, 24);
-
- const auto layout = lookAndFeel.getSliderLayout (slider);
- CHECK (layout.sliderBounds == slider.getLocalBounds());
- CHECK (layout.textBoxBounds.getCentre() == slider.getLocalBounds().getCentre());
-
- auto* valueLabel = dynamic_cast (slider.getChildComponent (0));
- REQUIRE (valueLabel != nullptr);
- bool labelInterceptsClicks = true;
- bool editorInterceptsClicks = false;
- valueLabel->getInterceptsMouseClicks (labelInterceptsClicks, editorInterceptsClicks);
- CHECK_FALSE (labelInterceptsClicks);
- CHECK (editorInterceptsClicks);
- CHECK (slider.getComponentAt (layout.textBoxBounds.getCentre()) == &slider);
+ for (auto* c : editor.getChildren())
+ if (c->getName() == name) return dynamic_cast (c);
+ return nullptr;
}
-
-TEST_CASE ("bundled display font is available in both weights")
+void capture (juce::Component& editor, const juce::String& name, float scale = 1.0f)
{
- const auto regular = SuppressorTheme::makeFont (18.0f);
- const auto semibold = SuppressorTheme::makeFont (18.0f, true);
- CHECK (regular.getTypefaceName() == "Barlow Condensed");
- CHECK (semibold.getTypefaceName() == "Barlow Condensed");
- CHECK (regular.getTypefaceStyle() != semibold.getTypefaceStyle());
+ const auto path = juce::SystemStats::getEnvironmentVariable ("SUPPRESSOR_UI_CAPTURE_DIR", {});
+ if (path.isEmpty()) return;
+ auto dir = juce::File (path);
+ REQUIRE (dir.createDirectory().wasOk());
+ const auto image = editor.createComponentSnapshot (editor.getLocalBounds(), true, scale);
+ auto file = dir.getChildFile (name + ".png");
+ file.deleteFile();
+ auto stream = file.createOutputStream();
+ REQUIRE (stream != nullptr);
+ REQUIRE (juce::PNGImageFormat().writeImageToStream (image, *stream));
+ stream->flush();
}
-
-TEST_CASE ("double-click opens exact value entry instead of resetting")
+float contrast (juce::Colour a, juce::Colour b)
{
- juce::ScopedJuceInitialiser_GUI gui;
- SuppressorLookAndFeel lookAndFeel;
- ParameterSlider slider;
- slider.setLookAndFeel (&lookAndFeel);
- slider.setBounds (0, 0, 120, 140);
- slider.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
- slider.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 80, 24);
- slider.setDoubleClickReturnValue (true, 0.5);
- slider.setValue (0.8);
-
- auto event = makeMouseEvent (slider,
- juce::ModifierKeys (juce::ModifierKeys::leftButtonModifier),
- 2);
- slider.mouseDoubleClick (event);
-
- auto* valueLabel = dynamic_cast (slider.getChildComponent (0));
- REQUIRE (valueLabel != nullptr);
- CHECK (valueLabel->isBeingEdited());
- CHECK (slider.getValue() == doctest::Approx (0.8));
- slider.hideTextBox (true);
+ auto lum = [] (juce::Colour c)
+ {
+ auto linear = [] (float v) { return v <= 0.04045f ? v / 12.92f : std::pow ((v + 0.055f) / 1.055f, 2.4f); };
+ return .2126f * linear (c.getFloatRed()) + .7152f * linear (c.getFloatGreen()) + .0722f * linear (c.getFloatBlue());
+ };
+ return (std::max (lum (a), lum (b)) + .05f) / (std::min (lum (a), lum (b)) + .05f);
}
+} // namespace
-TEST_CASE ("invalid exact-entry text preserves the current value")
+TEST_CASE ("theme retains bundled fonts and accessible contrast")
{
- ParameterSlider slider;
- slider.setRange (-80.0, 0.0, 0.1);
- slider.setTextValueSuffix (" dB");
- slider.setValue (-40.0);
-
- CHECK (slider.getValueFromText ("not a number") == doctest::Approx (-40.0));
- CHECK (slider.getValueFromText ("--12") == doctest::Approx (-40.0));
- CHECK (slider.getValueFromText ("1e2") == doctest::Approx (-40.0));
- CHECK (slider.getValueFromText ("0x10") == doctest::Approx (-40.0));
- CHECK (slider.getValueFromText ("-12.5 garbage") == doctest::Approx (-40.0));
- CHECK (slider.getValueFromText ("-12.5 dB") == doctest::Approx (-12.5));
+ using namespace SuppressorTheme;
+ CHECK (makeFont (18).getTypefaceName() == "Barlow Condensed");
+ CHECK (makeFont (18).getTypefaceStyle() != makeFont (18, true).getTypefaceStyle());
+ for (auto colour : { primaryText, secondaryText, accent, warning, error })
+ CHECK (contrast (colour, surface) >= 4.5f);
+ CHECK (contrast (controlOutline, surfaceRaised) >= 3.0f);
}
-TEST_CASE ("Alt/Option-click remains the non-conflicting default reset gesture")
+TEST_CASE ("four controls preserve the complete host parameter and saved-state contract")
{
- struct DragCounter final : juce::Slider::Listener
- {
- void sliderValueChanged (juce::Slider*) override {}
- void sliderDragStarted (juce::Slider*) override { ++starts; }
- void sliderDragEnded (juce::Slider*) override { ++ends; }
-
- int starts = 0;
- int ends = 0;
- } counter;
-
juce::ScopedJuceInitialiser_GUI gui;
- ParameterSlider slider;
- slider.addListener (&counter);
- slider.setBounds (0, 0, 120, 140);
- slider.setRange (0.0, 1.0, 0.01);
- slider.setDoubleClickReturnValue (true, 0.25, juce::ModifierKeys::altModifier);
- slider.setValue (0.8);
-
- auto event = makeMouseEvent (
- slider,
- juce::ModifierKeys (juce::ModifierKeys::leftButtonModifier
- | juce::ModifierKeys::altModifier),
- 1);
- slider.mouseDown (event);
- CHECK (slider.getValue() == doctest::Approx (0.25));
- CHECK (counter.starts == 1);
- CHECK (counter.ends == 1);
-
- auto dragEvent = makeMouseEvent (
- slider, juce::ModifierKeys (juce::ModifierKeys::leftButtonModifier), 1);
- slider.mouseDown (dragEvent);
- CHECK (counter.starts == 2);
- CHECK (counter.ends == 1);
- slider.endActiveGesture();
- CHECK (counter.ends == 2);
- CHECK (slider.getThumbBeingDragged() == -1);
-}
-
-
-TEST_CASE ("editor integrates every parameter with truthful state and balanced gestures")
-{
- juce::ScopedJuceInitialiser_GUI gui;
- SuppressorProcessor processor;
- auto editor = std::make_unique (processor);
-
- const juce::StringArray parameterIds {
- "strength", "threshold", "release", "gateMode", "depth", "hysteresis", "hold",
- "adaptiveRelease", "cue", "sidechain", "lookahead", "humEnable", "humBase",
- "humHarmonics", "humStrength", "humLearn", "bandMode", "bandsLearn",
- "deltaAudition", "outputGain"
- };
-
- CHECK (editor->getWidth() == 1000);
- CHECK (editor->getHeight() == 640);
- CHECK_FALSE (editor->isResizable());
- REQUIRE (editor->getNumChildComponents() > 0);
- CHECK (editor->getChildComponent (0)->getName() == "Suppression activity");
- CHECK (editor->getChildComponent (0)->getExplicitFocusOrder() == 1);
- CHECK (processor.getParameters().size() == parameterIds.size());
-
- std::set focusOrders;
- std::set accessibleNames;
- for (const auto& id : parameterIds)
+ SuppressorProcessor p;
+ SuppressorEditor editor (p);
+ editor.addToDesktop (0);
+ editor.setVisible (true);
+ const juce::StringArray ids { "strength", "threshold", "release", "gateMode", "depth", "hysteresis", "hold",
+ "adaptiveRelease", "cue", "sidechain", "lookahead", "humEnable", "humBase", "humHarmonics", "humStrength",
+ "humLearn", "bandMode", "bandsLearn", "deltaAudition", "outputGain" };
+ const std::array defaults { .9f, -40, 6, 0, 40, 6, 2, 0, 0, 0, 0, 0, 0, 8, 1, 0, 0, 0, 0, 0 };
+ REQUIRE (p.getParameters().size() == ids.size());
+ CHECK (editor.getWidth() == 640);
+ CHECK (editor.getHeight() == 460);
+ CHECK_FALSE (editor.isResizable());
+ std::set focus;
+ int controls = 0;
+ for (int i = 0; i < ids.size(); ++i)
{
- CAPTURE (id);
- auto* component = editor->findChildWithID (id);
- auto* parameter = processor.apvts.getParameter (id);
- REQUIRE (component != nullptr);
- REQUIRE (parameter != nullptr);
-
- const auto parameterIndex = processor.getParameters().indexOf (parameter);
- CHECK (editor->getControlParameterIndex (*component) == parameterIndex);
- CHECK (component->isVisible());
- CHECK (component->getWidth() >= 32);
- CHECK (component->getHeight() >= 32);
- CHECK (component->getWantsKeyboardFocus());
- CHECK (component->getExplicitFocusOrder() > 0);
- CHECK (focusOrders.insert (component->getExplicitFocusOrder()).second);
- CHECK (component->getName().isNotEmpty());
- CHECK (accessibleNames.insert (component->getName().toStdString()).second);
-
- if (auto* slider = dynamic_cast (component))
+ auto* param = p.apvts.getParameter (ids[i]);
+ REQUIRE (param != nullptr);
+ CHECK (p.getParameters()[i] == param);
+ CHECK (param->getVersionHint() == 1);
+ CHECK (param->convertFrom0to1 (param->getDefaultValue()) == doctest::Approx (defaults[static_cast (i)]));
+ auto* component = editor.findChildWithID (ids[i]);
+ const bool visible = ids[i] == "strength" || ids[i] == "threshold" || ids[i] == "release" || ids[i] == "deltaAudition";
+ CHECK ((component != nullptr) == visible);
+ if (component != nullptr)
{
- CHECK (slider->getTextFromValue (slider->getValue()).isNotEmpty());
- REQUIRE (slider->getNumChildComponents() > 0);
- CHECK (editor->getControlParameterIndex (*slider->getChildComponent (0))
- == parameterIndex);
+ ++controls;
+ CHECK (editor.getControlParameterIndex (*component) == i);
+ CHECK (component->getWidth() >= 32);
+ CHECK (component->getHeight() >= 32);
+ CHECK (component->getWantsKeyboardFocus());
+ CHECK (component->getName().isNotEmpty());
+ CHECK (component->getAccessibilityHandler() != nullptr);
+ CHECK (focus.insert (component->getExplicitFocusOrder()).second);
}
+ param->setValueNotifyingHost (param->convertTo0to1 (param->convertFrom0to1 (0.27f)));
}
- CHECK (focusOrders.size() == 20);
- CHECK (accessibleNames.size() == 20);
-
- auto* strength = dynamic_cast (editor->findChildWithID ("strength"));
- auto* strengthParameter = processor.apvts.getParameter ("strength");
- REQUIRE (strength != nullptr);
- REQUIRE (strengthParameter != nullptr);
- CHECK (strength->getValueFromText ("50 %") == doctest::Approx (0.5));
- CHECK (strength->getValueFromText ("50 garbage")
- == doctest::Approx (strength->getValue()));
- CHECK (strength->getValueFromText ("1e2")
- == doctest::Approx (strength->getValue()));
-
- ParameterGestureCounter gestures;
- strengthParameter->addListener (&gestures);
- strength->setValue (0.2, juce::sendNotificationSync);
- gestures.reset();
- CHECK (strength->keyPressed (juce::KeyPress (juce::KeyPress::rightKey)));
- CHECK (gestures.starts == 1);
- CHECK (gestures.ends == 1);
+ CHECK (controls == 4);
+ juce::MemoryBlock saved;
+ p.getStateInformation (saved);
+ SuppressorProcessor recalled;
+ recalled.setStateInformation (saved.getData(), static_cast (saved.getSize()));
+ for (const auto& id : ids)
+ CHECK (recalled.apvts.getParameter (id)->getValue() == doctest::Approx (p.apvts.getParameter (id)->getValue()));
+}
- strength->setValue (0.2, juce::sendNotificationSync);
- gestures.reset();
- auto resetEvent = makeMouseEvent (
- *strength,
- juce::ModifierKeys (juce::ModifierKeys::leftButtonModifier
- | juce::ModifierKeys::altModifier),
- 1);
- strength->mouseDown (resetEvent);
- CHECK (strength->getValue() == doctest::Approx (0.9));
+TEST_CASE ("dials support balanced drag, fine, exact, cancel, invalid, reset and close gestures")
+{
+ juce::ScopedJuceInitialiser_GUI gui;
+ SuppressorProcessor p;
+ auto editor = std::make_unique (p);
+ editor->addToDesktop (0);
+ editor->setVisible (true);
+ auto* dial = dynamic_cast (editor->findChildWithID ("strength"));
+ REQUIRE (dial != nullptr);
+ auto& param = dial->parameter;
+ Gestures gestures;
+ param.addListener (&gestures);
+ CHECK (dial->getComponentAt (dial->getLocalBounds().getCentre()) == dial);
+ setParameter (p, "strength", .5f);
+ dial->mouseDown (event (*dial));
+ dial->mouseDrag (event (*dial, { 60, 36 }));
+ dial->mouseUp (event (*dial));
+ CHECK (dial->getValue() == doctest::Approx (.6));
CHECK (gestures.starts == 1);
CHECK (gestures.ends == 1);
-
- auto* adaptive = dynamic_cast (
- editor->findChildWithID ("adaptiveRelease"));
- auto* adaptiveParameter = processor.apvts.getParameter ("adaptiveRelease");
- REQUIRE (adaptive != nullptr);
- REQUIRE (adaptiveParameter != nullptr);
- ParameterGestureCounter buttonGestures;
- adaptiveParameter->addListener (&buttonGestures);
- CHECK (adaptive->keyPressed (juce::KeyPress (' ')));
- CHECK (adaptive->getToggleState());
+ dial->mouseDown (event (*dial));
+ dial->mouseDrag (event (*dial, { 60, 36 }, juce::ModifierKeys::leftButtonModifier | juce::ModifierKeys::shiftModifier));
+ dial->mouseUp (event (*dial));
+ CHECK (dial->getValue() == doctest::Approx (.61));
+ CHECK (dial->keyPressed (juce::KeyPress (juce::KeyPress::rightKey)));
+ CHECK (dial->getValue() == doctest::Approx (.62));
+ auto* threshold = dynamic_cast (editor->findChildWithID ("threshold"));
+ REQUIRE (threshold != nullptr);
+ threshold->mouseDown (event (*threshold));
+ for (int pixel = 1; pixel <= 6; ++pixel)
+ threshold->mouseDrag (event (*threshold, { 60, 60.0f - static_cast (pixel) },
+ juce::ModifierKeys::leftButtonModifier | juce::ModifierKeys::shiftModifier));
+ threshold->mouseUp (event (*threshold));
+ CHECK (threshold->getValue() == doctest::Approx (-39.8));
+
+ auto enter = [&] (const juce::String& value, bool commit)
+ {
+ dial->keyPressed (juce::KeyPress (juce::KeyPress::returnKey));
+ auto* entry = dynamic_cast (dial->getChildComponent (0));
+ REQUIRE (entry != nullptr);
+ REQUIRE (entry->isVisible());
+ CHECK (editor->getControlParameterIndex (*entry) == param.getParameterIndex());
+ entry->setText (value);
+ if (commit) entry->onReturnKey(); else entry->onEscapeKey();
+ CHECK_FALSE (entry->isVisible());
+ };
+ enter ("35 %", true);
+ CHECK (param.convertFrom0to1 (param.getValue()) == doctest::Approx (.35));
+ enter ("80 %", false);
+ CHECK (dial->getValue() == doctest::Approx (.35));
+ for (const auto& invalid : { "junk", "nan", "inf", "--12", "12 garbage", "0x10", "1e2", "" })
+ {
+ enter (invalid, true);
+ CHECK (dial->getValue() == doctest::Approx (.35));
+ }
+ enter ("999 %", true);
+ CHECK (dial->getValue() == 1.0);
+ dial->mouseDown (event (*dial, { 60, 60 }, juce::ModifierKeys::leftButtonModifier | juce::ModifierKeys::altModifier));
+ CHECK (dial->getValue() == doctest::Approx (.9));
+ setParameter (p, "strength", .42f);
+ CHECK (dial->getValue() == doctest::Approx (.42));
+ capture (*editor, "suppressor-exact-entry-before");
+ dial->beginEntry();
+ capture (*editor, "suppressor-exact-entry");
+ CHECK (gestures.starts == gestures.ends);
+ auto* listen = dynamic_cast (editor->findChildWithID ("deltaAudition"));
+ REQUIRE (listen != nullptr);
+ Gestures buttonGestures;
+ auto* listenParam = p.apvts.getParameter ("deltaAudition");
+ listenParam->addListener (&buttonGestures);
+ CHECK (listen->keyPressed (juce::KeyPress (' ')));
+ CHECK (listenParam->getValue() == doctest::Approx (1.0));
CHECK (buttonGestures.starts == 1);
CHECK (buttonGestures.ends == 1);
- adaptiveParameter->removeListener (&buttonGestures);
-
- auto* bandMode = dynamic_cast (editor->findChildWithID ("bandMode"));
- auto* bandsLearn = dynamic_cast (
- editor->findChildWithID ("bandsLearn"));
- auto* bandsLearnParameter = processor.apvts.getParameter ("bandsLearn");
- REQUIRE (bandMode != nullptr);
- REQUIRE (bandsLearn != nullptr);
- REQUIRE (bandsLearnParameter != nullptr);
- ParameterGestureCounter learnGestures;
- bandsLearnParameter->addListener (&learnGestures);
- bandMode->setSelectedItemIndex (1, juce::sendNotificationSync);
- CHECK (bandsLearn->keyPressed (juce::KeyPress (' ')));
- CHECK (bandsLearn->getToggleState());
- CHECK (bandsLearnParameter->getValue() == doctest::Approx (1.0f));
- CHECK (learnGestures.starts == 1);
- CHECK (learnGestures.ends == 1);
- bandsLearnParameter->removeListener (&learnGestures);
- bandMode->setSelectedItemIndex (2, juce::sendNotificationSync);
+ listenParam->removeListener (&buttonGestures);
+ dial->mouseDown (event (*dial));
+ editor->setVisible (false);
+ CHECK (gestures.starts == gestures.ends);
+ editor->setVisible (true);
+ dial->mouseDown (event (*dial));
+ CHECK (gestures.starts == gestures.ends + 1);
+ editor.reset();
+ CHECK (gestures.starts == gestures.ends);
+ param.removeListener (&gestures);
+}
- juce::Component* activity = nullptr;
- juce::Label* bandsStatus = nullptr;
- for (auto* child : editor->getChildren())
+TEST_CASE ("processor metering follows actual output without altering DSP")
+{
+ juce::ScopedJuceInitialiser_GUI gui;
+ SuppressorProcessor p;
+ prepare (p);
+ suppressor::DenoiserEngine reference;
+ reference.prepare (48000, 2, 120);
+ suppressor::EngineParams targets;
+ targets.strength01 = p.apvts.getRawParameterValue ("strength")->load();
+ reference.setTargets (targets);
+ reference.reset();
+ juce::AudioBuffer audio (2, 256), expected (2, 256);
+ juce::MidiBuffer midi;
+ float* io[] { expected.getWritePointer (0), expected.getWritePointer (1) };
+ for (int block = 0; block < 50; ++block)
{
- if (child->getName() == "Suppression activity")
- activity = child;
- if (child->getName() == "Multiband learning status")
- bandsStatus = dynamic_cast (child);
+ for (int ch = 0; ch < 2; ++ch)
+ for (int n = 0; n < 256; ++n)
+ audio.setSample (ch, n, static_cast (.03 * std::sin ((block * 256 + n) * .12)));
+ expected.makeCopyOf (audio);
+ reference.setTargets (targets);
+ reference.processBlock (io, nullptr, 0, 256, 2);
+ p.processBlock (audio, midi);
+ for (int ch = 0; ch < 2; ++ch)
+ CHECK (std::equal (audio.getReadPointer (ch), audio.getReadPointer (ch) + 256, expected.getReadPointer (ch)));
}
- REQUIRE (activity != nullptr);
- REQUIRE (bandsStatus != nullptr);
- CHECK (activity->getDescription().containsIgnoreCase ("learn armed"));
- CHECK (activity->getDescription().containsIgnoreCase ("gain reduction unavailable"));
- CHECK (activity->getDescription().containsIgnoreCase ("passes while processing"));
- CHECK (bandsStatus->getText().containsIgnoreCase ("armed"));
- CHECK_FALSE (bandsStatus->getText().containsIgnoreCase ("capturing"));
-
- auto* delta = dynamic_cast (editor->findChildWithID ("deltaAudition"));
- REQUIRE (delta != nullptr);
- delta->setToggleState (true, juce::sendNotificationSync);
- bandMode->setSelectedItemIndex (1, juce::sendNotificationSync);
- CHECK (activity->getDescription().containsIgnoreCase ("output: removed signal"));
+ for (bool delta : { false, true })
+ {
+ p.releaseResources();
+ prepare (p);
+ setParameter (p, "deltaAudition", delta ? 1.0f : 0.0f);
+ setParameter (p, "outputGain", 6);
+ audio.clear();
+ audio.setSample (1, 0, .8f);
+ p.processBlock (audio, midi);
+ suppressor::MeterSnapshot s;
+ REQUIRE (p.readMeters (s));
+ CHECK (s.input == doctest::Approx (.8f));
+ CHECK (s.output == doctest::Approx (audio.getMagnitude (0, 256)));
+ CHECK (((s.flags & suppressor::MeterSnapshot::removed) != 0) == delta);
+ }
+ p.releaseResources();
+ suppressor::MeterSnapshot s;
+ REQUIRE (p.readMeters (s));
+ CHECK (s.flags == 0);
+}
- gestures.reset();
+TEST_CASE ("live meter states, stale data, legacy modes and monitor labels are explicit")
+{
+ juce::ScopedJuceInitialiser_GUI gui;
+ SuppressorProcessor p;
+ prepare (p);
+ auto editor = std::make_unique (p);
+ editor->addToDesktop (0);
+ editor->setVisible (true);
+ pump();
+ juce::Component::unfocusAllComponents();
+ auto* status = namedLabel (*editor, "Processing status");
+ auto* summary = namedLabel (*editor, "Signal meters");
+ REQUIRE (status != nullptr);
+ REQUIRE (summary != nullptr);
+ CHECK (status->getText() == "No audio");
+ capture (*editor, "suppressor-default");
+ feed (p);
+ expectStatus (*status, "Suppressing");
+ CHECK (summary->getDescription().contains ("High-band reduction"));
+ capture (*editor, "suppressor-ui");
+ for (float scale : { 1.0f, 1.25f, 1.5f, 1.75f, 2.0f })
{
- auto dragEvent = makeMouseEvent (
- *strength, juce::ModifierKeys (juce::ModifierKeys::leftButtonModifier), 1);
- strength->mouseDown (dragEvent);
+ editor->setScaleFactor (scale);
+ CHECK (editor->getWidth() == 640);
+ CHECK (editor->getLocalBounds().toFloat().transformedBy (editor->getTransform()).getWidth()
+ == doctest::Approx (640 * scale));
+ capture (*editor, "suppressor-scale-" + juce::String (juce::roundToInt (scale * 100)), scale);
}
- CHECK (gestures.starts == 1);
- CHECK (gestures.ends == 0);
+ editor->setScaleFactor (1);
+ setParameter (p, "deltaAudition", 1);
+ expectStatus (*status, "No audio"); // old output is not relabelled as removed audio
+ feed (p);
+ expectStatus (*status, "Listening to difference");
+ CHECK (summary->getDescription().contains ("removed"));
+ capture (*editor, "suppressor-delta-audition");
+ feed (p, 120, false, true);
+ expectStatus (*status, "Bypassed");
+ capture (*editor, "suppressor-bypassed");
+ setParameter (p, "deltaAudition", 0);
+ feed (p, 700, true);
+ expectStatus (*status, "No input");
+ capture (*editor, "suppressor-silence");
+ setParameter (p, "bandMode", 1); setParameter (p, "bandsLearn", 1);
+ feed (p);
+ expectStatus (*status, "Learning bands");
+ CHECK (summary->getDescription().contains ("unavailable"));
+ CHECK_FALSE (editor->findChildWithID ("threshold")->isEnabled());
+ CHECK_FALSE (editor->findChildWithID ("strength")->isEnabled());
+ CHECK (namedLabel (*editor, "Host settings")->getText() == "Host settings active");
+ capture (*editor, "suppressor-learning");
+ setParameter (p, "bandsLearn", 0); feed (p);
+ CHECK (waitFor ([&] { return summary->getDescription().contains ("Maximum band reduction"); }));
+ expectStatus (*status, "No audio");
+ CHECK (summary->getDescription() == "No current audio readings");
editor.reset();
- CHECK (gestures.ends == 1);
- strengthParameter->removeListener (&gestures);
+ editor = std::make_unique (p);
+ editor->addToDesktop (0);
+ editor->setVisible (true); pump();
+ CHECK (namedLabel (*editor, "Processing status")->getText() == "No audio");
}
-TEST_CASE ("declared host scale factors preserve logical layout and render cleanly")
+TEST_CASE ("sidechain levels are excluded and clipping is visible and accessible")
{
juce::ScopedJuceInitialiser_GUI gui;
- const std::array scales { 1.0f, 1.25f, 1.5f, 1.75f, 2.0f };
+ SuppressorProcessor p;
+ auto layout = p.getBusesLayout();
+ layout.inputBuses.set (1, juce::AudioChannelSet::stereo());
+ REQUIRE (p.setBusesLayout (layout));
+ prepare (p);
+ SuppressorEditor editor (p);
+ editor.addToDesktop (0);
+ editor.setVisible (true);
+ juce::AudioBuffer buffer (4, 256);
+ juce::MidiBuffer midi;
+ buffer.clear();
+ buffer.setSample (0, 0, .25f);
+ buffer.setSample (2, 0, 4.0f);
+ p.processBlock (buffer, midi);
+ suppressor::MeterSnapshot meters;
+ REQUIRE (p.readMeters (meters));
+ CHECK (meters.input == doctest::Approx (.25f));
+ CHECK ((meters.flags & suppressor::MeterSnapshot::inputClip) == 0);
+ buffer.clear();
+ buffer.setSample (1, 0, 1.25f);
+ p.processBlockBypassed (buffer, midi);
+ pump();
+ auto* summary = namedLabel (editor, "Signal meters");
+ REQUIRE (summary != nullptr);
+ CHECK (waitFor ([&] { return summary->getDescription().contains ("Input clipped")
+ && summary->getDescription().contains ("Output clipped"); }));
+ capture (editor, "suppressor-clipping");
+ for (float value : { 0.0f, 1.0f })
+ {
+ for (const char* id : { "threshold", "strength", "release" })
+ p.apvts.getParameter (id)->setValueNotifyingHost (value);
+ pump();
+ capture (editor, value > .5f ? "suppressor-maximum" : "suppressor-minimum");
+ }
+}
- for (const auto scale : scales)
+TEST_CASE ("VST3 native editor lifecycle and host parameter state recall")
+{
+ const auto path = juce::SystemStats::getEnvironmentVariable ("SUPPRESSOR_VST3_PATH", {});
+ if (path.isEmpty()) return;
+ juce::ScopedJuceInitialiser_GUI gui;
+ juce::VST3PluginFormat format;
+ juce::OwnedArray types;
+ format.findAllTypesForFile (types, path);
+ REQUIRE (types.size() == 1);
+ juce::String errorMessage;
+ auto p = format.createInstanceFromDescription (*types[0], 48000, 256, errorMessage);
+ INFO (errorMessage);
+ REQUIRE (p != nullptr);
+ prepare (*p);
+ juce::Component hostWindow;
+ hostWindow.setBounds (80, 80, 640, 460);
+ hostWindow.addToDesktop (0);
+ hostWindow.setVisible (true);
+ pump();
+ for (int reopen = 0; reopen < 3; ++reopen)
{
- CAPTURE (scale);
- SuppressorProcessor processor;
- auto editor = std::make_unique (processor);
- const auto width = juce::roundToInt (1000.0f * scale);
- const auto height = juce::roundToInt (640.0f * scale);
+ std::unique_ptr editor (p->createEditorIfNeeded());
+ REQUIRE (editor != nullptr);
+ // Exercise embedding in the host's native window, rather than making
+ // the VST3 adapter itself the top-level window.
+ hostWindow.addAndMakeVisible (*editor);
+ pump();
+ feed (*p); pump();
+ CHECK (editor->getWidth() == 640);
+ CHECK (editor->getHeight() == 460);
+ // This is the host's native embedding view, not the plugin's JUCE
+ // component tree. Exercise the real wrapper rather than casting it.
+ REQUIRE (p->getParameters().size() == 21); // includes VST3 wrapper bypass
+ auto* parameter = p->getParameters()[1];
+ CHECK (parameter->getName (64) == "Threshold");
+ parameter->setValueNotifyingHost (.6f);
+ feed (*p); pump();
+ juce::MemoryBlock state;
+ p->getStateInformation (state);
+ const auto saved = parameter->getValue();
+ parameter->setValueNotifyingHost (.8f);
+ feed (*p); pump();
+ CHECK (parameter->getValue() == doctest::Approx (.8f));
+ p->setStateInformation (state.getData(), static_cast (state.getSize()));
+ feed (*p); pump();
+ CHECK (parameter->getValue() == doctest::Approx (saved));
+ editor->setVisible (false);
+ pump();
editor->setVisible (true);
- editor->setScaleFactor (scale);
-
- CHECK (editor->getWidth() == 1000);
- CHECK (editor->getHeight() == 640);
- const auto transformedBounds = editor->getLocalBounds().toFloat().transformedBy (
- editor->getTransform());
- CHECK (transformedBounds.getWidth() == doctest::Approx (width));
- CHECK (transformedBounds.getHeight() == doctest::Approx (height));
-
- juce::Image image (juce::Image::ARGB, width, height, true);
- {
- juce::Graphics graphics (image);
- graphics.addTransform (juce::AffineTransform::scale (scale));
- editor->paintEntireComponent (graphics, true);
- }
- CHECK (image.getPixelAt (width - 2, height - 2).getAlpha() == 0xff);
- CHECK (countPixelsNear (image, SuppressorTheme::accent)
- > juce::roundToInt (100.0f * scale * scale));
-
- const auto captureDirectory = juce::SystemStats::getEnvironmentVariable (
- "SUPPRESSOR_UI_CAPTURE_DIR", {});
- auto captureSucceeded = true;
- if (captureDirectory.isNotEmpty())
- {
- const auto directory = juce::File (captureDirectory);
- captureSucceeded = directory.createDirectory().wasOk();
- auto output = directory.getChildFile (
- "suppressor-ui-" + juce::String (juce::roundToInt (scale * 100.0f))
- + ".png");
- juce::PNGImageFormat format;
- auto stream = output.createOutputStream();
- captureSucceeded = captureSucceeded && stream != nullptr;
- if (stream != nullptr)
- captureSucceeded = captureSucceeded
- && format.writeImageToStream (image, *stream);
- }
- CHECK (captureSucceeded);
+ pump();
}
+ p->releaseResources();
}
diff --git a/Tests/test_ui_main.cpp b/Tests/test_ui_main.cpp
new file mode 100644
index 0000000..10ba639
--- /dev/null
+++ b/Tests/test_ui_main.cpp
@@ -0,0 +1,33 @@
+#define DOCTEST_CONFIG_IMPLEMENT
+#include
+#include
+
+// Native embedding needs an actual host application, not only a message-thread
+// initializer. In particular JUCE's XEmbed host makes an initial property query
+// before the client window exists; JUCEApplication owns the platform event/error
+// handling for that lifecycle, just as AudioPluginHost and pluginval do.
+class UITestApplication final : public juce::JUCEApplication
+{
+public:
+ const juce::String getApplicationName() override { return "Suppressor UI tests"; }
+ const juce::String getApplicationVersion() override { return "1.0"; }
+ bool moreThanOneInstanceAllowed() override { return true; }
+
+ void initialise (const juce::String&) override
+ {
+ juce::MessageManager::callAsync ([this]
+ {
+ auto arguments = getCommandLineParameterArray();
+ arguments.insert (0, getApplicationName());
+ std::vector argv;
+ for (const auto& argument : arguments) argv.push_back (argument.toRawUTF8());
+ doctest::Context context (static_cast (argv.size()), argv.data());
+ setApplicationReturnValue (context.run());
+ quit();
+ });
+ }
+
+ void shutdown() override {}
+};
+
+START_JUCE_APPLICATION (UITestApplication)
diff --git a/docs/images/suppressor-ui.png b/docs/images/suppressor-ui.png
index 17e2614..eac0e87 100644
Binary files a/docs/images/suppressor-ui.png and b/docs/images/suppressor-ui.png differ