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
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ body alone. Formats: **VST3** and **CLAP** (Windows, macOS, Linux).
[![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: input and output LED ladders around a backlit high-band reduction meter, with Threshold, Strength and Release knobs and a Listen removed key" width="800">
<img src="docs/images/suppressor-ui.png" alt="Suppressor in the Stealth finish: a screen plotting six seconds of high-band reduction beside input and output bar meters, with Threshold, Strength and Release knobs and a Listen key" width="800">
</p>

## What it does
Expand Down Expand Up @@ -37,13 +37,14 @@ biquads plus an envelope per channel).
| **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. |

The **IN** and **OUT** ladders show **peak dBFS**, each with a clip cell held for
one second. The backlit meter shows **high-band gain reduction**, not
whole-signal loudness loss; in multiband sessions it shows the deepest band
reduction. While *Listen removed* is on, the output column is relabelled
**REMOVED**. Bypass, silence, learning, and stopped audio are labelled
explicitly, and stopped meters go dark (the meter lamp dims and the needle
rests) instead of holding stale readings.
The screen plots **high-band gain reduction** over the last six seconds, not
whole-signal loudness loss, with the current value above the plot; in multiband
sessions it shows the deepest band reduction. The **INPUT** and **OUTPUT** bars
show **peak dBFS** with a readout, each with a clip cell held for one second.
While *Listen removed* is on, the output bar is relabelled **REMOVED**. Bypass,
silence, learning, and stopped audio are labelled explicitly; the plot breaks
wherever there was no reading, and stopped bars go dark instead of holding
stale readings.

Drag a knob to adjust; **Shift-drag** for fine control, **double-click / Return**
for exact entry in its readout, and **Alt/Option-click / Home** to reset.
Expand Down
402 changes: 284 additions & 118 deletions Source/PluginEditor.cpp

Large diffs are not rendered by default.

41 changes: 35 additions & 6 deletions Source/PluginEditor.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace SuppressorTheme
{
/** Graphite anodising with a glacier-cyan emission: the Suppressor finish of the Affine family. */
/** Stealth finish: matte powder coat, white LED rings and a colour screen. */
affine::Theme theme();
}

Expand All @@ -15,6 +15,36 @@ class ListenButton final : public affine::KeyButton
ListenButton() : affine::KeyButton ("Listen removed") {}
};

/**
The screen: gain reduction as a scrolling history, with input and output
peak bars. Only real snapshots are plotted; stretches without audio stay empty.
*/
class ReductionScreen final : public juce::Component
{
public:
ReductionScreen();

void setTheme (const affine::Theme&);
/** Appends one frame. `reductionDb` is ignored when `hasReduction` is false. */
void push (float reductionDb, bool hasReduction, float inputDb, float outputDb, bool live, bool inputClip,
bool outputClip, bool removed, bool multiband);
void paint (juce::Graphics&) override;

private:
static constexpr int frames = 180; // six seconds at 30 Hz

void paintHistory (juce::Graphics&, juce::Rectangle<float>);
void paintLevel (juce::Graphics&, juce::Rectangle<float>, const juce::String& name, float level, bool clipped,
juce::Colour);

affine::Theme theme;
affine::render::GlowText readoutGlow;
std::array<float, frames> history {};
int head = 0, valid = 0;
float input = -100.0f, output = -100.0f, latest = 0.0f;
bool live = false, current = false, inputClipped = false, outputClipped = false, removedSignal = false, multi = false;
};

