#include "PluginEditor.h" #include "BuildInfo.h" #include 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 (&feedbackKnob), static_cast (&loCutKnob), static_cast (&hiCutKnob), static_cast (&timeKnob), static_cast (&mixKnob), static_cast (&panKnob), static_cast (&syncToggle), static_cast (&modeToggle), static_cast (&doubleToggle), static_cast (¢erToggle), static_cast (&autoPanToggle), static_cast (&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); syncToggle.onClick = [this] { if (syncToggle.getToggleState()) return; const int index = juce::roundToInt (proc.apvts.getRawParameterValue (ParameterIDs::timeSync)->load()); const float ms = mindball::DelayEngine::syncTimeMs (index, proc.getCurrentBpm()); if (auto* timeParam = proc.apvts.getParameter (ParameterIDs::time)) { timeParam->beginChangeGesture(); timeParam->setValueNotifyingHost (timeParam->convertTo0to1 (juce::roundToInt (ms))); timeParam->endChangeGesture(); } }; 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 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 (getHeight()) - 46.0f, static_cast (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 (&feedbackKnob), static_cast (&loCutKnob), static_cast (&hiCutKnob), static_cast (&timeKnob), static_cast (&panKnob), static_cast (&mixKnob) }) { knob->setBounds (x, 14, knobW, knobH); x += knobW + gap; } x = margin; for (auto* toggle : { static_cast (&onToggle), static_cast (&modeToggle), static_cast (&doubleToggle), static_cast (&syncToggle), static_cast (&autoPanToggle), static_cast (¢erToggle) }) { 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 (proc.apvts, ParameterIDs::timeSync, timeKnob); else timeAtt = std::make_unique (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 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 (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 (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)); }