mirror of
https://codeberg.org/armin/mindball.git
synced 2026-09-01 11:50:52 +02:00
455 lines
16 KiB
C
455 lines
16 KiB
C
|
|
#pragma once
|
||
|
|
|
||
|
|
#include "PluginProcessor.h"
|
||
|
|
|
||
|
|
#include <JuceHeader.h>
|
||
|
|
|
||
|
|
#include <array>
|
||
|
|
#include <functional>
|
||
|
|
#include <memory>
|
||
|
|
#include <cmath>
|
||
|
|
|
||
|
|
namespace MindballColors
|
||
|
|
{
|
||
|
|
inline const juce::Colour bg { 0xff171718 };
|
||
|
|
inline const juce::Colour arcBg { 0xff292a2b };
|
||
|
|
inline const juce::Colour accent { 0xff6fc1ff };
|
||
|
|
inline const juce::Colour textDim { 0xff757575 };
|
||
|
|
inline const juce::Colour textBright { 0xffe6e6e6 };
|
||
|
|
inline const juce::Colour meterGreen { 0xff45a9f9 };
|
||
|
|
inline const juce::Colour meterRed { 0xffff2c6d };
|
||
|
|
}
|
||
|
|
|
||
|
|
// --------------------------------------------------------------------------------
|
||
|
|
|
||
|
|
namespace Presets
|
||
|
|
{
|
||
|
|
struct Definition
|
||
|
|
{
|
||
|
|
const char* name;
|
||
|
|
float feedback;
|
||
|
|
float loCut;
|
||
|
|
float hiCut;
|
||
|
|
float timeMs;
|
||
|
|
int timeSyncIndex;
|
||
|
|
float dryWet;
|
||
|
|
bool sync;
|
||
|
|
int mode;
|
||
|
|
bool doubleTap;
|
||
|
|
bool center;
|
||
|
|
bool autoPan;
|
||
|
|
float panDepth;
|
||
|
|
};
|
||
|
|
|
||
|
|
inline constexpr Definition all[] = {
|
||
|
|
{ "Default", 0.35f, 100.0f, 12000.0f, 300.0f, 4, 0.40f, false, 0, false, false, false, 1.00f },
|
||
|
|
{ "Slapback", 0.20f, 80.0f, 15000.0f, 120.0f, 4, 0.30f, false, 0, false, false, false, 1.00f },
|
||
|
|
{ "Analog Echo", 0.45f, 120.0f, 5000.0f, 350.0f, 4, 0.35f, false, 0, false, false, false, 1.00f },
|
||
|
|
{ "Tape Echo", 0.55f, 100.0f, 3000.0f, 450.0f, 4, 0.40f, false, 0, false, false, false, 1.00f },
|
||
|
|
{ "Dub Delay", 0.70f, 150.0f, 2000.0f, 600.0f, 4, 0.50f, false, 0, false, false, false, 1.00f },
|
||
|
|
{ "Ping-Pong", 0.45f, 100.0f, 10000.0f, 400.0f, 4, 0.40f, false, 2, false, false, false, 1.00f },
|
||
|
|
{ "Wide Ping-Pong", 0.40f, 100.0f, 10000.0f, 500.0f, 4, 0.45f, false, 2, true, false, false, 1.00f },
|
||
|
|
{ "SuperPong", 0.50f, 100.0f, 8000.0f, 350.0f, 4, 0.45f, false, 3, false, false, false, 1.00f },
|
||
|
|
{ "Sync 1/16", 0.35f, 100.0f, 12000.0f, 250.0f, 0, 0.35f, true, 0, false, false, false, 1.00f },
|
||
|
|
{ "Sync 1/8", 0.40f, 100.0f, 12000.0f, 250.0f, 1, 0.40f, true, 0, false, false, false, 1.00f },
|
||
|
|
{ "Sync 1/4", 0.45f, 100.0f, 12000.0f, 300.0f, 2, 0.45f, true, 0, false, false, false, 1.00f },
|
||
|
|
{ "Sync 1/2", 0.50f, 100.0f, 12000.0f, 300.0f, 4, 0.45f, true, 0, false, false, false, 1.00f },
|
||
|
|
{ "Sync 3/4", 0.55f, 100.0f, 12000.0f, 300.0f, 5, 0.50f, true, 0, false, false, false, 1.00f },
|
||
|
|
{ "Invert", 0.40f, 100.0f, 12000.0f, 300.0f, 4, 0.40f, false, 1, false, false, false, 1.00f },
|
||
|
|
{ "Hall Echo", 0.25f, 200.0f, 8000.0f, 1200.0f, 4, 0.30f, false, 0, false, false, false, 1.00f },
|
||
|
|
{ "Whale Song", 0.15f, 300.0f, 6000.0f, 3000.0f, 4, 0.50f, false, 0, false, false, false, 1.00f },
|
||
|
|
{ "Filtered Space", 0.55f, 300.0f, 4000.0f, 900.0f, 4, 0.45f, false, 0, false, false, false, 1.00f },
|
||
|
|
{ "Auto-Pan Delay", 0.35f, 100.0f, 12000.0f, 250.0f, 4, 0.40f, false, 0, false, false, true, 1.00f },
|
||
|
|
{ "Centered Pong", 0.60f, 100.0f, 8000.0f, 500.0f, 4, 0.45f, false, 2, false, true, false, 1.00f },
|
||
|
|
{ "Clean Echo", 0.15f, 60.0f, 16000.0f, 280.0f, 4, 0.25f, false, 0, false, false, false, 1.00f },
|
||
|
|
};
|
||
|
|
|
||
|
|
inline constexpr int count = static_cast<int> (std::size (all));
|
||
|
|
}
|
||
|
|
|
||
|
|
// --------------------------------------------------------------------------------
|
||
|
|
|
||
|
|
class Knob final : public juce::Slider
|
||
|
|
{
|
||
|
|
public:
|
||
|
|
Knob (const juce::String& labelText, double defaultValue)
|
||
|
|
: label (labelText)
|
||
|
|
{
|
||
|
|
setSliderStyle (juce::Slider::SliderStyle::RotaryVerticalDrag);
|
||
|
|
setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0);
|
||
|
|
setDoubleClickReturnValue (true, defaultValue);
|
||
|
|
setColour (juce::Slider::ColourIds::thumbColourId, MindballColors::accent);
|
||
|
|
}
|
||
|
|
|
||
|
|
void setValueTextFunction (std::function<juce::String (double)> fn)
|
||
|
|
{
|
||
|
|
valueTextFn = std::move (fn);
|
||
|
|
}
|
||
|
|
|
||
|
|
void paint (juce::Graphics& g) override
|
||
|
|
{
|
||
|
|
const auto area = getLocalBounds().toFloat();
|
||
|
|
const float cx = area.getCentreX();
|
||
|
|
const float cy = area.getCentreY() - 8.0f;
|
||
|
|
const float radius = juce::jmin (area.getWidth(), area.getHeight()) * 0.30f;
|
||
|
|
|
||
|
|
const float range = static_cast<float> (getMaximum()) - static_cast<float> (getMinimum());
|
||
|
|
const float norm = juce::jlimit (0.0f, 1.0f,
|
||
|
|
range > 0.0f
|
||
|
|
? (static_cast<float> (getValue()) - static_cast<float> (getMinimum())) / range
|
||
|
|
: 0.0f);
|
||
|
|
|
||
|
|
const float sweep = juce::MathConstants<float>::twoPi * 0.75f;
|
||
|
|
|
||
|
|
// JUCE's addCentredArc measures clockwise from the top, so the arc
|
||
|
|
// angles are the screen angles shifted by +90 degrees.
|
||
|
|
const float arcStart = juce::degreesToRadians (225.0f); // down-left
|
||
|
|
const float arcEnd = arcStart + sweep; // down-right
|
||
|
|
|
||
|
|
juce::Path arc;
|
||
|
|
arc.addCentredArc (cx, cy, radius, radius, 0.0f, arcStart, arcEnd, true);
|
||
|
|
g.setColour (MindballColors::arcBg);
|
||
|
|
g.strokePath (arc, juce::PathStrokeType (3.0f,
|
||
|
|
juce::PathStrokeType::JointStyle::curved,
|
||
|
|
juce::PathStrokeType::EndCapStyle::rounded));
|
||
|
|
|
||
|
|
juce::Path valueArc;
|
||
|
|
valueArc.addCentredArc (cx, cy, radius, radius, 0.0f, arcStart, arcStart + sweep * norm, true);
|
||
|
|
g.setColour (MindballColors::accent);
|
||
|
|
g.strokePath (valueArc, juce::PathStrokeType (3.0f,
|
||
|
|
juce::PathStrokeType::JointStyle::curved,
|
||
|
|
juce::PathStrokeType::EndCapStyle::rounded));
|
||
|
|
|
||
|
|
if (valueTextFn != nullptr)
|
||
|
|
{
|
||
|
|
const juce::String valueText = valueTextFn (getValue());
|
||
|
|
if (valueText.isNotEmpty())
|
||
|
|
{
|
||
|
|
g.setColour (MindballColors::textBright);
|
||
|
|
g.setFont (juce::Font (juce::FontOptions (9.0f)));
|
||
|
|
g.drawFittedText (valueText, getLocalBounds().removeFromBottom (32).removeFromTop (16),
|
||
|
|
juce::Justification::centred, 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
g.setColour (MindballColors::textDim);
|
||
|
|
g.setFont (juce::Font (juce::FontOptions (9.0f)));
|
||
|
|
g.drawFittedText (label, getLocalBounds().removeFromBottom (16),
|
||
|
|
juce::Justification::centredBottom, 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
private:
|
||
|
|
juce::String label;
|
||
|
|
std::function<juce::String (double)> valueTextFn;
|
||
|
|
};
|
||
|
|
|
||
|
|
// --------------------------------------------------------------------------------
|
||
|
|
|
||
|
|
class LevelMeter final : public juce::Component
|
||
|
|
{
|
||
|
|
public:
|
||
|
|
explicit LevelMeter (const juce::String& meterName)
|
||
|
|
: name (meterName)
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
void update (float linL, float linR, float decay)
|
||
|
|
{
|
||
|
|
levels[0] = linL;
|
||
|
|
levels[1] = linR;
|
||
|
|
peaks[0] = std::max (peaks[0] * decay, linL);
|
||
|
|
peaks[1] = std::max (peaks[1] * decay, linR);
|
||
|
|
repaint();
|
||
|
|
}
|
||
|
|
|
||
|
|
void paint (juce::Graphics& g) override
|
||
|
|
{
|
||
|
|
constexpr int steps = 15;
|
||
|
|
constexpr float dbMin = -30.0f;
|
||
|
|
constexpr float dbPerStep = 2.0f;
|
||
|
|
|
||
|
|
auto b = getLocalBounds().toFloat();
|
||
|
|
g.setColour (MindballColors::textDim);
|
||
|
|
g.setFont (juce::Font (juce::FontOptions (9.0f)));
|
||
|
|
g.drawText (name, b.removeFromTop (13.0f), juce::Justification::centred, false);
|
||
|
|
|
||
|
|
const float rowH = (b.getHeight() - 4.0f) * 0.5f;
|
||
|
|
const float segGap = 1.0f;
|
||
|
|
const float segW = (b.getWidth() - (steps - 1) * segGap) / static_cast<float> (steps);
|
||
|
|
|
||
|
|
for (int ch = 0; ch < 2; ++ch)
|
||
|
|
{
|
||
|
|
auto row = b.removeFromTop (rowH);
|
||
|
|
const float lin = std::max (0.0f, levels[ch]);
|
||
|
|
const float db = 20.0f * std::log10 (lin > 0.0f ? lin : 1e-9f);
|
||
|
|
const int lit = juce::jlimit (0, steps, juce::roundToInt ((db - dbMin) / dbPerStep));
|
||
|
|
const int pk = juce::jlimit (0, steps, juce::roundToInt ((20.0f * std::log10 (std::max (1e-9f, peaks[ch])) - dbMin) / dbPerStep));
|
||
|
|
|
||
|
|
for (int s = 0; s < steps; ++s)
|
||
|
|
{
|
||
|
|
const auto seg = juce::Rectangle<float> (row.getX() + s * (segW + segGap),
|
||
|
|
row.getY() + 2.0f, segW, rowH - 4.0f);
|
||
|
|
g.setColour (s < lit ? MindballColors::meterGreen : MindballColors::arcBg);
|
||
|
|
g.fillRoundedRectangle (seg, 1.0f);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (pk > lit)
|
||
|
|
{
|
||
|
|
const auto seg = juce::Rectangle<float> (row.getX() + pk * (segW + segGap),
|
||
|
|
row.getY() + 2.0f, segW, rowH - 4.0f);
|
||
|
|
g.setColour (MindballColors::meterRed);
|
||
|
|
g.fillRoundedRectangle (seg, 1.0f);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private:
|
||
|
|
juce::String name;
|
||
|
|
float levels[2] = { 0.0f, 0.0f };
|
||
|
|
float peaks[2] = { 0.0f, 0.0f };
|
||
|
|
};
|
||
|
|
|
||
|
|
// --------------------------------------------------------------------------------
|
||
|
|
|
||
|
|
class PillToggle final : public juce::Button
|
||
|
|
{
|
||
|
|
public:
|
||
|
|
explicit PillToggle (const juce::String& text)
|
||
|
|
: juce::Button (text)
|
||
|
|
{
|
||
|
|
setClickingTogglesState (true);
|
||
|
|
}
|
||
|
|
|
||
|
|
void setInverted (bool shouldInvert)
|
||
|
|
{
|
||
|
|
inverted = shouldInvert;
|
||
|
|
repaint();
|
||
|
|
}
|
||
|
|
|
||
|
|
void paintButton (juce::Graphics& g, bool, bool) override
|
||
|
|
{
|
||
|
|
const auto b = getLocalBounds().toFloat().reduced (1.0f);
|
||
|
|
const bool on = inverted ? ! getToggleState() : getToggleState();
|
||
|
|
|
||
|
|
if (on)
|
||
|
|
{
|
||
|
|
g.setColour (MindballColors::accent);
|
||
|
|
g.fillRoundedRectangle (b, b.getHeight() * 0.5f);
|
||
|
|
g.setColour (MindballColors::bg);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
g.setColour (MindballColors::arcBg);
|
||
|
|
g.drawRoundedRectangle (b, b.getHeight() * 0.5f, 1.0f);
|
||
|
|
g.setColour (MindballColors::textDim);
|
||
|
|
}
|
||
|
|
|
||
|
|
g.setFont (juce::Font (juce::FontOptions (9.0f)));
|
||
|
|
g.drawText (getButtonText(), getLocalBounds(), juce::Justification::centred, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
private:
|
||
|
|
bool inverted = false;
|
||
|
|
};
|
||
|
|
|
||
|
|
// --------------------------------------------------------------------------------
|
||
|
|
|
||
|
|
class ModeToggle final : public juce::Button, private juce::AudioProcessorParameter::Listener
|
||
|
|
{
|
||
|
|
public:
|
||
|
|
explicit ModeToggle (juce::AudioProcessorValueTreeState& apvts)
|
||
|
|
: juce::Button ("Mode"),
|
||
|
|
param (static_cast<juce::AudioParameterChoice*> (apvts.getParameter (ParameterIDs::mode)))
|
||
|
|
{
|
||
|
|
if (param != nullptr)
|
||
|
|
param->addListener (this);
|
||
|
|
}
|
||
|
|
|
||
|
|
~ModeToggle() override
|
||
|
|
{
|
||
|
|
if (param != nullptr)
|
||
|
|
param->removeListener (this);
|
||
|
|
}
|
||
|
|
|
||
|
|
void clicked() override
|
||
|
|
{
|
||
|
|
if (param == nullptr)
|
||
|
|
return;
|
||
|
|
|
||
|
|
const int next = (param->getIndex() + 1) % param->choices.size();
|
||
|
|
param->beginChangeGesture();
|
||
|
|
param->setValueNotifyingHost (param->convertTo0to1 (next));
|
||
|
|
param->endChangeGesture();
|
||
|
|
}
|
||
|
|
|
||
|
|
void parameterValueChanged (int, float) override
|
||
|
|
{
|
||
|
|
juce::MessageManager::callAsync ([this] { repaint(); });
|
||
|
|
}
|
||
|
|
|
||
|
|
void parameterGestureChanged (int, bool) override {}
|
||
|
|
|
||
|
|
void paintButton (juce::Graphics& g, bool, bool) override
|
||
|
|
{
|
||
|
|
const auto b = getLocalBounds().toFloat().reduced (1.0f);
|
||
|
|
const int mode = param != nullptr ? param->getIndex() : 0;
|
||
|
|
const bool on = mode != 0;
|
||
|
|
|
||
|
|
if (on)
|
||
|
|
{
|
||
|
|
g.setColour (MindballColors::accent);
|
||
|
|
g.fillRoundedRectangle (b, b.getHeight() * 0.5f);
|
||
|
|
g.setColour (MindballColors::bg);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
g.setColour (MindballColors::arcBg);
|
||
|
|
g.drawRoundedRectangle (b, b.getHeight() * 0.5f, 1.0f);
|
||
|
|
g.setColour (MindballColors::textDim);
|
||
|
|
}
|
||
|
|
|
||
|
|
g.setFont (juce::Font (juce::FontOptions (9.0f)));
|
||
|
|
g.drawText (param != nullptr ? param->getCurrentChoiceName() : "Mode",
|
||
|
|
getLocalBounds(), juce::Justification::centred, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
private:
|
||
|
|
juce::AudioParameterChoice* param;
|
||
|
|
};
|
||
|
|
|
||
|
|
// --------------------------------------------------------------------------------
|
||
|
|
|
||
|
|
class ScaleButton final : public juce::Button
|
||
|
|
{
|
||
|
|
public:
|
||
|
|
explicit ScaleButton (std::function<void (float)> onPick)
|
||
|
|
: juce::Button ("UI Scale"), pickCallback (std::move (onPick))
|
||
|
|
{
|
||
|
|
setScale (1.5f);
|
||
|
|
}
|
||
|
|
|
||
|
|
void setScale (float s)
|
||
|
|
{
|
||
|
|
scale = s;
|
||
|
|
setButtonText (juce::String (juce::roundToInt (scale * 100.0f)) + "%");
|
||
|
|
repaint();
|
||
|
|
}
|
||
|
|
|
||
|
|
void paintButton (juce::Graphics& g, bool, bool) override
|
||
|
|
{
|
||
|
|
const auto b = getLocalBounds().toFloat().reduced (1.0f);
|
||
|
|
g.setColour (MindballColors::arcBg);
|
||
|
|
g.drawRoundedRectangle (b, b.getHeight() * 0.5f, 1.0f);
|
||
|
|
g.setColour (MindballColors::textBright);
|
||
|
|
g.setFont (juce::Font (juce::FontOptions (11.0f)));
|
||
|
|
g.drawText (getButtonText(), getLocalBounds(), juce::Justification::centred, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
void clicked() override
|
||
|
|
{
|
||
|
|
const float options[] = { 0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 2.0f, 2.5f, 3.0f };
|
||
|
|
|
||
|
|
juce::PopupMenu menu;
|
||
|
|
for (int i = 0; i < 8; ++i)
|
||
|
|
menu.addItem (i + 1, juce::String (juce::roundToInt (options[i] * 100.0f)) + "%",
|
||
|
|
true, scale == options[i]);
|
||
|
|
|
||
|
|
menu.showMenuAsync (juce::PopupMenu::Options().withTargetComponent (this),
|
||
|
|
[this, options] (int result)
|
||
|
|
{
|
||
|
|
if (result >= 1 && result <= 8 && pickCallback != nullptr)
|
||
|
|
pickCallback (options[result - 1]);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private:
|
||
|
|
std::function<void (float)> pickCallback;
|
||
|
|
float scale = 1.5f;
|
||
|
|
};
|
||
|
|
|
||
|
|
// --------------------------------------------------------------------------------
|
||
|
|
|
||
|
|
class PresetButton final : public juce::Button
|
||
|
|
{
|
||
|
|
public:
|
||
|
|
explicit PresetButton (std::function<void (int)> onPick)
|
||
|
|
: juce::Button ("Presets"), pickCallback (std::move (onPick))
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
void setCurrent (int index)
|
||
|
|
{
|
||
|
|
current = index;
|
||
|
|
setButtonText (juce::String (Presets::all[index].name));
|
||
|
|
repaint();
|
||
|
|
}
|
||
|
|
|
||
|
|
void setCurrentProvider (std::function<int()> provider)
|
||
|
|
{
|
||
|
|
currentProvider = std::move (provider);
|
||
|
|
}
|
||
|
|
|
||
|
|
void paintButton (juce::Graphics& g, bool, bool) override
|
||
|
|
{
|
||
|
|
const auto b = getLocalBounds().toFloat().reduced (1.0f);
|
||
|
|
g.setColour (MindballColors::arcBg);
|
||
|
|
g.drawRoundedRectangle (b, b.getHeight() * 0.5f, 1.0f);
|
||
|
|
g.setColour (MindballColors::textBright);
|
||
|
|
g.setFont (juce::Font (juce::FontOptions (11.0f)));
|
||
|
|
g.drawText (getButtonText(), b, juce::Justification::centred, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
void clicked() override
|
||
|
|
{
|
||
|
|
const int checked = currentProvider != nullptr ? currentProvider() : current;
|
||
|
|
|
||
|
|
juce::PopupMenu menu;
|
||
|
|
for (int i = 0; i < Presets::count; ++i)
|
||
|
|
menu.addItem (i + 1, juce::String (Presets::all[i].name), true, i == checked);
|
||
|
|
|
||
|
|
menu.showMenuAsync (juce::PopupMenu::Options().withTargetComponent (this),
|
||
|
|
[this] (int result)
|
||
|
|
{
|
||
|
|
if (result >= 1 && result <= Presets::count && pickCallback != nullptr)
|
||
|
|
pickCallback (result - 1);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private:
|
||
|
|
std::function<void (int)> pickCallback;
|
||
|
|
std::function<int()> currentProvider;
|
||
|
|
int current = 0;
|
||
|
|
};
|
||
|
|
|
||
|
|
// --------------------------------------------------------------------------------
|
||
|
|
|
||
|
|
class UI;
|
||
|
|
|
||
|
|
class MindballAudioProcessorEditor final : public juce::AudioProcessorEditor
|
||
|
|
{
|
||
|
|
public:
|
||
|
|
explicit MindballAudioProcessorEditor (MindballAudioProcessor& p);
|
||
|
|
~MindballAudioProcessorEditor() override;
|
||
|
|
|
||
|
|
void paint (juce::Graphics&) override;
|
||
|
|
void resized() override;
|
||
|
|
|
||
|
|
private:
|
||
|
|
static constexpr int baseW = 736;
|
||
|
|
static constexpr int baseH = 320;
|
||
|
|
|
||
|
|
void setUiScale (float s);
|
||
|
|
void applyPreset (int index);
|
||
|
|
int findMatchingPreset() const;
|
||
|
|
bool presetMatches (const Presets::Definition& p) const;
|
||
|
|
void setParam (const char* paramID, float value);
|
||
|
|
void setChoiceParam (const char* paramID, int index);
|
||
|
|
|
||
|
|
MindballAudioProcessor& proc;
|
||
|
|
std::unique_ptr<UI> ui;
|
||
|
|
ScaleButton scaleButton;
|
||
|
|
PresetButton presetButton;
|
||
|
|
float uiScale = 1.5f;
|
||
|
|
|
||
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MindballAudioProcessorEditor)
|
||
|
|
};
|