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
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
A pitch-tracking ring modulator audio plugin (VST3/AU/Standalone) built with JUCE 8.

<p align="center">
<img src="img/hdn-ring-modulator-ui.png" alt="HDN Ring Modulator tracking an A2 at 110 Hz: cyan input-pitch display locked, Nixie counter showing a 220 Hz carrier at a 2x rate multiplier, and the Tracking, Carrier and Output controls" width="900">
<img src="img/hdn-ring-modulator-ui.png" alt="HDN Ring Modulator in the Stealth finish tracking an A2 at 110 Hz: an amber screen with the input note, tuning and signal bars, four cycles of the sine carrier across two input periods, and a 220.0 Hz carrier readout at a 2.00x ratio, above knobs with white LED rings and soft keys" width="900">
</p>

## What Is This?
Expand All @@ -20,11 +20,12 @@ Four oscillator waveforms are available (sine, triangle, square, saw), each prod

## Interface

The panel reads left to right in signal order: what the tracker hears, the carrier it drives, and the controls for each stage.
The screen reads left to right in signal order: what the tracker hears, the carrier it drives, and its frequency. The controls for each stage sit below it.

- **Input pitch** (cyan display): the detected note and frequency. **LOCK** lights only while the carrier is actually following the input. The confidence bar shows how certain the detector is, and the marker under it is the **Sensitivity** threshold a detection must clear. The display reads *LISTENING* while the tracker searches, *NO AUDIO* when the host is not processing, and *OFF* in Manual mode.
- **Carrier frequency** (Nixie counter): the frequency the carrier oscillator is running at, published by the processor rather than estimated in the UI, with a trace of the selected waveform. The tubes go dark while the effect stays dry.
- **Source** keys switch between Pitch Track and Manual. Controls that do not act in the current source keep working but show it: their activity lamp goes out and their readout dims.
- **Input**: the detected note and frequency, a tuning bar showing how far the input is from the nearest note in cents, and the **Signal** bar showing how certain the detector is, with the **Sensitivity** mark a detection must reach. **LOCK** lights only while the carrier is actually following the input. The input reads *LISTENING* while the tracker searches, *NO AUDIO* when the host is not processing, and *OFF* in Manual mode.
- **Wave**: the selected waveform drawn at the frequency the carrier is running at. While tracking, the window spans two periods of the input, so the Rate Multiplier shows as the number of cycles; in Manual mode it spans 10 ms. It stays flat while the effect is dry.
- **Carrier**: the frequency the carrier oscillator is running at, published by the processor rather than estimated in the UI, as a readout and a note, with its ratio to the input pitch. The readouts go dark while the effect stays dry.
- **Source** keys switch between Pitch Track and Manual. Controls that do not act in the current source keep working but show it: their activity lamp goes out and their LED ring and readout dim.
- **Waveform** keys select the carrier shape.

Knobs have calibrated scales. Drag anywhere on a knob (up or right increases, Shift for fine), double-click or press Return to type an exact value, Alt/Option-click or Home to reset, and right-click for the parameter menu. The interface follows the shared [Affine design language](libs/affine-ui/DESIGN_LANGUAGE.md).
Expand Down
Binary file modified img/hdn-ring-modulator-ui.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
178 changes: 104 additions & 74 deletions libs/affine-ui/DESIGN_LANGUAGE.md

Large diffs are not rendered by default.

