Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
43 changes: 34 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

[![CI](https://github.com/user1303836/suppressor/actions/workflows/ci.yml/badge.svg)](https://github.com/user1303836/suppressor/actions/workflows/ci.yml)
[![CI](https://github.com/affinedsp/suppressor/actions/workflows/ci.yml/badge.svg)](https://github.com/affinedsp/suppressor/actions/workflows/ci.yml)

<p align="center">
<img src="docs/images/suppressor-ui.png" alt="Suppressor plugin interface" width="900">
<img src="docs/images/suppressor-ui.png" alt="Suppressor: three essential knobs, input/output peak meters, and a clearly labelled high-band reduction display" width="640">
</p>

## What it does
Expand All @@ -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

Expand All @@ -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:
Expand Down
28 changes: 24 additions & 4 deletions Source/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -56,21 +56,41 @@ 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
${CMAKE_CURRENT_SOURCE_DIR})

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
$<$<PLATFORM_ID:Windows>:_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=$<TARGET_GENEX_EVAL:Suppressor_VST3,$<TARGET_PROPERTY:Suppressor_VST3,JUCE_PLUGIN_ARTEFACT_FILE>>")
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 "$<TARGET_BUNDLE_DIR:Suppressor_CLAP>")
else()
set(clap_test_path "$<TARGET_FILE:Suppressor_CLAP>")
endif()
set_property(TEST suppressor-ui-unit APPEND PROPERTY ENVIRONMENT
"SUPPRESSOR_CLAP_PATH=${clap_test_path}")
endif()
endif()
86 changes: 86 additions & 0 deletions Source/Metering.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#pragma once

#include <algorithm>
#include <atomic>
#include <cmath>
#include <cstdint>

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<float>::is_always_lock_free);
static_assert (std::atomic<uint32_t>::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<float> (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<float> input { 0 }, output { 0 }, gr { 0 };
std::atomic<uint32_t> status { 0 }, sequence { 0 };
};
} // namespace suppressor
Loading
Loading