Skip to content
Closed
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
7 changes: 4 additions & 3 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 Broadcast finish tracking an A2 at 110 Hz: Nixie note tubes, a confidence VU meter with the lock jewel lit, a Nixie counter showing a 220 Hz carrier with its sine on a green scope, and the Tracking, Carrier and Output controls on a hammertone rack panel" width="900">
</p>

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

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

- **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.
- **Input pitch** (Nixie tubes): the detected note, with its frequency in the amber window underneath. The window reads *LISTENING* while the tracker searches, *NO AUDIO* when the host is not processing, and *MANUAL CARRIER* in Manual mode.
- **Confidence** meter: how certain the detector is. Its green zone starts at the **Sensitivity** threshold a detection must reach, and the **LOCK** jewel lights only while the carrier is actually following the input.
- **Carrier frequency** (Nixie counter): the frequency the carrier oscillator is running at, published by the processor rather than estimated in the UI, with the selected waveform on the scope. 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.
- **Waveform** keys select the carrier shape.

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