12 changes: 7 additions & 5 deletions libs/affine-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ target_link_libraries(MyPlugin PRIVATE affine_ui)
```cpp
#include <affine_ui/affine_ui.h>

affine::Theme theme; // shared palette, per-product finish
affine::Theme theme; // the release's finish, in product colours
theme.panel.texture = affine::PanelFinish::Texture::anodised;
theme.panel.base = juce::Colour (0xff2a3037);
theme.palette.accent = juce::Colour (0xff4fd6ff);

Expand All @@ -35,15 +36,16 @@ Requires JUCE 8 and C++17. The module depends on `juce_gui_basics` and

| Path | What |
|---|---|
| `affine_ui/render` | Procedural renderers: knob shader, anodised faceplate, glow and blur |
| `affine_ui/components` | `Knob`, `KeyButton`, `SelectorKeys`, `NeedleMeter`, `LadderMeter`, `NixieDisplay`, `DisplayLabel`, `Faceplate` and silkscreen helpers |
| `affine_ui/render` | Procedural renderers: knob materials, faceplate textures (anodised, hammertone, powder, glass), panel hardware, glow and blur |
| `affine_ui/components` | `Knob`, `KeyButton`, `SelectorKeys`, `NeedleMeter`, `LadderMeter`, `NixieDisplay`, `TuningDial`, `IlluminatedLegends`, `DisplayLabel`, `Faceplate` and silkscreen helpers |
| `affine_ui/theme` | Palette, metrics, fonts, `LookAndFeel` |
| `docs/finishes.png` | Both products in each of the four finishes |
| `fonts` | Barlow Condensed, Michroma, Nixie One, DSEG14 Classic, Share Tech Mono (all SIL OFL 1.1; licences in `fonts/licenses`) |
| `DESIGN_LANGUAGE.md` | Principles, tokens, anatomy, component contracts, states |
| `DESIGN_LANGUAGE.md` | Principles, finishes, tokens, anatomy, component contracts, states |

## Rendering notes

Knobs, the faceplate and meter faces are shaded per pixel at the display's physical
Knobs, faceplates and meter faces are shaded per pixel at the display's physical
scale and cached, so they stay sharp at 100–200% and on Retina displays. Only the parts
that move are redrawn: the knob's knurled grip band is re-shaded (about a millisecond at
2x) when its value changes, and meters repaint their own bounds. Nothing here runs on the
Expand Down
1 change: 1 addition & 0 deletions libs/affine-ui/affine_ui/affine_ui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@
#include "components/affine_Keys.cpp"
#include "components/affine_Nixie.cpp"
#include "components/affine_Faceplate.cpp"
#include "components/affine_Legends.cpp"
#include "theme/affine_LookAndFeel.cpp"
4 changes: 3 additions & 1 deletion libs/affine-ui/affine_ui/affine_ui.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

ID: affine_ui
vendor: affine
version: 1.0.0
version: 2.0.0
name: Affine instrument UI
description: Shared rendered-instrument design language for Affine plug-ins
license: GPL-3.0-or-later
Expand All @@ -26,6 +26,7 @@
#include <cstdlib>
#include <functional>
#include <limits>
#include <map>
#include <memory>

#include "render/affine_Shading.h"
Expand All @@ -41,4 +42,5 @@
#include "components/affine_Keys.h"
#include "components/affine_Nixie.h"
#include "components/affine_Faceplate.h"
#include "components/affine_Legends.h"
#include "theme/affine_LookAndFeel.h"
26 changes: 25 additions & 1 deletion libs/affine-ui/affine_ui/components/affine_Displays.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ void lamp (juce::Graphics& g, juce::Point<float> centre, float diameter, juce::C
g.fillEllipse (juce::Rectangle<float> (r * 0.62f, r * 0.42f).withCentre (centre.translated (-r * 0.28f, -r * 0.40f)));
}

void indicator (juce::Graphics& g, juce::Point<float> centre, float diameter, juce::Colour colour, float level, const Palette& palette)
{
if (palette.lamp == Palette::Lamp::jewel)
jewel (g, centre, diameter, colour, level);
else
lamp (g, centre, diameter, colour, level);
}

void glass (juce::Graphics& g, juce::Rectangle<float> area, const Palette& palette, float corner)
{
juce::ColourGradient body (palette.glassTint, area.getX(), area.getY(),
Expand Down Expand Up @@ -84,6 +92,12 @@ void DisplayLabel::setShowsLamp (bool shouldShow)
repaint();
}

void DisplayLabel::setBacklit (bool shouldBeBacklit)
{
backlit = shouldBeBacklit;
repaint();
}

void DisplayLabel::setGlassVisible (bool shouldShow)
{
glassVisible = shouldShow;
Expand All @@ -98,6 +112,16 @@ void DisplayLabel::setDisplayFont (const juce::Font& f)

void DisplayLabel::paint (juce::Graphics& g)
{
if (backlit)
{
const auto window = getLocalBounds().toFloat().reduced (2.0f);
render::litWindow (g, window, emission, lampLevel);
g.setColour (theme.palette.readoutInk.withMultipliedAlpha (0.35f + 0.55f * lampLevel));
g.setFont (font);
g.drawText (getText().toUpperCase(), window.reduced (8.0f, 0.0f), getJustificationType(), false);
return;
}

auto area = getLocalBounds().toFloat();
if (glassVisible)
render::glass (g, area, theme.palette);
Expand All @@ -119,7 +143,7 @@ void DisplayLabel::paint (juce::Graphics& g)
const auto textWidth = glyphs.getBoundingBox (0, -1, true).getWidth();
lampSlot = lampSlot.withX (area.getRight() - textWidth - 19.0f);
}
render::lamp (g, lampSlot.getCentre(), 6.0f, lampColour.isTransparent() ? emission : lampColour, lampLevel);
render::indicator (g, lampSlot.getCentre(), 6.0f, lampColour.isTransparent() ? emission : lampColour, lampLevel, theme.palette);
area.removeFromLeft (7.0f);
}

Expand Down
7 changes: 6 additions & 1 deletion libs/affine-ui/affine_ui/components/affine_Displays.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ void lamp (juce::Graphics&, juce::Point<float> centre, float diameter, juce::Col

/** A glass display window cut into the faceplate. */
void glass (juce::Graphics&, juce::Rectangle<float> area, const Palette&, float corner = metrics::displayCorner);

/** An indicator in the palette's lamp style: an LED lens or a jewel. */
void indicator (juce::Graphics&, juce::Point<float> centre, float diameter, juce::Colour colour, float level, const Palette&);
} // namespace render

/**
Expand All @@ -26,6 +29,8 @@ class DisplayLabel : public juce::Label
void setLampColour (juce::Colour);
void setShowsLamp (bool);
void setGlassVisible (bool);
/** Dark lettering on a window lit in the emission colour, as on an illuminated legend. */
void setBacklit (bool);
void setDisplayFont (const juce::Font&);

void paint (juce::Graphics&) override;
Expand All @@ -35,7 +40,7 @@ class DisplayLabel : public juce::Label
juce::Colour emission, lampColour;
juce::Font font;
float lampLevel = 0.0f;
bool showsLamp = true, glassVisible = true;
bool showsLamp = true, glassVisible = true, backlit = false;
render::GlowText glow;
};
} // namespace affine
10 changes: 9 additions & 1 deletion libs/affine-ui/affine_ui/components/affine_Faceplate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ void Faceplate::paint (juce::Graphics& g, juce::Rectangle<int> bounds, const The
layer.setColour (juce::Colours::black.withAlpha (0.45f));
layer.fillRect (edge.withTop (edge.getBottom() - 1.0f));

silkscreen::screws (layer, edge);
if (screws)
silkscreen::screws (layer, edge);
if (print != nullptr)
print (layer);
}
Expand Down Expand Up @@ -121,5 +122,12 @@ void screws (juce::Graphics& g, juce::Rectangle<float> bounds)
for (int i = 0; i < 4; ++i)
render::screw (g, corners[i], metrics::screwDiameter, turns[i]);
}

void litLegend (juce::Graphics& g, const juce::String& text, juce::Rectangle<float> area, juce::Colour colour,
const juce::Font& font, juce::Justification justification, float glow)
{
render::GlowText lit;
lit.draw (g, text, font, area, justification, colour, 2.4f, glow);
}
} // namespace silkscreen
} // namespace affine
8 changes: 8 additions & 0 deletions libs/affine-ui/affine_ui/components/affine_Faceplate.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ class Faceplate
/** Forces the next paint to rebuild, e.g. after printed text changed. */
void invalidate() noexcept { cache = {}; }

/** Corner screws; off for fronts that are held some other way (rack ears, glass). */
void setShowsScrews (bool shouldShow) { screws = shouldShow; cache = {}; }

private:
bool screws = true;
juce::Image cache;
juce::Rectangle<int> cachedBounds;
float cachedScale = 0.0f;
Expand All @@ -45,5 +49,9 @@ void legend (juce::Graphics&, const juce::String&, juce::Rectangle<float>, const

/** Four corner screws. */
void screws (juce::Graphics&, juce::Rectangle<float> bounds);

/** A legend printed on the back of a glass front and lit from behind: it glows softly. */
void litLegend (juce::Graphics&, const juce::String&, juce::Rectangle<float>, juce::Colour, const juce::Font&,
juce::Justification = juce::Justification::centredLeft, float glow = 0.6f);
} // namespace silkscreen
} // namespace affine
Loading
Loading