mindball/Source/PluginEditor.cpp
2026-08-14 21:48:05 +02:00

321 lines
12 KiB
C++

#include "PluginEditor.h"
#include "BuildInfo.h"
#include <cmath>
class UI final : public juce::Component, public juce::Timer, private juce::AudioProcessorValueTreeState::Listener
{
public:
explicit UI (MindballAudioProcessor& p)
: proc (p),
feedbackKnob ("FEEDBACK", 0.35),
loCutKnob ("LO-CUT", 100.0),
hiCutKnob ("HI-CUT", 12000.0),
timeKnob ("TIME", 300.0),
mixKnob ("DRY/WET", 0.4),
panKnob ("PAN", 1.0),
syncToggle ("SYNC"),
doubleToggle ("DOUBLE"),
centerToggle ("CENTER"),
autoPanToggle ("AUTO-PAN"),
onToggle ("ON"),
modeToggle (p.apvts),
dryMeter ("DRY"),
wetMeter ("WET"),
feedbackAtt (p.apvts, ParameterIDs::feedback, feedbackKnob),
loCutAtt (p.apvts, ParameterIDs::locut, loCutKnob),
hiCutAtt (p.apvts, ParameterIDs::hicut, hiCutKnob),
mixAtt (p.apvts, ParameterIDs::drywet, mixKnob),
panAtt (p.apvts, ParameterIDs::panDepth, panKnob),
syncAtt (p.apvts, ParameterIDs::sync, syncToggle),
doubleAtt (p.apvts, ParameterIDs::doubleTap, doubleToggle),
centerAtt (p.apvts, ParameterIDs::center, centerToggle),
autoPanAtt (p.apvts, ParameterIDs::autopan, autoPanToggle),
onAtt (p.apvts, ParameterIDs::bypass, onToggle)
{
for (auto* c : { static_cast<juce::Component*> (&feedbackKnob), static_cast<juce::Component*> (&loCutKnob),
static_cast<juce::Component*> (&hiCutKnob), static_cast<juce::Component*> (&timeKnob),
static_cast<juce::Component*> (&mixKnob), static_cast<juce::Component*> (&panKnob),
static_cast<juce::Component*> (&syncToggle), static_cast<juce::Component*> (&modeToggle),
static_cast<juce::Component*> (&doubleToggle), static_cast<juce::Component*> (&centerToggle),
static_cast<juce::Component*> (&autoPanToggle), static_cast<juce::Component*> (&onToggle) })
addAndMakeVisible (c);
addAndMakeVisible (dryMeter);
addAndMakeVisible (wetMeter);
feedbackKnob.setValueTextFunction ([] (double v) { return juce::String (juce::roundToInt (v * 100.0)) + "%"; });
loCutKnob.setValueTextFunction ([] (double v) { return formatHz (v); });
hiCutKnob.setValueTextFunction ([] (double v) { return formatHz (v); });
timeKnob.setValueTextFunction ([this] (double v)
{
if (proc.apvts.getRawParameterValue (ParameterIDs::sync)->load() > 0.5f)
return syncDivisionNames [juce::jlimit (0, syncDivisionNames.size() - 1, juce::roundToInt (v))];
return juce::String (juce::roundToInt (v)) + " ms";
});
mixKnob.setValueTextFunction ([] (double v) { return juce::String (juce::roundToInt (v * 100.0)) + "%"; });
panKnob.setValueTextFunction ([] (double v) { return juce::String (juce::roundToInt (v * 100.0)) + "%"; });
onToggle.setInverted (true);
buildInfo.setJustificationType (juce::Justification::centredRight);
buildInfo.setFont (juce::Font (juce::Font::getDefaultMonospacedFontName(), 9.0f, 0));
juce::String dirty = mindball::BuildInfo::gitDirty ? " +dirty" : "";
juce::String hash = juce::String (mindball::BuildInfo::gitHash);
juce::String branch = juce::String (mindball::BuildInfo::gitBranch);
if (hash.isEmpty() || hash == "n/a")
hash = "no-commit";
if (branch.isEmpty() || branch == "n/a")
branch = "-";
buildInfo.setText ("Mindball v0.0.5\n"
+ hash + " " + branch + dirty + "\n"
+ juce::String (mindball::BuildInfo::buildTime),
juce::dontSendNotification);
addAndMakeVisible (buildInfo);
proc.apvts.addParameterListener (ParameterIDs::sync, this);
updateTimeAttachment();
startTimerHz (30);
}
~UI() override
{
stopTimer();
proc.apvts.removeParameterListener (ParameterIDs::sync, this);
}
void timerCallback() override
{
dryMeter.update (proc.getDryMeterL(), proc.getDryMeterR(), meterDecay);
wetMeter.update (proc.getWetMeterL(), proc.getWetMeterR(), meterDecay);
}
void parameterChanged (const juce::String& paramID, float) override
{
if (paramID == ParameterIDs::sync)
{
const juce::Component::SafePointer<UI> safeThis (this);
juce::MessageManager::callAsync ([safeThis]
{
if (safeThis != nullptr)
safeThis->updateTimeAttachment();
});
}
}
void paint (juce::Graphics& g) override
{
g.fillAll (MindballColors::bg);
g.setColour (MindballColors::arcBg);
g.fillRect (0.0f, static_cast<float> (getHeight()) - 46.0f,
static_cast<float> (getWidth()), 1.0f);
g.setColour (MindballColors::textBright);
g.setFont (juce::Font (juce::FontOptions (11.0f)));
g.drawText ("MINDBALL", 30, getHeight() - 40, 200, 15, juce::Justification::left);
}
void resized() override
{
const int width = getWidth();
const int margin = 30;
const int knobW = 96;
const int knobH = 118;
const int gap = (width - 2 * margin - 6 * knobW) / 5;
int x = margin;
for (auto* knob : { static_cast<juce::Slider*> (&feedbackKnob), static_cast<juce::Slider*> (&loCutKnob),
static_cast<juce::Slider*> (&hiCutKnob), static_cast<juce::Slider*> (&timeKnob),
static_cast<juce::Slider*> (&panKnob), static_cast<juce::Slider*> (&mixKnob) })
{
knob->setBounds (x, 14, knobW, knobH);
x += knobW + gap;
}
x = margin;
for (auto* toggle : { static_cast<juce::Button*> (&onToggle), static_cast<juce::Button*> (&modeToggle),
static_cast<juce::Button*> (&doubleToggle), static_cast<juce::Button*> (&syncToggle),
static_cast<juce::Button*> (&autoPanToggle), static_cast<juce::Button*> (&centerToggle) })
{
toggle->setBounds (x, 146, knobW, 30);
x += knobW + gap;
}
const int meterW = (width - 2 * margin - 20) / 2;
dryMeter.setBounds (margin, 206, meterW, 41);
wetMeter.setBounds (margin + meterW + 20, 206, meterW, 41);
buildInfo.setBounds (width - 270, getHeight() - 44, 240, 38);
}
private:
static juce::String formatHz (double v)
{
if (v >= 1000.0)
return juce::String (v / 1000.0, 1) + " kHz";
return juce::String (juce::roundToInt (v)) + " Hz";
}
void updateTimeAttachment()
{
timeAtt.reset();
timeSyncAtt.reset();
if (proc.apvts.getRawParameterValue (ParameterIDs::sync)->load() > 0.5f)
timeSyncAtt = std::make_unique<SliderAttachment> (proc.apvts, ParameterIDs::timeSync, timeKnob);
else
timeAtt = std::make_unique<SliderAttachment> (proc.apvts, ParameterIDs::time, timeKnob);
timeKnob.repaint();
}
using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment;
using ButtonAttachment = juce::AudioProcessorValueTreeState::ButtonAttachment;
static constexpr float meterDecay = 0.9636077f; // exp (-1.0f / (0.9f * 30.0f))
MindballAudioProcessor& proc;
static const juce::StringArray syncDivisionNames;
Knob feedbackKnob, loCutKnob, hiCutKnob, timeKnob, mixKnob, panKnob;
PillToggle syncToggle, doubleToggle, centerToggle, autoPanToggle, onToggle;
ModeToggle modeToggle;
LevelMeter dryMeter, wetMeter;
SliderAttachment feedbackAtt, loCutAtt, hiCutAtt, mixAtt, panAtt;
ButtonAttachment syncAtt, doubleAtt, centerAtt, autoPanAtt, onAtt;
std::unique_ptr<SliderAttachment> timeAtt, timeSyncAtt;
juce::Label buildInfo;
};
const juce::StringArray UI::syncDivisionNames = { "1/16", "1/8", "1/4", "1/3", "1/2", "3/4", "1", "3/2", "2", "3", "4", "8" };
// --------------------------------------------------------------------------------
MindballAudioProcessorEditor::MindballAudioProcessorEditor (MindballAudioProcessor& p)
: juce::AudioProcessorEditor (p),
proc (p),
ui (std::make_unique<UI> (p)),
scaleButton ([this] (float s) { setUiScale (s); }),
presetButton ([this] (int index) { applyPreset (index); })
{
setSize (juce::roundToInt (baseW * uiScale), juce::roundToInt (baseH * uiScale));
addAndMakeVisible (ui.get());
addAndMakeVisible (scaleButton);
addAndMakeVisible (presetButton);
presetButton.setCurrentProvider ([this] { return findMatchingPreset(); });
presetButton.setCurrent (findMatchingPreset());
}
MindballAudioProcessorEditor::~MindballAudioProcessorEditor() = default;
void MindballAudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll (MindballColors::bg);
}
void MindballAudioProcessorEditor::resized()
{
ui->setBounds (0, 0, baseW, baseH);
ui->setTransform (juce::AffineTransform::scale (uiScale));
const int btnH = 24;
const int btnY = 8;
scaleButton.setBounds (getWidth() - 52, btnY, 42, btnH);
presetButton.setBounds (getWidth() - 218, btnY, 160, btnH);
}
void MindballAudioProcessorEditor::setParam (const char* paramID, float value)
{
if (auto* param = proc.apvts.getParameter (paramID))
{
param->beginChangeGesture();
param->setValueNotifyingHost (param->convertTo0to1 (value));
param->endChangeGesture();
}
}
void MindballAudioProcessorEditor::setChoiceParam (const char* paramID, int index)
{
if (auto* param = static_cast<juce::AudioParameterChoice*> (proc.apvts.getParameter (paramID)))
{
param->beginChangeGesture();
param->setValueNotifyingHost (param->convertTo0to1 (index));
param->endChangeGesture();
}
}
void MindballAudioProcessorEditor::applyPreset (int index)
{
if (index < 0 || index >= Presets::count)
return;
const auto& p = Presets::all[index];
setParam (ParameterIDs::feedback, p.feedback);
setParam (ParameterIDs::locut, p.loCut);
setParam (ParameterIDs::hicut, p.hiCut);
setParam (ParameterIDs::time, p.timeMs);
setChoiceParam (ParameterIDs::timeSync, p.timeSyncIndex);
setParam (ParameterIDs::drywet, p.dryWet);
setParam (ParameterIDs::sync, p.sync ? 1.0f : 0.0f);
setChoiceParam (ParameterIDs::mode, p.mode);
setParam (ParameterIDs::doubleTap, p.doubleTap ? 1.0f : 0.0f);
setParam (ParameterIDs::center, p.center ? 1.0f : 0.0f);
setParam (ParameterIDs::autopan, p.autoPan ? 1.0f : 0.0f);
setParam (ParameterIDs::panDepth, p.panDepth);
presetButton.setCurrent (index);
}
bool MindballAudioProcessorEditor::presetMatches (const Presets::Definition& p) const
{
auto val = [this] (const char* paramID) { return proc.apvts.getRawParameterValue (paramID)->load(); };
auto near = [] (float a, float b) { return std::fabs (a - b) < 0.02f; };
if (! near (val (ParameterIDs::feedback), p.feedback)) return false;
if (! near (val (ParameterIDs::locut), p.loCut)) return false;
if (! near (val (ParameterIDs::hicut), p.hiCut)) return false;
if (! near (val (ParameterIDs::time), p.timeMs)) return false;
if (! near (val (ParameterIDs::drywet), p.dryWet)) return false;
if (! near (val (ParameterIDs::panDepth), p.panDepth)) return false;
if (std::round (val (ParameterIDs::timeSync)) != p.timeSyncIndex) return false;
if (std::round (val (ParameterIDs::mode)) != p.mode) return false;
auto flag = [&val] (const char* paramID, bool expected)
{
return (val (paramID) > 0.5f) == expected;
};
if (! flag (ParameterIDs::sync, p.sync)) return false;
if (! flag (ParameterIDs::doubleTap, p.doubleTap)) return false;
if (! flag (ParameterIDs::center, p.center)) return false;
if (! flag (ParameterIDs::autopan, p.autoPan)) return false;
return true;
}
int MindballAudioProcessorEditor::findMatchingPreset() const
{
for (int i = 0; i < Presets::count; ++i)
{
if (presetMatches (Presets::all[i]))
return i;
}
return 0;
}
void MindballAudioProcessorEditor::setUiScale (float s)
{
uiScale = s;
scaleButton.setScale (s);
ui->setTransform (juce::AffineTransform::scale (s));
setSize (juce::roundToInt (baseW * s), juce::roundToInt (baseH * s));
}