gelyk/Source/PluginEditor.cpp

423 lines
17 KiB
C++

#include "PluginEditor.h"
namespace gelyk
{
static juce::Font monoFont(float height)
{
return juce::Font(juce::FontOptions(juce::Font::getDefaultMonospacedFontName(), height,
juce::Font::plain));
}
// ---------------------------------------------------------------------------
// NeonLookAndFeel
// ---------------------------------------------------------------------------
NeonLookAndFeel::NeonLookAndFeel()
{
setColour(juce::Slider::textBoxTextColourId, cyan);
setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentBlack);
setColour(juce::Slider::textBoxBackgroundColourId, panel);
setColour(juce::ComboBox::backgroundColourId, panel);
setColour(juce::ComboBox::outlineColourId, line);
setColour(juce::ComboBox::textColourId, cyan);
setColour(juce::ComboBox::arrowColourId, cyan);
}
void NeonLookAndFeel::drawLinearSlider(juce::Graphics& g, int x, int y, int width, int height,
float sliderPos, float, float,
const juce::Slider::SliderStyle style, juce::Slider& slider)
{
const bool vertical = (style == juce::Slider::LinearVertical);
const float cx = x + width * 0.5f;
// Track
if (vertical)
{
const float tw = 2.0f;
juce::Path track;
track.addRoundedRectangle(cx - tw * 0.5f, (float) y, tw, (float) height, 1.0f);
g.setColour(line);
g.fillPath(track);
// Filled region from centre (0 dB) down to the handle, glowing.
const float midY = y + height * 0.5f;
const float top = juce::jmin(midY, sliderPos);
const float bottom = juce::jmax(midY, sliderPos);
juce::Path fill;
fill.addRoundedRectangle(cx - 3.0f, top, 6.0f, juce::jmax(1.0f, bottom - top), 3.0f);
g.setColour(cyan);
g.fillPath(fill);
// Handle.
g.setColour(slider.isMouseOverOrDragging(true) ? violet : cyan);
g.fillEllipse(cx - 5.0f, sliderPos - 5.0f, 10.0f, 10.0f);
g.setColour(bg);
g.fillEllipse(cx - 2.0f, sliderPos - 2.0f, 4.0f, 4.0f);
}
else
{
const float th = 2.0f;
juce::Path track;
track.addRoundedRectangle((float) x, y + height * 0.5f - th * 0.5f, (float) width, th, 1.0f);
g.setColour(line);
g.fillPath(track);
g.setColour(cyan);
g.drawHorizontalLine((int) (y + height * 0.5f), sliderPos - 5.0f, sliderPos + 5.0f);
}
}
void NeonLookAndFeel::drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height,
float sliderPos, float startAngle, float endAngle, juce::Slider& slider)
{
const float cx = x + width * 0.5f;
const float cy = y + height * 0.5f;
const float radius = juce::jmin(width, height) * 0.5f;
const float toAngle = startAngle + sliderPos * (endAngle - startAngle);
const float lineW = juce::jmax(2.0f, radius * 0.10f);
const float arcRadius = radius - lineW * 0.5f;
// Background disc.
g.setColour(panel);
g.fillEllipse(cx - radius, cy - radius, radius * 2.0f, radius * 2.0f);
g.setColour(line);
g.drawEllipse(cx - radius, cy - radius, radius * 2.0f, radius * 2.0f, 1.0f);
// Filled arc (progress).
juce::Path filledArc;
filledArc.addCentredArc(cx, cy, arcRadius, arcRadius, 0.0f,
startAngle, toAngle, true);
juce::PathStrokeType stroke(lineW, juce::PathStrokeType::curved, juce::PathStrokeType::rounded);
g.setColour(slider.isMouseOverOrDragging(true) ? magenta : cyan);
g.strokePath(filledArc, stroke);
// Pointer line.
juce::Path pointer;
const float pointerLength = radius * 0.68f;
pointer.startNewSubPath(cx, cy);
pointer.lineTo(cx + pointerLength * std::cos(toAngle),
cy + pointerLength * std::sin(toAngle));
g.setColour(slider.isMouseOverOrDragging(true) ? violet : bg);
g.strokePath(pointer, juce::PathStrokeType(juce::jmax(1.5f, radius * 0.07f),
juce::PathStrokeType::curved,
juce::PathStrokeType::rounded));
// Centre hub.
g.setColour(slider.isMouseOverOrDragging(true) ? cyan : line);
g.fillEllipse(cx - 2.0f, cy - 2.0f, 4.0f, 4.0f);
}
void NeonLookAndFeel::drawComboBox(juce::Graphics& g, int width, int height, bool,
int, int, int, int, juce::ComboBox& box)
{
auto corner = 4.0f;
auto bounds = box.getLocalBounds().toFloat();
g.setColour(panel);
g.fillRoundedRectangle(bounds, corner);
g.setColour(cyan);
g.drawRoundedRectangle(bounds, corner, 1.0f);
juce::Rectangle<int> arrowZone(width - 20, 0, 20, height);
juce::Path p;
p.startNewSubPath((float) arrowZone.getX() + 4.0f, (float) arrowZone.getCentreY() - 3.0f);
p.lineTo((float) arrowZone.getCentreX(), (float) arrowZone.getCentreY() + 3.0f);
p.lineTo((float) arrowZone.getX() + 12.0f, (float) arrowZone.getCentreY() - 3.0f);
g.setColour(cyan);
g.strokePath(p, juce::PathStrokeType(1.5f));
}
void NeonLookAndFeel::positionComboBoxText(juce::ComboBox& box, juce::Label& label)
{
label.setBounds(2, 1, box.getWidth() - 22, box.getHeight() - 2);
label.setFont(monoFont(13.0f));
}
// ---------------------------------------------------------------------------
// GelykEQAudioProcessorEditor
// ---------------------------------------------------------------------------
GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor& p)
: AudioProcessorEditor(&p),
processor(p)
{
setLookAndFeel(&lookAndFeel);
setOpaque(true);
titleLabel = std::make_unique<juce::Label>("title", "GELYKEQ");
titleLabel->setFont(monoFont(26.0f));
titleLabel->setColour(juce::Label::textColourId, lookAndFeel.cyan);
titleLabel->setJustificationType(juce::Justification::centredLeft);
addAndMakeVisible(*titleLabel);
scaleBox = std::make_unique<juce::ComboBox>("scale");
scaleBox->addItem("100%", 100);
scaleBox->addItem("125%", 125);
scaleBox->addItem("150%", 150);
scaleBox->addItem("175%", 175);
scaleBox->addItem("200%", 200);
scaleBox->setSelectedId(100, juce::dontSendNotification);
scaleBox->setTooltip("UI scaling");
scaleBox->onChange = [this] { scaleUI((float) scaleBox->getSelectedId()); };
addAndMakeVisible(*scaleBox);
// Master gain (vertical fader, far right).
masterLabel = std::make_unique<juce::Label>("mlab", "MASTER");
masterLabel->setFont(monoFont(11.0f));
masterLabel->setColour(juce::Label::textColourId, lookAndFeel.magenta);
masterLabel->setJustificationType(juce::Justification::centred);
master = std::make_unique<juce::Slider>(juce::Slider::LinearVertical, juce::Slider::TextBoxBelow);
master->setLookAndFeel(&lookAndFeel);
master->setSliderStyle(juce::Slider::LinearVertical);
master->setTextBoxStyle(juce::Slider::TextBoxBelow, false, 40, 14);
master->setRange(-24.0, 24.0, 0.01);
master->setDoubleClickReturnValue(true, 0.0);
addAndMakeVisible(*masterLabel);
addAndMakeVisible(*master);
masterAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processor.getAPVTS(), "master", *master);
// Band faders.
const char* const names[numBands] = { "LOW SHELF", "LOW", "LOW-MID", "MID", "HIGH-MID", "HIGH", "HIGH SHELF" };
const char* const freqs[numBands] = { "60", "160", "400", "800", "1.6K", "4K", "10K" };
for (int i = 0; i < numBands; ++i)
{
auto& w = bands[i];
const juce::Colour accent = (i % 2 == 0) ? lookAndFeel.cyan : lookAndFeel.magenta;
w.freqLabel = std::make_unique<juce::Label>("freq", freqs[i]);
w.freqLabel->setFont(monoFont(14.0f).boldened());
w.freqLabel->setColour(juce::Label::textColourId, accent);
w.freqLabel->setJustificationType(juce::Justification::centred);
w.gainCaption = std::make_unique<juce::Label>("gname", names[i]);
w.gainCaption->setFont(monoFont(10.0f));
w.gainCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim);
w.gainCaption->setJustificationType(juce::Justification::centred);
w.gainValue = std::make_unique<juce::Label>("gval", "0.0");
w.gainValue->setFont(monoFont(11.0f));
w.gainValue->setColour(juce::Label::textColourId, accent);
w.gainValue->setJustificationType(juce::Justification::centred);
w.qCaption = std::make_unique<juce::Label>("qcap", "Q");
w.qCaption->setFont(monoFont(10.0f));
w.qCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim);
w.qCaption->setJustificationType(juce::Justification::centred);
w.qValue = std::make_unique<juce::Label>("qval", "1.00");
w.qValue->setFont(monoFont(10.0f));
w.qValue->setColour(juce::Label::textColourId, lookAndFeel.violet);
w.qValue->setJustificationType(juce::Justification::centred);
w.gain = std::make_unique<juce::Slider>(juce::Slider::LinearVertical, juce::Slider::NoTextBox);
w.gain->setLookAndFeel(&lookAndFeel);
w.gain->setSliderStyle(juce::Slider::LinearVertical);
w.gain->setRange(-24.0, 24.0, 0.01);
w.gain->setDoubleClickReturnValue(true, 0.0);
w.gain->setValue(0.0, juce::dontSendNotification);
w.gain->onValueChange = [this, i]
{
bands[i].gainValue->setText(juce::String(bands[i].gain->getValue(), 1), juce::dontSendNotification);
};
w.q = std::make_unique<juce::Slider>(juce::Slider::RotaryVerticalDrag, juce::Slider::NoTextBox);
w.q->setLookAndFeel(&lookAndFeel);
w.q->setSliderStyle(juce::Slider::RotaryVerticalDrag);
w.q->setRotaryParameters(2.4f, 6.9f, true); // ~270° sweep
w.q->setRange(0.1, 6.0, 0.01);
w.q->setDoubleClickReturnValue(true, 1.0);
w.q->setValue(1.0, juce::dontSendNotification);
w.q->onValueChange = [this, i]
{
bands[i].qValue->setText(juce::String(bands[i].q->getValue(), 2), juce::dontSendNotification);
};
addAndMakeVisible(*w.freqLabel);
addAndMakeVisible(*w.gainCaption);
addAndMakeVisible(*w.gainValue);
addAndMakeVisible(*w.gain);
addAndMakeVisible(*w.qCaption);
addAndMakeVisible(*w.q);
addAndMakeVisible(*w.qValue);
gainAttachments[i] = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processor.getAPVTS(), "gain" + juce::String(i), *w.gain);
qAttachments[i] = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processor.getAPVTS(), "q" + juce::String(i), *w.q);
}
// Status bar.
statusGit = std::make_unique<juce::Label>("git", processor.getGitInfo());
statusGit->setFont(monoFont(11.0f));
statusGit->setColour(juce::Label::textColourId, lookAndFeel.textDim);
statusGit->setJustificationType(juce::Justification::centredRight);
statusBar = std::make_unique<juce::Label>("status",
"GELYKEQ v" + juce::String(ProjectInfo::versionString) + " | ANALYSER");
statusBar->setFont(monoFont(11.0f));
statusBar->setColour(juce::Label::textColourId, lookAndFeel.cyan.withAlpha(0.8f));
statusBar->setJustificationType(juce::Justification::centredLeft);
addAndMakeVisible(*statusBar);
addAndMakeVisible(*statusGit);
spectrum.ensureStorageAllocated(128);
freqBins.ensureStorageAllocated(128);
startTimerHz(30);
scaleUI(100.0f);
}
GelykEQAudioProcessorEditor::~GelykEQAudioProcessorEditor()
{
stopTimer();
setLookAndFeel(nullptr);
}
void GelykEQAudioProcessorEditor::timerCallback()
{
processor.getAnalyser().copySpectrum(spectrum, freqBins, 128);
repaint();
}
void GelykEQAudioProcessorEditor::rebuildFaders()
{
}
void GelykEQAudioProcessorEditor::scaleUI(float percent)
{
const float s = percent / 100.0f;
scaling.store(s);
setSize((int) (1180 * s), (int) (640 * s));
resized();
}
void GelykEQAudioProcessorEditor::resized()
{
juce::Rectangle<int> area = getLocalBounds();
const float s = scaling.load();
const int headerH = (int) (48 * s);
const int footerH = (int) (26 * s);
const int pad = (int) (12 * s);
// Header.
juce::Rectangle<int> header = area.removeFromTop(headerH);
titleLabel->setBounds(header.removeFromLeft((int) (180 * s)).reduced(pad, 0));
scaleBox->setBounds(header.removeFromRight((int) (90 * s)).reduced((int)(6*s), (int)(9*s)));
// Footer.
juce::Rectangle<int> footer = area.removeFromBottom(footerH);
footer.reduce(pad, 0);
statusBar->setBounds(footer.removeFromLeft(footer.getWidth() / 2));
statusGit->setBounds(footer);
// Master column (right).
const int masterW = (int) (64 * s);
juce::Rectangle<int> masterArea = area.removeFromRight(masterW);
const int x0 = area.getX();
const int y0 = area.getY();
const int totalW = area.getWidth();
const int height = area.getHeight();
const int gap = (int) (12 * s);
const int colW = (totalW - gap * (numBands - 1)) / numBands;
for (int i = 0; i < numBands; ++i)
{
auto& w = bands[i];
juce::Rectangle<int> col(x0 + i * (colW + gap), y0, colW, height);
const int captionH = (int) (18 * s);
const int valueH = (int) (15 * s);
const int freqH = (int) (20 * s);
const int qValueH = (int) (14 * s);
const int qKnobH = (int) (58 * s);
const int qCapH = (int) (14 * s);
w.gainCaption->setBounds(col.removeFromTop(captionH));
w.gainValue->setBounds(col.removeFromTop(valueH));
// The gain fader fills the large analyser area (spectrum drawn behind it).
w.gain->setBounds(col.removeFromTop(col.getHeight()
- freqH - qValueH - qKnobH - qCapH - (int)(6*s)));
w.freqLabel->setBounds(col.removeFromTop(freqH));
// Q rotary control: value label, knob, caption.
w.qValue->setBounds(col.removeFromTop(qValueH));
juce::Rectangle<int> knobArea = col.removeFromTop(qKnobH);
w.q->setBounds(knobArea.reduced((int)(colW * 0.22f), 0));
w.qCaption->setBounds(col.removeFromTop(qCapH));
}
// Master column.
juce::Rectangle<int> msub = masterArea.reduced((int)(14*s), (int)(6*s));
masterLabel->setBounds(msub.removeFromTop((int) (20 * s)));
master->setBounds(msub);
}
void GelykEQAudioProcessorEditor::paint(juce::Graphics& g)
{
const float s = scaling.load();
g.fillAll(lookAndFeel.bg);
juce::Rectangle<int> area = getLocalBounds();
const int headerH = (int) (46 * s);
const int footerH = (int) (26 * s);
juce::Rectangle<int> mainArea = area;
mainArea.removeFromTop(headerH);
mainArea.removeFromBottom(footerH);
mainArea.removeFromRight((int) (64 * s));
const int topPad = (int) (20 * s);
const int botPad = (int) (28 * s);
const float topY = (float) (mainArea.getY() + topPad);
const float botY = (float) (mainArea.getY() + mainArea.getHeight() - botPad);
// Neon grid lines.
g.setColour(lookAndFeel.line);
for (int i = 1; i <= 4; ++i)
{
if (i == 3) continue; // skip the 0 dB centre (drawn separately)
float yy = topY + (botY - topY) * (i / 5.0f);
g.drawHorizontalLine((int) yy, (float) mainArea.getX(), (float) mainArea.getRight());
}
// 0 dB centre line (glowing cyan).
const float centreLineY = topY + (botY - topY) * 0.5f;
g.setColour(lookAndFeel.cyan.withAlpha(0.55f));
g.drawHorizontalLine((int) centreLineY, (float) mainArea.getX(), (float) mainArea.getRight());
// Spectrum overlay.
if (spectrum.size() > 2)
{
juce::Path path;
const float x0 = (float) mainArea.getX();
const float width = (float) mainArea.getWidth();
for (int i = 0; i < spectrum.size(); ++i)
{
float db = juce::jlimit(-72.0f, 12.0f, spectrum[i]);
float norm = (db + 72.0f) / 84.0f; // 0..1 up
float xx = x0 + (float) i / (float) (spectrum.size() - 1) * width;
float yy = botY - norm * (botY - topY);
if (i == 0) path.startNewSubPath(xx, yy);
else path.lineTo(xx, yy);
}
juce::Path filled = path;
filled.lineTo((float) mainArea.getRight(), botY);
filled.lineTo((float) mainArea.getX(), botY);
filled.closeSubPath();
g.setGradientFill(juce::ColourGradient(lookAndFeel.magenta.withAlpha(0.30f),
(float) mainArea.getX(), topY,
lookAndFeel.magenta.withAlpha(0.05f),
(float) mainArea.getX(), botY, false));
g.fillPath(filled);
g.setColour(lookAndFeel.cyan.withAlpha(0.9f));
g.strokePath(path, juce::PathStrokeType(1.6f * s));
}
}
} // namespace gelyk