class SuppressorEditor final : public juce::AudioProcessorEditor, private juce::Timer
{
public:
Expand All @@ -25,7 +55,7 @@ class SuppressorEditor final : public juce::AudioProcessorEditor, private juce::
void visibilityChanged() override;
int getControlParameterIndex (juce::Component&) override;

static constexpr int width = 800, height = 530;
static constexpr int width = 820, height = 540;

private:
void timerCallback() override;
Expand All @@ -37,15 +67,14 @@ class SuppressorEditor final : public juce::AudioProcessorEditor, private juce::
affine::Knob threshold, strength, release;
ListenButton listen;
juce::AudioProcessorValueTreeState::ButtonAttachment listenAttachment;
affine::NeedleMeter reduction;
affine::LadderMeter inputLadder, outputLadder;
affine::DisplayLabel status, inputPeak, outputPeak, hostSettings;
ReductionScreen screen;
affine::DisplayLabel status, hostSettings;
juce::Label meterSummary;
juce::TooltipWindow tooltips { this, 700 };
suppressor::MeterSnapshot meter;
uint32_t lastSequence = 0;
double lastUpdateMs = 0.0;
bool fresh = false, auditionShown = false;
bool fresh = false;

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SuppressorEditor)
};
35 changes: 35 additions & 0 deletions Tests/test_ui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,39 @@ void feed (juce::AudioProcessor& p, int blocks = 120, bool silence = false, bool
if (bypass) p.processBlockBypassed (buffer, midi); else p.processBlock (buffer, midi);
}
}
bool capturing() { return juce::SystemStats::getEnvironmentVariable ("SUPPRESSOR_UI_CAPTURE_DIR", {}).isNotEmpty(); }
// Plucked notes over a hiss floor, processed at real-time pace so the editor's
// reduction history fills with readings the processor actually produced.
void playPlucks (juce::AudioProcessor& p)
{
struct Pluck { double at, level; };
const Pluck notes[] { { 0.0, 0.30 }, { 0.9, 0.18 }, { 1.5, 0.34 }, { 2.9, 0.22 }, { 3.4, 0.12 }, { 4.6, 0.32 }, { 5.3, 0.2 } };
constexpr double length = 6.2, fundamental = 196.0;
juce::AudioBuffer<float> buffer (2, 256);
juce::MidiBuffer midi;
juce::Random hiss (7);
const auto start = juce::Time::getMillisecondCounterHiRes();
const auto blocks = static_cast<int> (length * 48000.0 / 256.0);
for (int b = 0; b < blocks; ++b)
{
for (int n = 0; n < 256; ++n)
{
const auto t = (b * 256 + n) / 48000.0;
auto sample = 0.002 * (hiss.nextDouble() * 2.0 - 1.0);
for (const auto& note : notes)
if (t >= note.at)
for (int k = 1; k <= 30; ++k)
sample += note.level / k * std::exp (-(t - note.at) * (1.5 + 0.35 * k))
* std::sin (juce::MathConstants<double>::twoPi * fundamental * k * (t - note.at));
buffer.setSample (0, n, static_cast<float> (sample));
buffer.setSample (1, n, static_cast<float> (sample));
}
p.processBlock (buffer, midi);
const auto due = start + (b + 1) * 256.0 / 48.0;
while (juce::Time::getMillisecondCounterHiRes() < due)
pump (2);
}
}
juce::MouseEvent event (juce::Component& c, juce::Point<float> pos = { 60, 60 }, int modifiers = juce::ModifierKeys::leftButtonModifier)
{
const auto now = juce::Time::getCurrentTime();
Expand Down Expand Up @@ -322,6 +355,8 @@ TEST_CASE ("live meter states, stale data, legacy modes and monitor labels are e
feed (p);
expectStatus (*status, "Suppressing");
CHECK (summary->getDescription().contains ("High-band reduction"));
if (capturing())
playPlucks (p);
capture (*editor, "suppressor-ui");
for (float scale : { 1.0f, 1.25f, 1.5f, 1.75f, 2.0f })
{
Expand Down
59 changes: 31 additions & 28 deletions docs/UI_DESIGN.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Suppressor interface

The editor implements the shared [Affine design language](../libs/affine-ui/DESIGN_LANGUAGE.md)
(v1.0) through the vendored `affine_ui` JUCE module. This document records only the
Suppressor-specific decisions.
(v2.0) in its **Stealth** finish through the vendored `affine_ui` JUCE module. This
document records only the Suppressor-specific decisions.

<p align="center">
<img src="images/suppressor-ui.png" alt="Suppressor suppressing: live input and output ladders, backlit high-band reduction meter near 40 dB, and the three suppression knobs" width="800">
<img src="images/suppressor-ui.png" alt="Suppressor in the Stealth finish: a matte black panel with a teal screen plotting six seconds of high-band reduction between plucked notes, input and output bar meters, three knobs with white LED rings and a Listen soft key" width="800">
</p>

## Product
Expand All @@ -14,19 +14,18 @@ Suppressor-specific decisions.
high-gain amplification.
- **Primary task:** remove high-band noise between notes without losing body, sustain or
pick attack, and check what was removed when in doubt.
- **Finish:** graphite anodising, glacier-cyan emission, an incandescent meter card.
- **Editor:** fixed 800 × 530 logical pixels; renders natively at 100–200% and on Retina.
- **Finish:** modern matte hardware: black powder coat, dark knobs with white pointers
and white LED rings, a rubber soft key, and one colour screen. Teal appears only on the
screen and the *Suppressing* LED.
- **Editor:** fixed 820 × 540 logical pixels; renders natively at 100–200% and on Retina.

## Layout

The signal runs left to right across the meter bridge, and the controls sit below it.

| Zone | Contents |
|---|---|
| Header | Wordmark; processing-status display with lamp; *Host settings active* lamp underneath when non-default legacy settings exist |
| Meter bridge | **IN** peak ladder → backlit **reduction** meter → **OUT** peak ladder (relabelled **REMOVED** while auditioning), each ladder with a clip cell and a peak readout |
| Suppression frame | Threshold, Strength, Release: medium knobs with calibrated scales and glass readouts |
| Monitor frame | *Listen removed* latching key with an amber LED |
| Header | Wide-tracked wordmark and descriptor; the processing status with its LED; *Host settings active* underneath when non-default legacy settings exist |
| Screen | Left: **high-band reduction** over the last six seconds, with the current value above the plot. Right: **INPUT** and **OUTPUT** peak bars (the output relabelled **REMOVED** while auditioning), each with a readout and a clip cell |
| Controls | Threshold, Strength, Release: medium knobs with LED rings and glass readouts; after a hairline, *Listen removed* as a soft key with a headphones glyph |

## Telemetry contract

Expand All @@ -36,36 +35,38 @@ Listen and band-mode settings, so an old block is never relabelled as the new mo

| Display | Source | Presentation |
|---|---|---|
| IN / OUT ladders | Peak across channels, 300 ms decay | −60 to 0 dBFS in 3 dB segments: cyan, amber from −12 dBFS, red from −3 dBFS |
| Clip cells | Processor clip flags, held for one second | Separate red cell above each column |
| Reduction plot | Deepest high-band gain reduction, 120 ms decay, sampled 30 times a second while the editor is open | 0–60 dB on a square-root scale, 0 dB at the top, newest at the right; the plot breaks wherever reduction was unavailable (no audio, no input, learning) and reads *NO AUDIO* once six seconds hold no readings |
| Reduction readout | The latest reading | dB to 0.1, `60+` beyond the scale, `--` when unavailable |
| INPUT / OUTPUT bars | Peak across channels, 300 ms decay | −60 to 0 dBFS in 1.5 dB segments: teal, pale from −12 dBFS, amber from −3 dBFS |
| Clip cells | Processor clip flags, held for one second | Separate red cell at the end of each bar |
| Peak readouts | Same peaks | dBFS to 0.1 dB, `-inf` for silence, `--` when not live |
| Reduction meter | Deepest high-band gain reduction, 120 ms decay | 0–60 dB on a square-root scale so small reductions stay readable; lamp dims and needle rests when reduction is unavailable (no audio, no input, learning) |
| Status | Snapshot flags | *No audio*, *No input*, *Passing signal*, *Suppressing*, *Listening to difference*, *Bypassed*, *Learning bands* |

The needle's spring-damper ballistics (about 300 ms to settle) are presentation only.
The plot shows stored readings only: nothing is interpolated across gaps or extrapolated
past the newest reading.

## States

| State | Presentation |
|---|---|
| Suppressing | Cyan status lamp, needle on the reduction, ladders live |
| Listen removed | Amber key LED and status, **REMOVED** legend and amber readout on the output column |
| Multiband topology | Meter caption reads **MAX BAND REDUCTION**; Threshold and Strength are disabled (veiled) because that mode uses its own splits and learned thresholds |
| Learning bands | Amber status; reduction marked unavailable, so the meter lamp dims |
| Bypassed | Needle at rest with the lamp lit (no reduction is applied) |
| Stopped audio | Lamp dims, needle rests, ladders dark, readouts `--` |
| Suppressing | Teal status LED, the plot follows the reduction, bars live |
| Listen removed | Amber LED strip on the key and amber status LED; the output bar reads **REMOVED** in amber |
| Multiband topology | The plot title reads **MAX BAND REDUCTION**; Threshold and Strength are disabled (their rings go dark) because that mode uses its own splits and learned thresholds |
| Learning bands | Amber status LED; reduction is unavailable, so the plot breaks |
| Bypassed | Red status LED; the plot runs along 0 dB (no reduction is applied) |
| Stopped audio | Status LED off, bars dark, readouts `--`, the plot breaks |

<p align="center">
<img src="images/suppressor-delta-audition.png" alt="Listening to the removed signal: amber status, lit Listen key and REMOVED output column" width="400">
<img src="images/suppressor-learning.png" alt="Learning bands in multiband mode: amber status, dimmed meter, disabled Threshold and Strength, host settings lamp" width="400">
<img src="images/suppressor-delta-audition.png" alt="Listening to the removed signal: amber status LED and key strip, output bar relabelled REMOVED in amber" width="400">
<img src="images/suppressor-learning.png" alt="Learning bands in multiband mode: amber status LED, plot titled MAX BAND REDUCTION, disabled Threshold and Strength, host settings line" width="400">
</p>

## Parameter contract

Unchanged: all 20 parameters keep their IDs, order, version hints, ranges, defaults,
tapers, text conversion and automation. The editor exposes Threshold, Strength, Release
and Delta Audition; the remaining engine settings stay available in the host's parameter
view and are summarised by the *Host settings active* lamp's tooltip.
view and are summarised by the *Host settings active* tooltip.

## Interaction

Expand All @@ -86,10 +87,10 @@ view and are summarised by the *Host settings active* lamp's tooltip.

- Every control exposes its parameter name, formatted value with unit, and a description.
- The *Signal meters* label carries a live summary (input and output dBFS, reduction or
why it is unavailable, clip warnings); the per-column readouts are hidden from assistive
why it is unavailable, clip warnings); the screen itself is hidden from assistive
technology so values are not announced twice.
- State is never colour-only: every lamp has a text legend and every mode change has a
printed or displayed label.
- State is never colour-only: the status is always spelled out next to its LED, and
every mode change has a printed or displayed label.
- Printed text meets 4.5:1 on the plate, including its darker lower edge; emitted
colours meet 4.5:1 on display glass. Both are unit-tested.

Expand All @@ -102,4 +103,6 @@ view and are summarised by the *Host settings active* lamp's tooltip.
- The DSP suite is unchanged and passes.
- pluginval 1.0.4 at strictness 10 reports `SUCCESS` for the VST3.
- With `SUPPRESSOR_UI_CAPTURE_DIR` set, the UI tests write a PNG of every state and scale;
CI uploads them for macOS, Windows and Linux.
before the main capture they play six seconds of plucked notes at real-time pace, so
the plot shows readings the processor produced. CI uploads the captures for macOS,
Windows and Linux.
Binary file modified docs/images/suppressor-delta-audition.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/suppressor-exact-entry.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/suppressor-learning.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/suppressor-ui.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading