mirror of
https://codeberg.org/armin/mindball.git
synced 2026-09-01 03:40:49 +02:00
init
This commit is contained in:
parent
1b541ba587
commit
78e52e612f
9 changed files with 1553 additions and 0 deletions
12
Source/BuildInfo.h.in
Normal file
12
Source/BuildInfo.h.in
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
namespace mindball
|
||||
{
|
||||
namespace BuildInfo
|
||||
{
|
||||
inline constexpr const char* gitHash = "@MINDBALL_GIT_HASH@";
|
||||
inline constexpr const char* gitBranch = "@MINDBALL_GIT_BRANCH@";
|
||||
inline constexpr bool gitDirty = @MINDBALL_GIT_DIRTY@;
|
||||
inline constexpr const char* buildTime = "@MINDBALL_BUILD_TIME@";
|
||||
}
|
||||
}
|
||||
282
Source/DelayEngine.h
Normal file
282
Source/DelayEngine.h
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
#pragma once
|
||||
|
||||
#include "DelayLine.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace mindball
|
||||
{
|
||||
|
||||
class DelayEngine
|
||||
{
|
||||
public:
|
||||
enum Mode
|
||||
{
|
||||
normal = 0,
|
||||
invert,
|
||||
pingpong,
|
||||
superpong
|
||||
};
|
||||
|
||||
struct Params
|
||||
{
|
||||
float feedback = 0.35f;
|
||||
float loCutHz = 100.0f;
|
||||
float hiCutHz = 12000.0f;
|
||||
float dryWet = 0.4f;
|
||||
float timeMs = 300.0f;
|
||||
int timeSyncIndex = 0;
|
||||
int mode = normal;
|
||||
bool sync = false;
|
||||
bool doubleTap = false;
|
||||
bool center = false;
|
||||
bool autoPan = false;
|
||||
float panDepth = 1.0f;
|
||||
double bpm = 120.0;
|
||||
};
|
||||
|
||||
void prepare (double sampleRate, double maxDelaySeconds)
|
||||
{
|
||||
sr = static_cast<float> (sampleRate);
|
||||
dl.prepare (static_cast<float> (maxDelaySeconds * sampleRate));
|
||||
dr.prepare (static_cast<float> (maxDelaySeconds * sampleRate));
|
||||
clear();
|
||||
|
||||
sFeedback = 0.0f;
|
||||
sLoCut = 20.0f;
|
||||
sHiCut = 18000.0f;
|
||||
sDryWet = 0.0f;
|
||||
head1 = 1.0f;
|
||||
head2 = 1.0f;
|
||||
blend = 0.0f;
|
||||
lfoPhase = 0.0f;
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
dl.clear();
|
||||
dr.clear();
|
||||
|
||||
lpL.reset(); hpL.reset();
|
||||
lpR.reset(); hpR.reset();
|
||||
|
||||
lfoPhase = 0.0f;
|
||||
}
|
||||
|
||||
void process (const float* inL, const float* inR,
|
||||
float* outL, float* outR,
|
||||
int numSamples, const Params& p,
|
||||
float* wetLOut = nullptr, float* wetROut = nullptr)
|
||||
{
|
||||
constexpr float pi = 3.14159265358979f;
|
||||
|
||||
const float kPar = 1.0f - std::exp (-1.0f / (0.02f * sr));
|
||||
|
||||
const float fadeInc = 1.0f / (0.012f * sr);
|
||||
|
||||
float targetTimeSamples;
|
||||
if (p.sync)
|
||||
{
|
||||
const int idx = std::clamp (p.timeSyncIndex, 0, numDivisions - 1);
|
||||
const double secondsPerBeat = 60.0 / std::max (1.0, p.bpm);
|
||||
targetTimeSamples = static_cast<float> (divisionBeats[static_cast<std::size_t> (idx)]
|
||||
* secondsPerBeat * sr);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetTimeSamples = p.timeMs * 0.001f * sr;
|
||||
}
|
||||
targetTimeSamples = std::max (2.0f, targetTimeSamples);
|
||||
|
||||
const float targetFeedback = p.feedback;
|
||||
const float targetLoCut = p.loCutHz;
|
||||
const float targetHiCut = p.hiCutHz;
|
||||
const float targetDryWet = p.dryWet;
|
||||
|
||||
const bool dbl = p.doubleTap;
|
||||
const bool ctr = p.center;
|
||||
const bool needB = dbl || (ctr && (p.mode == pingpong || p.mode == superpong));
|
||||
|
||||
const float lfoRate = p.autoPan ? (p.sync ? static_cast<float> (p.bpm / 240.0) : 0.25f) : 0.0f;
|
||||
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
sFeedback += (targetFeedback - sFeedback) * kPar;
|
||||
sLoCut += (targetLoCut - sLoCut) * kPar;
|
||||
sHiCut += (targetHiCut - sHiCut) * kPar;
|
||||
sDryWet += (targetDryWet - sDryWet) * kPar;
|
||||
|
||||
// Delay time changes are done with two stationary read heads and a
|
||||
// short crossfade, never by gliding a read position. This avoids the
|
||||
// pitch-bend "sped up / slowed down" artifacts that come from a
|
||||
// moving read head. When the target moves, snap head2 to it and fade
|
||||
// between the two fixed heads; once blended, head2 takes over.
|
||||
if (blend == 0.0f && std::fabs (targetTimeSamples - head1) > 0.5f)
|
||||
head2 = targetTimeSamples;
|
||||
|
||||
if (blend < 1.0f)
|
||||
{
|
||||
blend += fadeInc;
|
||||
if (blend >= 1.0f)
|
||||
{
|
||||
head1 = head2;
|
||||
blend = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
lpL.setFc (sHiCut, sr); hpL.setFc (sLoCut, sr);
|
||||
lpR.setFc (sHiCut, sr); hpR.setFc (sLoCut, sr);
|
||||
|
||||
const float h1L = dl.read (head1);
|
||||
const float h2L = dl.read (head2);
|
||||
const float h1R = dr.read (head1);
|
||||
const float h2R = dr.read (head2);
|
||||
const float aL = h1L + (h2L - h1L) * blend;
|
||||
const float aR = h1R + (h2R - h1R) * blend;
|
||||
|
||||
float bL = 0.0f, bR = 0.0f;
|
||||
if (needB)
|
||||
{
|
||||
const float two1 = 2.0f * head1;
|
||||
const float two2 = 2.0f * head2;
|
||||
bL = dl.read (two1) + (dl.read (two2) - dl.read (two1)) * blend;
|
||||
bR = dr.read (two1) + (dr.read (two2) - dr.read (two1)) * blend;
|
||||
}
|
||||
|
||||
float fbL = 0.0f, fbR = 0.0f;
|
||||
float wetL = 0.0f, wetR = 0.0f;
|
||||
|
||||
switch (p.mode)
|
||||
{
|
||||
case normal:
|
||||
fbL = aL;
|
||||
fbR = aR;
|
||||
wetL = aL + bL;
|
||||
wetR = aR + bR;
|
||||
break;
|
||||
|
||||
case invert:
|
||||
fbL = aL;
|
||||
fbR = -aR;
|
||||
wetL = aL + bL;
|
||||
wetR = -(aR + bR);
|
||||
break;
|
||||
|
||||
case pingpong:
|
||||
fbL = aR;
|
||||
fbR = aL;
|
||||
wetL = aL + bL;
|
||||
wetR = aR + bR;
|
||||
break;
|
||||
|
||||
case superpong:
|
||||
{
|
||||
const float sumA = aL + aR;
|
||||
const float sumAB = sumA + (needB ? bL + bR : 0.0f);
|
||||
fbL = 0.5f * sumA;
|
||||
fbR = 0.5f * sumA;
|
||||
wetL = sumAB * 0.5f;
|
||||
wetR = sumAB * 0.5f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ctr && (p.mode == pingpong || p.mode == superpong))
|
||||
{
|
||||
const float c = 0.5f * (aL + aR);
|
||||
wetL = c + (needB ? bL : 0.0f);
|
||||
wetR = c + (needB ? bR : 0.0f);
|
||||
}
|
||||
|
||||
if (p.autoPan)
|
||||
{
|
||||
const float pan = std::sin (2.0f * pi * lfoPhase);
|
||||
lfoPhase += lfoRate / sr;
|
||||
if (lfoPhase >= 1.0f)
|
||||
lfoPhase -= std::floor (lfoPhase);
|
||||
|
||||
const float s = std::clamp (p.panDepth, 0.0f, 1.0f);
|
||||
const float angle = (1.0f - s) * pi * 0.25f + s * (pan + 1.0f) * pi * 0.25f;
|
||||
const float gL = std::cos (angle);
|
||||
const float gR = std::sin (angle);
|
||||
wetL *= gL;
|
||||
wetR *= gR;
|
||||
}
|
||||
|
||||
const float g = sFeedback * (1.0f + 0.002f * std::pow (sFeedback, 100.0f));
|
||||
dl.write (inL[i] + softClip (lpL.processLP (hpL.processHP (fbL * g))));
|
||||
dr.write (inR[i] + softClip (lpR.processLP (hpR.processHP (fbR * g))));
|
||||
|
||||
const float w = sDryWet;
|
||||
float dryGain, wetGain;
|
||||
if (w <= 0.5f)
|
||||
{
|
||||
dryGain = 1.0f;
|
||||
wetGain = 2.0f * w;
|
||||
}
|
||||
else
|
||||
{
|
||||
dryGain = 2.0f * (1.0f - w);
|
||||
wetGain = 1.0f;
|
||||
}
|
||||
outL[i] = inL[i] * dryGain + wetL * wetGain;
|
||||
outR[i] = inR[i] * dryGain + wetR * wetGain;
|
||||
if (wetLOut != nullptr)
|
||||
wetLOut[i] = wetL * wetGain;
|
||||
if (wetROut != nullptr)
|
||||
wetROut[i] = wetR * wetGain;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static float softClip (float x)
|
||||
{
|
||||
const float ax = std::fabs (x);
|
||||
if (ax < 0.8f)
|
||||
return x;
|
||||
return std::copysign (0.8f + 0.2f * std::tanh ((ax - 0.8f) / 0.2f), x);
|
||||
}
|
||||
|
||||
struct OnePole
|
||||
{
|
||||
float alpha = 0.0f;
|
||||
float state = 0.0f;
|
||||
float xPrev = 0.0f;
|
||||
|
||||
void reset() { state = 0.0f; xPrev = 0.0f; }
|
||||
|
||||
void setFc (float fc, float sr)
|
||||
{
|
||||
constexpr float pi = 3.14159265358979f;
|
||||
alpha = 1.0f - std::exp (-2.0f * pi * fc / sr);
|
||||
}
|
||||
|
||||
float processLP (float x) { state += alpha * (x - state); return state; }
|
||||
float processHP (float x) { state = x - xPrev + (1.0f - alpha) * state; xPrev = x; return state; }
|
||||
};
|
||||
|
||||
static constexpr int numDivisions = 12;
|
||||
static constexpr std::array<float, numDivisions> divisionBeats = {
|
||||
1.0f / 16.0f, 1.0f / 8.0f, 1.0f / 4.0f, 1.0f / 3.0f, 1.0f / 2.0f, 3.0f / 4.0f,
|
||||
1.0f, 3.0f / 2.0f, 2.0f, 3.0f, 4.0f, 8.0f
|
||||
};
|
||||
|
||||
DelayLine dl, dr;
|
||||
OnePole lpL, hpL, lpR, hpR;
|
||||
|
||||
float sr = 48000.0f;
|
||||
float lfoPhase = 0.0f;
|
||||
|
||||
float sFeedback = 0.0f;
|
||||
float sLoCut = 0.0f;
|
||||
float sHiCut = 0.0f;
|
||||
float sDryWet = 0.0f;
|
||||
float head1 = 1.0f;
|
||||
float head2 = 1.0f;
|
||||
float blend = 0.0f;
|
||||
};
|
||||
|
||||
}
|
||||
59
Source/DelayLine.h
Normal file
59
Source/DelayLine.h
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace mindball
|
||||
{
|
||||
|
||||
// Simple fractional-delay ring buffer with linear interpolation.
|
||||
class DelayLine
|
||||
{
|
||||
public:
|
||||
void prepare (float maxDelaySamples)
|
||||
{
|
||||
maxDelay = std::max (1.0f, maxDelaySamples);
|
||||
buffer.assign (static_cast<std::size_t> (maxDelay) + 2, 0.0f);
|
||||
writePos = 0;
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
std::fill (buffer.begin(), buffer.end(), 0.0f);
|
||||
writePos = 0;
|
||||
}
|
||||
|
||||
void write (float sample)
|
||||
{
|
||||
buffer[static_cast<std::size_t> (writePos)] = sample;
|
||||
writePos = (writePos + 1) % static_cast<int> (buffer.size());
|
||||
}
|
||||
|
||||
float read (float delaySamples) const
|
||||
{
|
||||
if (buffer.empty())
|
||||
return 0.0f;
|
||||
|
||||
const float d = std::max (1.0f, std::min (delaySamples, maxDelay - 1.0f));
|
||||
const int size = static_cast<int> (buffer.size());
|
||||
const float readPos = static_cast<float> (writePos) - d;
|
||||
const float floorPos = std::floor (readPos);
|
||||
|
||||
int i0 = static_cast<int> (floorPos) % size;
|
||||
if (i0 < 0)
|
||||
i0 += size;
|
||||
|
||||
const int i1 = (i0 + 1) % size;
|
||||
const float frac = readPos - floorPos;
|
||||
return buffer[static_cast<std::size_t> (i0)]
|
||||
+ (buffer[static_cast<std::size_t> (i1)] - buffer[static_cast<std::size_t> (i0)]) * frac;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<float> buffer;
|
||||
float maxDelay = 1.0f;
|
||||
int writePos = 0;
|
||||
};
|
||||
|
||||
}
|
||||
320
Source/PluginEditor.cpp
Normal file
320
Source/PluginEditor.cpp
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
#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*> (¢erToggle),
|
||||
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 v1.0.0\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
|
||||
{
|
||||
const float decay = std::exp (-1.0f / (0.9f * 30.0f));
|
||||
dryMeter.update (proc.getDryMeterL(), proc.getDryMeterR(), decay);
|
||||
wetMeter.update (proc.getWetMeterL(), proc.getWetMeterR(), decay);
|
||||
}
|
||||
|
||||
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*> (¢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<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;
|
||||
|
||||
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));
|
||||
}
|
||||
455
Source/PluginEditor.h
Normal file
455
Source/PluginEditor.h
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
#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)
|
||||
};
|
||||
196
Source/PluginProcessor.cpp
Normal file
196
Source/PluginProcessor.cpp
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
#include "PluginProcessor.h"
|
||||
#include "PluginEditor.h"
|
||||
|
||||
MindballAudioProcessor::MindballAudioProcessor()
|
||||
: AudioProcessor (BusesProperties()
|
||||
.withInput ("Input", juce::AudioChannelSet::stereo(), true)
|
||||
.withOutput ("Output", juce::AudioChannelSet::stereo(), true)),
|
||||
apvts (*this, nullptr, "Parameters", createParameterLayout())
|
||||
{
|
||||
}
|
||||
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout MindballAudioProcessor::createParameterLayout()
|
||||
{
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout layout;
|
||||
|
||||
layout.add (std::make_unique<juce::AudioParameterFloat> (
|
||||
ParameterIDs::feedback, "Feedback",
|
||||
juce::NormalisableRange<float> (0.0f, 1.0f, 0.0001f, 1.0f), 0.35f));
|
||||
|
||||
layout.add (std::make_unique<juce::AudioParameterFloat> (
|
||||
ParameterIDs::locut, "Lo-Cut",
|
||||
juce::NormalisableRange<float> (20.0f, 4000.0f, 1.0f, 0.3f), 100.0f));
|
||||
|
||||
layout.add (std::make_unique<juce::AudioParameterFloat> (
|
||||
ParameterIDs::hicut, "Hi-Cut",
|
||||
juce::NormalisableRange<float> (500.0f, 18000.0f, 1.0f, 0.3f), 12000.0f));
|
||||
|
||||
layout.add (std::make_unique<juce::AudioParameterFloat> (
|
||||
ParameterIDs::time, "Time",
|
||||
juce::NormalisableRange<float> (10.0f, 3000.0f, 1.0f, 0.4f), 300.0f));
|
||||
|
||||
layout.add (std::make_unique<juce::AudioParameterChoice> (
|
||||
ParameterIDs::timeSync, "Time (Sync)",
|
||||
juce::StringArray { "1/16", "1/8", "1/4", "1/3", "1/2", "3/4", "1", "3/2", "2", "3", "4", "8" }, 4));
|
||||
|
||||
layout.add (std::make_unique<juce::AudioParameterFloat> (
|
||||
ParameterIDs::drywet, "Dry/Wet",
|
||||
juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f, 1.0f), 0.4f));
|
||||
|
||||
layout.add (std::make_unique<juce::AudioParameterBool> (ParameterIDs::sync, "Time Sync", false));
|
||||
layout.add (std::make_unique<juce::AudioParameterChoice> (
|
||||
ParameterIDs::mode, "Mode",
|
||||
juce::StringArray { "Normal", "Invert", "Ping-Pong", "SuperPong" }, 0));
|
||||
layout.add (std::make_unique<juce::AudioParameterBool> (ParameterIDs::doubleTap, "Double", false));
|
||||
layout.add (std::make_unique<juce::AudioParameterBool> (ParameterIDs::center, "Center", false));
|
||||
layout.add (std::make_unique<juce::AudioParameterBool> (ParameterIDs::autopan, "Auto-Pan", false));
|
||||
layout.add (std::make_unique<juce::AudioParameterFloat> (
|
||||
ParameterIDs::panDepth, "Pan Depth",
|
||||
juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f, 1.0f), 1.0f));
|
||||
layout.add (std::make_unique<juce::AudioParameterBool> (ParameterIDs::bypass, "Bypass", false));
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
const juce::String MindballAudioProcessor::getName() const { return JucePlugin_Name; }
|
||||
|
||||
bool MindballAudioProcessor::acceptsMidi() const { return false; }
|
||||
bool MindballAudioProcessor::producesMidi() const { return false; }
|
||||
double MindballAudioProcessor::getTailLengthSeconds() const { return 10.0; }
|
||||
|
||||
int MindballAudioProcessor::getNumPrograms() { return 1; }
|
||||
int MindballAudioProcessor::getCurrentProgram() { return 0; }
|
||||
void MindballAudioProcessor::setCurrentProgram (int) {}
|
||||
const juce::String MindballAudioProcessor::getProgramName (int) { return {}; }
|
||||
void MindballAudioProcessor::changeProgramName (int, const juce::String&) {}
|
||||
|
||||
void MindballAudioProcessor::prepareToPlay (double sampleRate, int /*samplesPerBlock*/)
|
||||
{
|
||||
engine.prepare (sampleRate, maxDelaySeconds);
|
||||
meterDecay = static_cast<float> (std::exp (-1.0 / (0.4 * sampleRate)));
|
||||
}
|
||||
|
||||
void MindballAudioProcessor::releaseResources()
|
||||
{
|
||||
engine.clear();
|
||||
}
|
||||
|
||||
bool MindballAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
|
||||
{
|
||||
const auto mainIn = layouts.getMainInputChannelSet();
|
||||
const auto mainOut = layouts.getMainOutputChannelSet();
|
||||
|
||||
return mainIn == mainOut
|
||||
&& (mainIn == juce::AudioChannelSet::mono() || mainIn == juce::AudioChannelSet::stereo());
|
||||
}
|
||||
|
||||
void MindballAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer&)
|
||||
{
|
||||
juce::ScopedNoDenormals noDenormals;
|
||||
|
||||
const int numSamples = buffer.getNumSamples();
|
||||
const int numChannels = buffer.getNumChannels();
|
||||
|
||||
if (numSamples == 0 || numChannels == 0)
|
||||
return;
|
||||
|
||||
const bool bypass = *apvts.getRawParameterValue (ParameterIDs::bypass) > 0.5f;
|
||||
if (bypass)
|
||||
{
|
||||
if (!wasBypassed)
|
||||
{
|
||||
engine.clear();
|
||||
wasBypassed = true;
|
||||
}
|
||||
meterDryL = meterDryR = meterWetL = meterWetR = 0.0f;
|
||||
dryMeterL.store (0.0f); dryMeterR.store (0.0f);
|
||||
wetMeterL.store (0.0f); wetMeterR.store (0.0f);
|
||||
return;
|
||||
}
|
||||
wasBypassed = false;
|
||||
|
||||
double bpm = 120.0;
|
||||
if (auto* playHead = getPlayHead())
|
||||
{
|
||||
juce::AudioPlayHead::CurrentPositionInfo pos;
|
||||
if (playHead->getCurrentPosition (pos))
|
||||
bpm = pos.bpm > 0 ? pos.bpm : 120.0;
|
||||
}
|
||||
|
||||
mindball::DelayEngine::Params p;
|
||||
p.feedback = *apvts.getRawParameterValue (ParameterIDs::feedback);
|
||||
p.loCutHz = *apvts.getRawParameterValue (ParameterIDs::locut);
|
||||
p.hiCutHz = *apvts.getRawParameterValue (ParameterIDs::hicut);
|
||||
p.timeMs = *apvts.getRawParameterValue (ParameterIDs::time);
|
||||
p.timeSyncIndex = static_cast<int> (*apvts.getRawParameterValue (ParameterIDs::timeSync));
|
||||
p.dryWet = *apvts.getRawParameterValue (ParameterIDs::drywet);
|
||||
p.mode = static_cast<int> (*apvts.getRawParameterValue (ParameterIDs::mode));
|
||||
p.sync = *apvts.getRawParameterValue (ParameterIDs::sync) > 0.5f;
|
||||
p.doubleTap = *apvts.getRawParameterValue (ParameterIDs::doubleTap) > 0.5f;
|
||||
p.center = *apvts.getRawParameterValue (ParameterIDs::center) > 0.5f;
|
||||
p.autoPan = *apvts.getRawParameterValue (ParameterIDs::autopan) > 0.5f;
|
||||
p.panDepth = *apvts.getRawParameterValue (ParameterIDs::panDepth);
|
||||
p.bpm = bpm;
|
||||
|
||||
wetScratch.setSize (2, numSamples);
|
||||
|
||||
if (numChannels >= 2)
|
||||
{
|
||||
engine.process (buffer.getReadPointer (0), buffer.getReadPointer (1),
|
||||
buffer.getWritePointer (0), buffer.getWritePointer (1),
|
||||
numSamples, p,
|
||||
wetScratch.getWritePointer (0), wetScratch.getWritePointer (1));
|
||||
}
|
||||
else
|
||||
{
|
||||
engine.process (buffer.getReadPointer (0), buffer.getReadPointer (0),
|
||||
buffer.getWritePointer (0), buffer.getWritePointer (0),
|
||||
numSamples, p,
|
||||
wetScratch.getWritePointer (0), wetScratch.getWritePointer (1));
|
||||
}
|
||||
|
||||
const float* outL = buffer.getReadPointer (0);
|
||||
const float* outR = numChannels >= 2 ? buffer.getReadPointer (1) : outL;
|
||||
const float* wetL = wetScratch.getReadPointer (0);
|
||||
const float* wetR = wetScratch.getReadPointer (1);
|
||||
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
const float dryL = outL[i] - wetL[i];
|
||||
const float dryR = outR[i] - wetR[i];
|
||||
|
||||
meterDryL = std::max (std::abs (dryL), meterDryL * meterDecay);
|
||||
meterDryR = std::max (std::abs (dryR), meterDryR * meterDecay);
|
||||
meterWetL = std::max (std::abs (wetL[i]), meterWetL * meterDecay);
|
||||
meterWetR = std::max (std::abs (wetR[i]), meterWetR * meterDecay);
|
||||
}
|
||||
|
||||
dryMeterL.store (meterDryL); dryMeterR.store (meterDryR);
|
||||
wetMeterL.store (meterWetL); wetMeterR.store (meterWetR);
|
||||
}
|
||||
|
||||
juce::AudioProcessorEditor* MindballAudioProcessor::createEditor()
|
||||
{
|
||||
return new MindballAudioProcessorEditor (*this);
|
||||
}
|
||||
|
||||
bool MindballAudioProcessor::hasEditor() const { return true; }
|
||||
|
||||
void MindballAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
|
||||
{
|
||||
auto state = apvts.copyState();
|
||||
std::unique_ptr<juce::XmlElement> xml (state.createXml());
|
||||
copyXmlToBinary (*xml, destData);
|
||||
}
|
||||
|
||||
void MindballAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
|
||||
{
|
||||
std::unique_ptr<juce::XmlElement> xml (getXmlFromBinary (data, sizeInBytes));
|
||||
if (xml != nullptr)
|
||||
apvts.replaceState (juce::ValueTree::fromXml (*xml));
|
||||
}
|
||||
|
||||
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
|
||||
{
|
||||
return new MindballAudioProcessor();
|
||||
}
|
||||
75
Source/PluginProcessor.h
Normal file
75
Source/PluginProcessor.h
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
#pragma once
|
||||
|
||||
#include "DelayEngine.h"
|
||||
|
||||
#include <JuceHeader.h>
|
||||
|
||||
namespace ParameterIDs
|
||||
{
|
||||
inline constexpr const char* feedback = "feedback";
|
||||
inline constexpr const char* locut = "locut";
|
||||
inline constexpr const char* hicut = "hicut";
|
||||
inline constexpr const char* time = "time";
|
||||
inline constexpr const char* timeSync = "timeSync";
|
||||
inline constexpr const char* drywet = "drywet";
|
||||
inline constexpr const char* sync = "sync";
|
||||
inline constexpr const char* mode = "mode";
|
||||
inline constexpr const char* doubleTap = "double";
|
||||
inline constexpr const char* center = "center";
|
||||
inline constexpr const char* autopan = "autopan";
|
||||
inline constexpr const char* panDepth = "pandepth";
|
||||
inline constexpr const char* bypass = "bypass";
|
||||
}
|
||||
|
||||
class MindballAudioProcessor : public juce::AudioProcessor
|
||||
{
|
||||
public:
|
||||
MindballAudioProcessor();
|
||||
~MindballAudioProcessor() override = default;
|
||||
|
||||
juce::AudioProcessorValueTreeState apvts;
|
||||
|
||||
void prepareToPlay (double sampleRate, int samplesPerBlock) override;
|
||||
void releaseResources() override;
|
||||
|
||||
bool isBusesLayoutSupported (const BusesLayout& layouts) const override;
|
||||
|
||||
void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
|
||||
|
||||
juce::AudioProcessorEditor* createEditor() override;
|
||||
bool hasEditor() const override;
|
||||
|
||||
const juce::String getName() const override;
|
||||
bool acceptsMidi() const override;
|
||||
bool producesMidi() const override;
|
||||
double getTailLengthSeconds() const override;
|
||||
|
||||
int getNumPrograms() override;
|
||||
int getCurrentProgram() override;
|
||||
void setCurrentProgram (int index) override;
|
||||
const juce::String getProgramName (int index) override;
|
||||
void changeProgramName (int index, const juce::String& newName) override;
|
||||
|
||||
void getStateInformation (juce::MemoryBlock& destData) override;
|
||||
void setStateInformation (const void* data, int sizeInBytes) override;
|
||||
|
||||
float getDryMeterL() const { return dryMeterL.load (std::memory_order_relaxed); }
|
||||
float getDryMeterR() const { return dryMeterR.load (std::memory_order_relaxed); }
|
||||
float getWetMeterL() const { return wetMeterL.load (std::memory_order_relaxed); }
|
||||
float getWetMeterR() const { return wetMeterR.load (std::memory_order_relaxed); }
|
||||
|
||||
private:
|
||||
static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
|
||||
|
||||
static constexpr double maxDelaySeconds = 16.0;
|
||||
|
||||
mindball::DelayEngine engine;
|
||||
bool wasBypassed = false;
|
||||
|
||||
juce::AudioBuffer<float> wetScratch;
|
||||
float meterDryL = 0.0f, meterDryR = 0.0f;
|
||||
float meterWetL = 0.0f, meterWetR = 0.0f;
|
||||
float meterDecay = 0.0f;
|
||||
std::atomic<float> dryMeterL { 0.0f }, dryMeterR { 0.0f };
|
||||
std::atomic<float> wetMeterL { 0.0f }, wetMeterR { 0.0f };
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue