mirror of
https://codeberg.org/armin/beamgrid.git
synced 2026-09-01 20:30:47 +02:00
- BOOST: global display level multiplier (0.75 - 2.5, 1.0 default) - 3D: pseudo-3D bevel/shading on bars, LEDs, peaks and curve (0 = flat) - G-ANG: 3D gradient angle (-180 - 180, 0 = top-lit) - Apply CURVING rounded corners to bars and peaks in BAR mode
349 lines
16 KiB
C++
349 lines
16 KiB
C++
#include "PluginEditor.h"
|
|
#include "GitVersion.h"
|
|
|
|
BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcessor& p)
|
|
: AudioProcessorEditor (&p),
|
|
processorRef (p),
|
|
analyserComponent (p.getAnalyser(), p.getParametersState(), lookAndFeel)
|
|
{
|
|
setLookAndFeel (&lookAndFeel);
|
|
setSize (980, 480);
|
|
|
|
// Make the editor freely resizable (with a corner resizer). The plugin
|
|
// reports IPlugView::canResize() == true to the host, which is what FL
|
|
// Studio (and other hosts) use to enable their native maximize button.
|
|
setResizable (true, true);
|
|
setResizeLimits (800, 260, 4000, 4000);
|
|
|
|
analyserComponent.setLookAndFeel (&lookAndFeel);
|
|
addAndMakeVisible (analyserComponent);
|
|
|
|
auto setupKnob = [&] (juce::Slider& s, const juce::String& name)
|
|
{
|
|
s.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
|
|
s.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 80, 18);
|
|
s.setColour (juce::Slider::textBoxTextColourId, juce::Colours::lightgrey);
|
|
s.setTextValueSuffix (" " + name);
|
|
s.setLookAndFeel (&lookAndFeel);
|
|
addAndMakeVisible (s);
|
|
};
|
|
|
|
setupKnob (graceSlider, "ms");
|
|
setupKnob (falloffSlider, "");
|
|
setupKnob (barfalloffSlider, "");
|
|
setupKnob (barsSlider, "");
|
|
setupKnob (hueSlider, "");
|
|
setupKnob (modeSlider, "");
|
|
setupKnob (ledsSlider, "");
|
|
setupKnob (smoothSlider, "");
|
|
setupKnob (gridSlider, "");
|
|
setupKnob (gridzoomSlider, "");
|
|
setupKnob (gridfadeSlider, "");
|
|
setupKnob (curvingSlider, "");
|
|
setupKnob (tiltSlider, "");
|
|
setupKnob (boostSlider, "");
|
|
setupKnob (depth3dSlider, "");
|
|
setupKnob (gradangleSlider, "");
|
|
barsSlider.setNumDecimalPlacesToDisplay (0);
|
|
modeSlider.setNumDecimalPlacesToDisplay (0);
|
|
ledsSlider.setNumDecimalPlacesToDisplay (0);
|
|
|
|
graceLabel.setText ("PEAK GRACE", juce::dontSendNotification);
|
|
graceLabel.setJustificationType (juce::Justification::centred);
|
|
graceLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
|
|
addAndMakeVisible (graceLabel);
|
|
|
|
falloffLabel.setText ("PEAK FALLOFF", juce::dontSendNotification);
|
|
falloffLabel.setJustificationType (juce::Justification::centred);
|
|
falloffLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
|
|
addAndMakeVisible (falloffLabel);
|
|
|
|
barfalloffLabel.setText ("BAR FALLOFF", juce::dontSendNotification);
|
|
barfalloffLabel.setJustificationType (juce::Justification::centred);
|
|
barfalloffLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
|
|
addAndMakeVisible (barfalloffLabel);
|
|
|
|
barsLabel.setText ("BARS", juce::dontSendNotification);
|
|
barsLabel.setJustificationType (juce::Justification::centred);
|
|
barsLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
|
|
addAndMakeVisible (barsLabel);
|
|
|
|
hueLabel.setText ("HUE", juce::dontSendNotification);
|
|
hueLabel.setJustificationType (juce::Justification::centred);
|
|
hueLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
|
|
addAndMakeVisible (hueLabel);
|
|
|
|
modeLabel.setText ("MODE", juce::dontSendNotification);
|
|
modeLabel.setJustificationType (juce::Justification::centred);
|
|
modeLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
|
|
addAndMakeVisible (modeLabel);
|
|
|
|
ledsLabel.setText ("LEDS", juce::dontSendNotification);
|
|
ledsLabel.setJustificationType (juce::Justification::centred);
|
|
ledsLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
|
|
addAndMakeVisible (ledsLabel);
|
|
|
|
smoothLabel.setText ("SMOOTH", juce::dontSendNotification);
|
|
smoothLabel.setJustificationType (juce::Justification::centred);
|
|
smoothLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
|
|
addAndMakeVisible (smoothLabel);
|
|
|
|
gridLabel.setText ("GRID", juce::dontSendNotification);
|
|
gridLabel.setJustificationType (juce::Justification::centred);
|
|
gridLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
|
|
addAndMakeVisible (gridLabel);
|
|
|
|
gridzoomLabel.setText ("GRID ZOOM", juce::dontSendNotification);
|
|
gridzoomLabel.setJustificationType (juce::Justification::centred);
|
|
gridzoomLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
|
|
addAndMakeVisible (gridzoomLabel);
|
|
|
|
gridfadeLabel.setText ("GRID FADE", juce::dontSendNotification);
|
|
gridfadeLabel.setJustificationType (juce::Justification::centred);
|
|
gridfadeLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
|
|
addAndMakeVisible (gridfadeLabel);
|
|
|
|
curvingLabel.setText ("CURVING", juce::dontSendNotification);
|
|
curvingLabel.setJustificationType (juce::Justification::centred);
|
|
curvingLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
|
|
addAndMakeVisible (curvingLabel);
|
|
|
|
tiltLabel.setText ("TILT", juce::dontSendNotification);
|
|
tiltLabel.setJustificationType (juce::Justification::centred);
|
|
tiltLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
|
|
addAndMakeVisible (tiltLabel);
|
|
|
|
boostLabel.setText ("BOOST", juce::dontSendNotification);
|
|
boostLabel.setJustificationType (juce::Justification::centred);
|
|
boostLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
|
|
addAndMakeVisible (boostLabel);
|
|
|
|
depth3dLabel.setText ("3D", juce::dontSendNotification);
|
|
depth3dLabel.setJustificationType (juce::Justification::centred);
|
|
depth3dLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
|
|
addAndMakeVisible (depth3dLabel);
|
|
|
|
gradangleLabel.setText ("G-ANG", juce::dontSendNotification);
|
|
gradangleLabel.setJustificationType (juce::Justification::centred);
|
|
gradangleLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
|
|
addAndMakeVisible (gradangleLabel);
|
|
|
|
scaleBox.addItemList (juce::StringArray ("75%", "100%", "150%", "200%", "250%", "300%"), 1);
|
|
scaleBox.setSelectedItemIndex (1); // 100%
|
|
scaleBox.setColour (juce::ComboBox::backgroundColourId, juce::Colours::black.brighter (0.15f));
|
|
scaleBox.setColour (juce::ComboBox::textColourId, juce::Colours::lightgrey);
|
|
scaleBox.setColour (juce::ComboBox::arrowColourId, juce::Colours::lightgrey);
|
|
scaleBox.onChange = [this]
|
|
{
|
|
const double pct = scaleBox.getText().retainCharacters ("0123456789.").getDoubleValue();
|
|
const double scale = juce::jlimit (0.5, 3.0, pct / 100.0);
|
|
setScaleFactor (scale);
|
|
processorRef.getParametersState().state.setProperty ("uiScale", scale, nullptr);
|
|
};
|
|
addAndMakeVisible (scaleBox);
|
|
|
|
titleLabel.setText ("BEAMGRID", juce::dontSendNotification);
|
|
titleLabel.setJustificationType (juce::Justification::centredRight);
|
|
titleLabel.setFont (juce::FontOptions (20.0f, juce::Font::bold));
|
|
titleLabel.setColour (juce::Label::textColourId, lookAndFeel.getTeal());
|
|
addAndMakeVisible (titleLabel);
|
|
|
|
statusLabel.setFont (juce::FontOptions (11.0f));
|
|
statusLabel.setColour (juce::Label::textColourId, juce::Colours::grey);
|
|
statusLabel.setJustificationType (juce::Justification::centredRight);
|
|
|
|
juce::String status;
|
|
status << "git " << juce::String (BEAMGRID_GIT_COMMIT).substring (0, 7);
|
|
status << " [" << (BEAMGRID_GIT_DIRTY ? "dirty" : "clean") << "]";
|
|
status << " build " << BEAMGRID_BUILD_DATE;
|
|
statusLabel.setText (status, juce::dontSendNotification);
|
|
addAndMakeVisible (statusLabel);
|
|
|
|
graceAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
|
processorRef.getParametersState(), "grace", graceSlider);
|
|
falloffAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
|
processorRef.getParametersState(), "falloff", falloffSlider);
|
|
barfalloffAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
|
processorRef.getParametersState(), "barfalloff", barfalloffSlider);
|
|
barsAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
|
processorRef.getParametersState(), "bars", barsSlider);
|
|
hueAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
|
processorRef.getParametersState(), "hue", hueSlider);
|
|
modeAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
|
processorRef.getParametersState(), "mode", modeSlider);
|
|
ledsAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
|
processorRef.getParametersState(), "leds", ledsSlider);
|
|
smoothAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
|
processorRef.getParametersState(), "smooth", smoothSlider);
|
|
gridAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
|
processorRef.getParametersState(), "grid", gridSlider);
|
|
gridzoomAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
|
processorRef.getParametersState(), "gridzoom", gridzoomSlider);
|
|
gridfadeAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
|
processorRef.getParametersState(), "gridfade", gridfadeSlider);
|
|
curvingAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
|
processorRef.getParametersState(), "curving", curvingSlider);
|
|
tiltAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
|
processorRef.getParametersState(), "tilt", tiltSlider);
|
|
boostAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
|
processorRef.getParametersState(), "boost", boostSlider);
|
|
depth3dAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
|
processorRef.getParametersState(), "depth3d", depth3dSlider);
|
|
gradangleAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
|
processorRef.getParametersState(), "gradangle", gradangleSlider);
|
|
|
|
// Set these AFTER the attachments: SliderAttachment installs its own
|
|
// textFromValueFunction, so assigning afterwards makes ours take effect.
|
|
barsSlider.textFromValueFunction = [] (double v)
|
|
{
|
|
return juce::String (juce::roundToInt (v));
|
|
};
|
|
|
|
modeSlider.textFromValueFunction = [] (double v)
|
|
{
|
|
const int m = juce::roundToInt (v);
|
|
return m == 2 ? juce::String ("SPECTRUM")
|
|
: m == 3 ? juce::String ("LED")
|
|
: juce::String ("BARS");
|
|
};
|
|
|
|
ledsSlider.textFromValueFunction = [] (double v)
|
|
{
|
|
return juce::String (juce::roundToInt (v));
|
|
};
|
|
|
|
tiltSlider.textFromValueFunction = [] (double v)
|
|
{
|
|
return (v >= 0.0 ? juce::String ("+") : juce::String (""))
|
|
+ juce::String (v, 2);
|
|
};
|
|
|
|
// Force an immediate repaint of the text boxes now that our formatters
|
|
// are installed (otherwise they keep the attachment's initial text).
|
|
barsSlider.updateText();
|
|
modeSlider.updateText();
|
|
ledsSlider.updateText();
|
|
tiltSlider.updateText();
|
|
|
|
processorRef.getParametersState().addParameterListener ("hue", this);
|
|
|
|
// Restore the saved UI size + scale from the plugin state (the host saves
|
|
// this state with the project), so the window reopens the same way.
|
|
auto& uiState = processorRef.getParametersState().state;
|
|
const double storedScale = uiState.getProperty ("uiScale", 1.0);
|
|
|
|
const double scales[] = { 0.75, 1.0, 1.5, 2.0, 2.5, 3.0 };
|
|
int scaleIdx = 1;
|
|
for (int i = 0; i < 6; ++i)
|
|
if (std::abs (scales[i] - storedScale) < 0.001)
|
|
scaleIdx = i;
|
|
scaleBox.setSelectedItemIndex (scaleIdx); // triggers onChange -> setScaleFactor
|
|
|
|
const int storedW = juce::jmax (800, static_cast<int> (uiState.getProperty ("uiWidth", getWidth())));
|
|
const int storedH = uiState.getProperty ("uiHeight", getHeight());
|
|
setSize (storedW, storedH);
|
|
}
|
|
|
|
BeamgridAudioProcessorEditor::~BeamgridAudioProcessorEditor()
|
|
{
|
|
processorRef.getParametersState().removeParameterListener ("hue", this);
|
|
setLookAndFeel (nullptr);
|
|
}
|
|
|
|
void BeamgridAudioProcessorEditor::parameterChanged (const juce::String& parameterID, float newValue)
|
|
{
|
|
if (parameterID == "hue")
|
|
{
|
|
// Update the shared LookAndFeel immediately and repaint the whole
|
|
// editor so every rotary knob's arc (and the title) recolours.
|
|
lookAndFeel.setHueTurns (newValue - 0.5f);
|
|
repaint();
|
|
}
|
|
}
|
|
|
|
void BeamgridAudioProcessorEditor::paint (juce::Graphics& g)
|
|
{
|
|
g.fillAll (juce::Colours::black);
|
|
|
|
// Keep the title colour in sync with the HUE knob.
|
|
titleLabel.setColour (juce::Label::textColourId, lookAndFeel.getTeal());
|
|
}
|
|
|
|
void BeamgridAudioProcessorEditor::resized()
|
|
{
|
|
const int panelH = 114;
|
|
const int y = getHeight() - panelH + 14;
|
|
|
|
// 32px blank bar across the very top of the plugin (the dropdown sits
|
|
// 16px below the top edge so it isn't glued to it). 30px blank strip on
|
|
// the right so the spectrum doesn't touch the right border.
|
|
const int topBarH = 32;
|
|
const int rightPad = 30;
|
|
analyserComponent.setBounds (0, topBarH, getWidth() - rightPad, getHeight() - panelH - topBarH);
|
|
|
|
const int margin = 24;
|
|
const int gap = 14;
|
|
const int count = 16;
|
|
const int titleW = 180;
|
|
|
|
// Available width for the knob row (leave room on the right for the title).
|
|
const int avail = getWidth() - margin * 2 - titleW;
|
|
|
|
// Shrink the knobs only if necessary so all `count` of them fit within the
|
|
// available width down to the 800px minimum; otherwise keep them at 64.
|
|
const int knob = juce::jlimit (40, 64, (avail - (count - 1) * 4) / count);
|
|
|
|
int spacing = knob + gap;
|
|
const int minSpacing = knob + 4;
|
|
if ((count - 1) * spacing + knob > avail)
|
|
spacing = juce::jmax (minSpacing, (avail - knob) / (count - 1));
|
|
|
|
struct KnobRow { juce::Slider& s; juce::Label& l; };
|
|
const KnobRow knobs[] = {
|
|
{ graceSlider, graceLabel },
|
|
{ falloffSlider, falloffLabel },
|
|
{ barfalloffSlider, barfalloffLabel },
|
|
{ barsSlider, barsLabel },
|
|
{ hueSlider, hueLabel },
|
|
{ modeSlider, modeLabel },
|
|
{ ledsSlider, ledsLabel },
|
|
{ smoothSlider, smoothLabel },
|
|
{ gridSlider, gridLabel },
|
|
{ gridzoomSlider, gridzoomLabel },
|
|
{ gridfadeSlider, gridfadeLabel },
|
|
{ curvingSlider, curvingLabel },
|
|
{ tiltSlider, tiltLabel },
|
|
{ boostSlider, boostLabel },
|
|
{ depth3dSlider, depth3dLabel },
|
|
{ gradangleSlider, gradangleLabel },
|
|
};
|
|
|
|
const juce::Font labelFont (juce::FontOptions (11.0f));
|
|
|
|
// Knobs are drawn 16px higher than the panel baseline; the title and
|
|
// status bar below stay anchored to the original baseline.
|
|
const int knobY = y - 16;
|
|
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
const int x = margin + i * spacing;
|
|
knobs[i].s.setBounds (x, knobY, knob, knob);
|
|
|
|
// Let the label span the full spacing (wider than the knob) so longer
|
|
// names like "BAR FALLOFF" aren't clipped, centred under the knob.
|
|
const int labelW = spacing;
|
|
const int labelX = x + (knob - labelW) / 2;
|
|
knobs[i].l.setFont (labelFont);
|
|
knobs[i].l.setBounds (labelX, knobY + knob + 2, labelW, 18);
|
|
}
|
|
|
|
titleLabel.setBounds (getWidth() - titleW - margin, y - 6, titleW, knob + 24);
|
|
statusLabel.setBounds (16, getHeight() - 16, getWidth() - 42, 14);
|
|
|
|
// UI scaling dropdown, top-right (16px below the top edge).
|
|
scaleBox.setBounds (getWidth() - 96 - margin, 16, 96, 16);
|
|
|
|
// Remember the current window size so it can be restored with the project.
|
|
processorRef.getParametersState().state.setProperty ("uiWidth", getWidth(), nullptr);
|
|
processorRef.getParametersState().state.setProperty ("uiHeight", getHeight(), nullptr);
|
|
}
|