feat(gui): add GUI components and displays
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
#include "Display.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
void Display::paint (juce::Graphics& g)
|
||||
{
|
||||
const auto b = getLocalBounds().toFloat();
|
||||
g.setColour (juce::Colours::black.withAlpha (0.45f));
|
||||
g.fillRoundedRectangle (b, 4.0f);
|
||||
g.setColour (theme::outline);
|
||||
g.drawRoundedRectangle (b.reduced (0.5f), 4.0f, 1.0f);
|
||||
|
||||
if (title.isNotEmpty())
|
||||
{
|
||||
g.setColour (theme::textDim);
|
||||
g.setFont (juce::Font (10.0f));
|
||||
g.drawText (title, 0, 3, getWidth(), 14, juce::Justification::centred, false);
|
||||
}
|
||||
|
||||
g.setColour (theme::accent);
|
||||
g.setFont (juce::Font (14.0f, juce::Font::bold));
|
||||
g.drawText (value, 0, title.isNotEmpty() ? 16 : 0, getWidth(), getHeight() - (title.isNotEmpty() ? 16 : 0),
|
||||
juce::Justification::centred, false);
|
||||
}
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "../Resources.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
// ===========================================================================
|
||||
// LED-style text readout (e.g. preset name / master level).
|
||||
// ===========================================================================
|
||||
class Display : public juce::Component
|
||||
{
|
||||
public:
|
||||
void setTitle (const juce::String& t) { title = t; repaint(); }
|
||||
void setValue (const juce::String& v) { value = v; repaint(); }
|
||||
|
||||
void paint (juce::Graphics& g) override;
|
||||
|
||||
private:
|
||||
juce::String title, value;
|
||||
};
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,80 @@
|
||||
#include "EnvelopeDisplay.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
void EnvelopeDisplay::setParams (float a, float d, float s, float r, float c)
|
||||
{
|
||||
attack = a; decay = d; sustain = s; release = r; curve = c;
|
||||
}
|
||||
|
||||
void EnvelopeDisplay::paint (juce::Graphics& g)
|
||||
{
|
||||
const auto b = getLocalBounds().toFloat().reduced (4.0f);
|
||||
g.setColour (juce::Colours::black.withAlpha (0.4f));
|
||||
g.fillRoundedRectangle (b.expanded (4.0f), 4.0f);
|
||||
g.setColour (theme::outline);
|
||||
g.drawRoundedRectangle (b.expanded (4.0f).reduced (0.5f), 4.0f, 1.0f);
|
||||
|
||||
const float atkShape = 0.3f + curve * 2.7f;
|
||||
const float decShape = 3.0f - curve * 2.7f;
|
||||
|
||||
// Normalise durations for display (attack 0..1, decay 0..0.6, release 0..0.6).
|
||||
const float aSec = maps::toSeconds (attack);
|
||||
const float dSec = maps::toSeconds (decay);
|
||||
const float rSec = maps::toSeconds (release);
|
||||
const float total = aSec + dSec + rSec + 0.01f;
|
||||
const float ax = (aSec / total);
|
||||
const float dx = (dSec / total);
|
||||
const float rx = (rSec / total);
|
||||
|
||||
const float left = b.getX();
|
||||
const float right = b.getRight();
|
||||
const float bottom = b.getBottom();
|
||||
const float top = b.getY();
|
||||
const float sustainY = bottom - sustain * b.getHeight();
|
||||
|
||||
juce::Path path;
|
||||
path.startNewSubPath (left, bottom);
|
||||
path.lineTo (left, top);
|
||||
|
||||
// Attack (curve-shaped).
|
||||
const int steps = 48;
|
||||
const float peakX = left + ax * b.getWidth();
|
||||
for (int i = 0; i <= steps; ++i)
|
||||
{
|
||||
const float p = (float) i / steps;
|
||||
const float y = top + (bottom - top) * std::pow (p, atkShape);
|
||||
path.lineTo (left + p * (peakX - left), y);
|
||||
}
|
||||
|
||||
// Decay to sustain.
|
||||
const float decX = peakX + dx * b.getWidth();
|
||||
for (int i = 0; i <= steps; ++i)
|
||||
{
|
||||
const float p = (float) i / steps;
|
||||
const float y = sustainY + (bottom - sustainY) * std::pow (1.0f - p, decShape);
|
||||
path.lineTo (peakX + p * (decX - peakX), y);
|
||||
}
|
||||
path.lineTo (decX, sustainY);
|
||||
path.lineTo (right - rx * b.getWidth(), sustainY);
|
||||
|
||||
// Release.
|
||||
const float relStartX = right - rx * b.getWidth();
|
||||
for (int i = 0; i <= steps; ++i)
|
||||
{
|
||||
const float p = (float) i / steps;
|
||||
const float y = sustainY + (bottom - sustainY) * std::pow (1.0f - p, decShape);
|
||||
path.lineTo (relStartX + p * (right - relStartX), y);
|
||||
}
|
||||
|
||||
g.setColour (theme::accent2.withAlpha (0.3f));
|
||||
juce::Path fill = path;
|
||||
fill.lineTo (right, bottom);
|
||||
fill.closeSubPath();
|
||||
g.fillPath (fill);
|
||||
g.setColour (theme::accent2);
|
||||
g.strokePath (path, juce::PathStrokeType (1.5f));
|
||||
}
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "../Resources.h"
|
||||
#include "../Params.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
// ===========================================================================
|
||||
// ADSR curve preview.
|
||||
// ===========================================================================
|
||||
class EnvelopeDisplay : public juce::Component
|
||||
{
|
||||
public:
|
||||
void setParams (float attack, float decay, float sustain, float release, float curve);
|
||||
|
||||
void paint (juce::Graphics& g) override;
|
||||
|
||||
private:
|
||||
float attack = 0.05f, decay = 0.25f, sustain = 0.7f, release = 0.3f, curve = 0.5f;
|
||||
};
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,79 @@
|
||||
#include "FilterDisplay.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
float FilterDisplay::magnitude (float freqHz, float cutoffHz, float res, int type)
|
||||
{
|
||||
const float w = (cutoffHz > 1.0f) ? freqHz / cutoffHz : 1.0f;
|
||||
const float q = 0.6f + res * 14.0f;
|
||||
const float w2 = w * w;
|
||||
const float denom = std::sqrt ((1.0f - w2) * (1.0f - w2) + (w / q) * (w / q));
|
||||
|
||||
switch ((FilterModel) type)
|
||||
{
|
||||
case FilterModel::LadderHP: return (denom > 1e-6f) ? w2 / denom : 0.0f;
|
||||
case FilterModel::LadderBP: return (denom > 1e-6f) ? (w / q) / denom : 0.0f;
|
||||
case FilterModel::Screamer: return (denom > 1e-6f) ? (w / (q * 0.7f)) / denom : 0.0f;
|
||||
case FilterModel::Comb:
|
||||
return 0.5f + 0.5f * std::cos (2.0f * juce::MathConstants<float>::pi * freqHz / cutoffHz);
|
||||
case FilterModel::Formant:
|
||||
{
|
||||
float m = 0.0f;
|
||||
const float fc[3] = { cutoffHz * 0.5f, cutoffHz * 1.6f, cutoffHz * 3.2f };
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
const float ww = freqHz / fc[i];
|
||||
const float dd = std::sqrt ((1.0f - ww * ww) * (1.0f - ww * ww) + (ww / q) * (ww / q));
|
||||
m += (ww / q) / (dd + 1e-6f);
|
||||
}
|
||||
return m / 3.0f;
|
||||
}
|
||||
case FilterModel::Diode:
|
||||
default:
|
||||
return (denom > 1e-6f) ? 1.0f / denom : 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void FilterDisplay::setParams (int t, float c, float r, float d, int s)
|
||||
{
|
||||
type = t; cutoff = c; res = r; drive = d; slope = s;
|
||||
}
|
||||
|
||||
void FilterDisplay::paint (juce::Graphics& g)
|
||||
{
|
||||
const auto b = getLocalBounds().toFloat();
|
||||
g.setColour (juce::Colours::black.withAlpha (0.4f));
|
||||
g.fillRoundedRectangle (b, 4.0f);
|
||||
g.setColour (theme::outline);
|
||||
g.drawRoundedRectangle (b.reduced (0.5f), 4.0f, 1.0f);
|
||||
|
||||
if (! enabled)
|
||||
return;
|
||||
|
||||
const float cutoffHz = maps::cutoffToHz (cutoff);
|
||||
const int n = 300;
|
||||
juce::Path path;
|
||||
path.startNewSubPath (b.getX() + 4.0f, b.getBottom() - 4.0f);
|
||||
|
||||
for (int i = 0; i <= n; ++i)
|
||||
{
|
||||
const float logF = std::log10 (20.0f) + ((float) i / (float) n) * (std::log10 (20000.0f) - std::log10 (20.0f));
|
||||
const float freq = std::pow (10.0f, logF);
|
||||
const float mag = magnitude (freq, cutoffHz, res, type);
|
||||
const float db = juce::Decibels::gainToDecibels (mag + 1e-5f);
|
||||
const float x = b.getX() + 4.0f + ((float) i / (float) n) * (b.getWidth() - 8.0f);
|
||||
const float y = juce::jmap (db, -30.0f, 12.0f, b.getBottom() - 4.0f, b.getY() + 4.0f);
|
||||
path.lineTo (x, y);
|
||||
}
|
||||
|
||||
path.lineTo (b.getRight() - 4.0f, b.getBottom() - 4.0f);
|
||||
path.closeSubPath();
|
||||
|
||||
g.setColour (theme::accent.withAlpha (0.25f));
|
||||
g.fillPath (path);
|
||||
g.setColour (theme::accent);
|
||||
g.strokePath (path, juce::PathStrokeType (1.5f));
|
||||
}
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "../Resources.h"
|
||||
#include "../Params.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
// ===========================================================================
|
||||
// Filter frequency-response view (magnitude vs log frequency).
|
||||
// ===========================================================================
|
||||
class FilterDisplay : public juce::Component
|
||||
{
|
||||
public:
|
||||
void setParams (int type, float cutoff, float res, float drive, int slope);
|
||||
void setEnabled (bool e) { enabled = e; }
|
||||
|
||||
void paint (juce::Graphics& g) override;
|
||||
|
||||
static float magnitude (float freqHz, float cutoffHz, float res, int type);
|
||||
|
||||
private:
|
||||
int type = 0;
|
||||
float cutoff = 0.5f, res = 0.0f, drive = 0.0f;
|
||||
int slope = 2;
|
||||
bool enabled = true;
|
||||
};
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,24 @@
|
||||
#include "Knob.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
Knob::Knob (const juce::String& name, std::function<juce::String (float)> fmt)
|
||||
: formatter (std::move (fmt))
|
||||
{
|
||||
setSliderStyle (juce::Slider::RotaryVerticalDrag);
|
||||
setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0);
|
||||
setRange (0.0, 1.0);
|
||||
setDoubleClickReturnValue (true, 0.5);
|
||||
setName (name);
|
||||
setComponentID (name);
|
||||
}
|
||||
|
||||
juce::String Knob::getTextFromValue (double value)
|
||||
{
|
||||
if (formatter)
|
||||
return formatter ((float) value);
|
||||
return formatPercent ((float) value);
|
||||
}
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "../Resources.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
// ===========================================================================
|
||||
// Rotary knob — a juce::Slider styled by SerumLookAndFeel. The value readout is
|
||||
// formatted by a user-provided function via getTextFromValue().
|
||||
// ===========================================================================
|
||||
class Knob : public juce::Slider
|
||||
{
|
||||
public:
|
||||
explicit Knob (const juce::String& name,
|
||||
std::function<juce::String (float)> formatter = {});
|
||||
|
||||
void setFormatter (std::function<juce::String (float)> f) { formatter = std::move (f); }
|
||||
|
||||
juce::String getTextFromValue (double value) override;
|
||||
|
||||
private:
|
||||
std::function<juce::String (float)> formatter;
|
||||
};
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,128 @@
|
||||
#include "LFODisplay.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
void LFODisplay::setShapeData (const std::vector<float>& data, int s)
|
||||
{
|
||||
steps = juce::jlimit (2, 64, s);
|
||||
shapeData = data;
|
||||
if ((int) shapeData.size() < 64)
|
||||
shapeData.resize (64, 0.0f);
|
||||
repaint();
|
||||
}
|
||||
|
||||
float LFODisplay::valueAt (float phase) const noexcept
|
||||
{
|
||||
switch ((LfoShape) shape)
|
||||
{
|
||||
case LfoShape::Sine: return std::sin (phase * 6.2831853f);
|
||||
case LfoShape::Triangle: return 1.0f - 4.0f * std::abs (phase - 0.5f);
|
||||
case LfoShape::Saw: return 2.0f * phase - 1.0f;
|
||||
case LfoShape::Square: return (phase < 0.5f) ? 1.0f : -1.0f;
|
||||
case LfoShape::SampleHold:
|
||||
{
|
||||
const int step = juce::jlimit (0, 15, (int) (phase * 16.0f));
|
||||
return ((step * 2654435761u) & 0xffffu) / 32768.0f - 1.0f;
|
||||
}
|
||||
case LfoShape::StepSeq:
|
||||
{
|
||||
const int idx = juce::jlimit (0, steps - 1, (int) (phase * steps));
|
||||
return shapeData.empty() ? 0.0f : shapeData[(size_t) idx];
|
||||
}
|
||||
case LfoShape::Freehand:
|
||||
{
|
||||
if (shapeData.empty()) return 0.0f;
|
||||
const float pos = phase * (float) (steps - 1);
|
||||
const int i0 = (int) pos;
|
||||
const int i1 = juce::jmin (i0 + 1, steps - 1);
|
||||
const float frac = pos - (float) i0;
|
||||
return shapeData[(size_t) i0] * (1.0f - frac) + shapeData[(size_t) i1] * frac;
|
||||
}
|
||||
default: return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void LFODisplay::paint (juce::Graphics& g)
|
||||
{
|
||||
const auto b = getLocalBounds().toFloat();
|
||||
g.setColour (juce::Colours::black.withAlpha (0.4f));
|
||||
g.fillRoundedRectangle (b, 4.0f);
|
||||
g.setColour (theme::outline);
|
||||
g.drawRoundedRectangle (b.reduced (0.5f), 4.0f, 1.0f);
|
||||
|
||||
const float midY = b.getCentreY();
|
||||
g.setColour (theme::outline);
|
||||
g.drawLine (b.getX() + 4.0f, midY, b.getRight() - 4.0f, midY, 1.0f);
|
||||
|
||||
// Editable shapes show step markers.
|
||||
if (shape == (int) LfoShape::StepSeq)
|
||||
{
|
||||
const float stepW = (b.getWidth() - 8.0f) / (float) steps;
|
||||
for (int i = 0; i < steps; ++i)
|
||||
{
|
||||
const float v = shapeData.empty() ? 0.0f : shapeData[(size_t) i];
|
||||
const float x = b.getX() + 4.0f + stepW * i;
|
||||
const float y = midY - v * (b.getHeight() * 0.42f);
|
||||
g.setColour (theme::amber);
|
||||
g.fillRect (x + 1.0f, juce::jmin (y, midY), stepW - 2.0f, std::abs (midY - y));
|
||||
}
|
||||
}
|
||||
|
||||
juce::Path path;
|
||||
path.startNewSubPath (b.getX() + 4.0f, midY);
|
||||
const int n = 256;
|
||||
for (int i = 0; i <= n; ++i)
|
||||
{
|
||||
const float phase = (float) i / (float) n;
|
||||
const float v = valueAt (phase);
|
||||
const float x = b.getX() + 4.0f + phase * (b.getWidth() - 8.0f);
|
||||
const float y = midY - v * (b.getHeight() * 0.42f);
|
||||
if (i == 0) path.startNewSubPath (x, y);
|
||||
else path.lineTo (x, y);
|
||||
}
|
||||
g.setColour (theme::green);
|
||||
g.strokePath (path, juce::PathStrokeType (1.5f));
|
||||
}
|
||||
|
||||
void LFODisplay::editAt (float x, float y)
|
||||
{
|
||||
const auto b = getLocalBounds().toFloat();
|
||||
const float midY = b.getCentreY();
|
||||
const float normX = juce::jlimit (0.0f, 1.0f, (x - b.getX() - 4.0f) / (b.getWidth() - 8.0f));
|
||||
const float v = juce::jlimit (-1.0f, 1.0f, (midY - y) / (b.getHeight() * 0.42f));
|
||||
|
||||
if (shapeData.empty())
|
||||
shapeData.assign (64, 0.0f);
|
||||
|
||||
if (shape == (int) LfoShape::StepSeq)
|
||||
{
|
||||
const int idx = juce::jlimit (0, steps - 1, (int) (normX * steps));
|
||||
shapeData[(size_t) idx] = v;
|
||||
}
|
||||
else if (shape == (int) LfoShape::Freehand)
|
||||
{
|
||||
const int idx = juce::jlimit (0, steps - 1, (int) (normX * steps));
|
||||
shapeData[(size_t) idx] = v;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (onEdited)
|
||||
onEdited (shapeData, steps);
|
||||
repaint();
|
||||
}
|
||||
|
||||
void LFODisplay::mouseDown (const juce::MouseEvent& e)
|
||||
{
|
||||
editAt (e.x, e.y);
|
||||
}
|
||||
|
||||
void LFODisplay::mouseDrag (const juce::MouseEvent& e)
|
||||
{
|
||||
editAt (e.x, e.y);
|
||||
}
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "../Resources.h"
|
||||
#include "../Params.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
// ===========================================================================
|
||||
// LFO shape view. Step-sequencer and freehand shapes are click/drag editable.
|
||||
// ===========================================================================
|
||||
class LFODisplay : public juce::Component
|
||||
{
|
||||
public:
|
||||
void setShape (int s) { shape = s; repaint(); }
|
||||
void setShapeData (const std::vector<float>& data, int steps);
|
||||
void setOnShapeEdited (std::function<void (const std::vector<float>&, int)> cb) { onEdited = std::move (cb); }
|
||||
|
||||
void paint (juce::Graphics& g) override;
|
||||
void mouseDown (const juce::MouseEvent& e) override;
|
||||
void mouseDrag (const juce::MouseEvent& e) override;
|
||||
|
||||
private:
|
||||
int shape = 0;
|
||||
std::vector<float> shapeData;
|
||||
int steps = 16;
|
||||
std::function<void (const std::vector<float>&, int)> onEdited;
|
||||
|
||||
float valueAt (float phase) const noexcept;
|
||||
void editAt (float x, float y);
|
||||
};
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "Panel.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
Panel::Panel (const juce::String& t) : title (t)
|
||||
{
|
||||
}
|
||||
|
||||
void Panel::paint (juce::Graphics& g)
|
||||
{
|
||||
const auto b = getLocalBounds().toFloat();
|
||||
g.setColour (theme::panel);
|
||||
g.fillRoundedRectangle (b, 8.0f);
|
||||
g.setColour (theme::outline);
|
||||
g.drawRoundedRectangle (b.reduced (0.5f), 8.0f, 1.0f);
|
||||
|
||||
if (title.isNotEmpty())
|
||||
{
|
||||
g.setColour (theme::textDim);
|
||||
g.setFont (juce::Font (11.0f, juce::Font::bold));
|
||||
g.drawText (title, 10, 6, getWidth() - 20, 16, juce::Justification::left, false);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "../Resources.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
// ===========================================================================
|
||||
// Rounded dark panel with an optional title bar.
|
||||
// ===========================================================================
|
||||
class Panel : public juce::Component
|
||||
{
|
||||
public:
|
||||
explicit Panel (const juce::String& title = {});
|
||||
|
||||
void paint (juce::Graphics& g) override;
|
||||
|
||||
private:
|
||||
juce::String title;
|
||||
};
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,156 @@
|
||||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "../Resources.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
// ===========================================================================
|
||||
// Dark vector look-and-feel. Rotary and linear sliders are drawn with juce::Path
|
||||
// (arcs + indicator + thumb) so they stay crisp at every UI scale.
|
||||
// ===========================================================================
|
||||
class SerumLookAndFeel : public juce::LookAndFeel_V4
|
||||
{
|
||||
public:
|
||||
SerumLookAndFeel()
|
||||
{
|
||||
setColour (juce::Slider::backgroundColourId, theme::panel);
|
||||
setColour (juce::Slider::thumbColourId, theme::accent);
|
||||
setColour (juce::Slider::trackColourId, theme::outline);
|
||||
setColour (juce::Slider::rotarySliderFillColourId, theme::accent);
|
||||
setColour (juce::Slider::rotarySliderOutlineColourId, theme::panelRaised);
|
||||
setColour (juce::Slider::textBoxTextColourId, theme::text);
|
||||
setColour (juce::Slider::textBoxBackgroundColourId, theme::panel);
|
||||
setColour (juce::Slider::textBoxHighlightColourId, theme::accent);
|
||||
setColour (juce::Slider::textBoxOutlineColourId, theme::outline);
|
||||
|
||||
setColour (juce::ComboBox::backgroundColourId, theme::panelRaised);
|
||||
setColour (juce::ComboBox::textColourId, theme::text);
|
||||
setColour (juce::ComboBox::arrowColourId, theme::textDim);
|
||||
setColour (juce::ComboBox::outlineColourId, theme::outline);
|
||||
setColour (juce::ComboBox::buttonColourId, theme::panelRaised);
|
||||
setColour (juce::ComboBox::focusedOutlineColourId, theme::accent);
|
||||
setColour (juce::PopupMenu::backgroundColourId, theme::panelRaised);
|
||||
setColour (juce::PopupMenu::textColourId, theme::text);
|
||||
setColour (juce::PopupMenu::highlightedBackgroundColourId, theme::accent);
|
||||
setColour (juce::PopupMenu::highlightedTextColourId, juce::Colours::black);
|
||||
|
||||
setColour (juce::TextButton::buttonColourId, theme::panelRaised);
|
||||
setColour (juce::TextButton::buttonOnColourId, theme::accent);
|
||||
setColour (juce::TextButton::textColourOffId, theme::textDim);
|
||||
setColour (juce::TextButton::textColourOnId, juce::Colours::black);
|
||||
|
||||
setColour (juce::TextEditor::backgroundColourId, theme::panel);
|
||||
setColour (juce::TextEditor::textColourId, theme::text);
|
||||
setColour (juce::TextEditor::outlineColourId, theme::outline);
|
||||
setColour (juce::TextEditor::focusedOutlineColourId, theme::accent);
|
||||
}
|
||||
|
||||
void drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
|
||||
float sliderPos, float, float, juce::Slider& slider) override
|
||||
{
|
||||
const int labelH = 18;
|
||||
const int knobH = height - labelH;
|
||||
const int knobSize = juce::jmin (width, knobH) - 6;
|
||||
const int cx = x + width / 2;
|
||||
const int cy = y + knobH / 2;
|
||||
const float radius = knobSize * 0.5f;
|
||||
|
||||
const juce::Rectangle<float> area ((float) cx - radius, (float) cy - radius,
|
||||
radius * 2.0f, radius * 2.0f);
|
||||
|
||||
// Body.
|
||||
g.setColour (theme::panelRaised);
|
||||
g.fillEllipse (area);
|
||||
g.setColour (theme::outline);
|
||||
g.drawEllipse (area, 1.0f);
|
||||
|
||||
const float startAngle = juce::MathConstants<float>::pi * 1.25f; // 225 deg
|
||||
const float endAngle = juce::MathConstants<float>::pi * -0.25f;
|
||||
const float valueAngle = startAngle + sliderPos * (endAngle - startAngle);
|
||||
|
||||
// Track arc.
|
||||
juce::Path track;
|
||||
track.addCentredArc ((float) cx, (float) cy, radius - 4.0f, radius - 4.0f,
|
||||
0.0f, startAngle, endAngle, true);
|
||||
g.setColour (theme::outline);
|
||||
g.strokePath (track, juce::PathStrokeType (3.0f, juce::PathStrokeType::curved));
|
||||
|
||||
// Value arc.
|
||||
juce::Path valueArc;
|
||||
valueArc.addCentredArc ((float) cx, (float) cy, radius - 4.0f, radius - 4.0f,
|
||||
0.0f, startAngle, valueAngle, true);
|
||||
g.setColour (theme::accent);
|
||||
g.strokePath (valueArc, juce::PathStrokeType (3.0f, juce::PathStrokeType::curved));
|
||||
|
||||
// Indicator.
|
||||
const float indX = (float) cx + (radius - 9.0f) * std::cos (valueAngle);
|
||||
const float indY = (float) cy + (radius - 9.0f) * std::sin (valueAngle);
|
||||
g.setColour (theme::text);
|
||||
g.drawLine ((float) cx, (float) cy, indX, indY, 2.0f);
|
||||
g.setColour (theme::text);
|
||||
g.fillEllipse (indX - 2.0f, indY - 2.0f, 4.0f, 4.0f);
|
||||
|
||||
// Value readout.
|
||||
g.setColour (theme::text);
|
||||
g.setFont (juce::Font (10.0f, juce::Font::bold));
|
||||
g.drawText (slider.getTextFromValue (slider.getValue()),
|
||||
juce::Rectangle<int> (x, y, width, knobH - (int) radius + 8),
|
||||
juce::Justification::centred, false);
|
||||
|
||||
// Name label.
|
||||
g.setColour (theme::textDim);
|
||||
g.setFont (juce::Font (11.0f));
|
||||
g.drawText (slider.getName(), juce::Rectangle<int> (x, y + knobH, width, labelH),
|
||||
juce::Justification::centred, false);
|
||||
}
|
||||
|
||||
void drawLinearSlider (juce::Graphics& g, int x, int y, int width, int height,
|
||||
float sliderPos, float, float, juce::Slider::SliderStyle, juce::Slider& slider) override
|
||||
{
|
||||
const bool vertical = height > width;
|
||||
juce::Rectangle<int> track;
|
||||
|
||||
if (vertical)
|
||||
{
|
||||
track = juce::Rectangle<int> (x + width / 2 - 3, y + 8, 6, height - 16);
|
||||
}
|
||||
else
|
||||
{
|
||||
track = juce::Rectangle<int> (x + 8, y + height / 2 - 3, width - 16, 6);
|
||||
}
|
||||
|
||||
g.setColour (theme::outline);
|
||||
g.fillRoundedRectangle (track.toFloat(), 3.0f);
|
||||
|
||||
juce::Rectangle<float> fill;
|
||||
if (vertical)
|
||||
{
|
||||
const float h = track.getHeight() * sliderPos;
|
||||
fill = juce::Rectangle<float> ((float) track.getX(), (float) track.getBottom() - h,
|
||||
(float) track.getWidth(), h);
|
||||
}
|
||||
else
|
||||
{
|
||||
fill = juce::Rectangle<float> ((float) track.getX(), (float) track.getY(),
|
||||
track.getWidth() * sliderPos, (float) track.getHeight());
|
||||
}
|
||||
g.setColour (theme::accent);
|
||||
g.fillRoundedRectangle (fill, 3.0f);
|
||||
|
||||
// Thumb.
|
||||
const float thumbC = vertical ? (track.getBottom() - track.getHeight() * sliderPos)
|
||||
: (track.getX() + track.getWidth() * sliderPos);
|
||||
juce::Point<float> thumb (vertical ? (float) track.getCentreX() : thumbC,
|
||||
vertical ? thumbC : (float) track.getCentreY());
|
||||
g.setColour (theme::text);
|
||||
g.fillEllipse (thumb.x - 6.0f, thumb.y - 6.0f, 12.0f, 12.0f);
|
||||
|
||||
g.setColour (theme::textDim);
|
||||
g.setFont (juce::Font (10.0f));
|
||||
g.drawText (slider.getName(), x, y, width, height, juce::Justification::bottomLeft, false);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,13 @@
|
||||
#include "Slider.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
Slider::Slider (const juce::String& name, std::function<juce::String (float)> formatter)
|
||||
: Knob (name, std::move (formatter))
|
||||
{
|
||||
setSliderStyle (juce::Slider::LinearHorizontal);
|
||||
setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0);
|
||||
}
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "Knob.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
// ===========================================================================
|
||||
// Horizontal slider (same formatting machinery as Knob).
|
||||
// ===========================================================================
|
||||
class Slider : public Knob
|
||||
{
|
||||
public:
|
||||
explicit Slider (const juce::String& name,
|
||||
std::function<juce::String (float)> formatter = {});
|
||||
};
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,71 @@
|
||||
#include "ToggleButton.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
ToggleButton::ToggleButton (const juce::String& lbl) : label (lbl)
|
||||
{
|
||||
setRepaintsOnMouseActivity (true);
|
||||
}
|
||||
|
||||
void ToggleButton::attach (juce::AudioProcessorValueTreeState& apvts, const juce::String& paramId)
|
||||
{
|
||||
param = apvts.getParameter (paramId);
|
||||
if (param != nullptr)
|
||||
{
|
||||
state = param->getValue() > 0.5f;
|
||||
attachment = std::make_unique<juce::ParameterAttachment> (*param,
|
||||
[this] (float newValue) { setToggleState (newValue > 0.5f); });
|
||||
}
|
||||
}
|
||||
|
||||
void ToggleButton::setToggleState (bool s)
|
||||
{
|
||||
if (state == s)
|
||||
return;
|
||||
state = s;
|
||||
repaint();
|
||||
}
|
||||
|
||||
void ToggleButton::paint (juce::Graphics& g)
|
||||
{
|
||||
const auto bounds = getLocalBounds().toFloat();
|
||||
const float ledSize = juce::jmin (bounds.getHeight() - 16.0f, 16.0f);
|
||||
const juce::Rectangle<float> led ((bounds.getWidth() - ledSize) * 0.5f, 4.0f, ledSize, ledSize);
|
||||
|
||||
if (state)
|
||||
{
|
||||
g.setColour (onColour.withAlpha (0.35f));
|
||||
g.fillEllipse (led.expanded (6.0f));
|
||||
g.setColour (onColour);
|
||||
}
|
||||
else
|
||||
{
|
||||
g.setColour (theme::outline);
|
||||
}
|
||||
g.fillEllipse (led);
|
||||
g.setColour (state ? juce::Colours::black : theme::textDim);
|
||||
g.drawEllipse (led, 1.0f);
|
||||
|
||||
g.setColour (state ? theme::text : theme::textDim);
|
||||
g.setFont (juce::Font (11.0f));
|
||||
g.drawText (label, 0, (int) (led.getBottom() + 2), getWidth(), 14, juce::Justification::centred, false);
|
||||
}
|
||||
|
||||
void ToggleButton::mouseDown (const juce::MouseEvent&)
|
||||
{
|
||||
const bool newState = ! state;
|
||||
|
||||
if (onClick)
|
||||
{
|
||||
onClick (newState);
|
||||
}
|
||||
else if (param != nullptr)
|
||||
{
|
||||
param->setValueNotifyingHost (newState ? 1.0f : 0.0f);
|
||||
}
|
||||
|
||||
setToggleState (newState);
|
||||
}
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "../Resources.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
// ===========================================================================
|
||||
// LED-style toggle (power) button that drives a float parameter (0/1) or an
|
||||
// optional callback (used by RAVE).
|
||||
// ===========================================================================
|
||||
class ToggleButton : public juce::Component
|
||||
{
|
||||
public:
|
||||
explicit ToggleButton (const juce::String& label = {});
|
||||
|
||||
void attach (juce::AudioProcessorValueTreeState& apvts, const juce::String& paramId);
|
||||
void setOnClick (std::function<void (bool)> cb) { onClick = std::move (cb); }
|
||||
|
||||
void setOnColour (juce::Colour c) { onColour = c; repaint(); }
|
||||
void setLabel (const juce::String& lbl) { label = lbl; repaint(); }
|
||||
|
||||
void setToggleState (bool s);
|
||||
bool getToggleState() const noexcept { return state; }
|
||||
|
||||
void paint (juce::Graphics& g) override;
|
||||
void mouseDown (const juce::MouseEvent&) override;
|
||||
|
||||
private:
|
||||
bool state = false;
|
||||
juce::String label;
|
||||
juce::RangedAudioParameter* param = nullptr;
|
||||
std::unique_ptr<juce::ParameterAttachment> attachment;
|
||||
std::function<void (bool)> onClick;
|
||||
juce::Colour onColour = theme::accent;
|
||||
};
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "WaveformDisplay.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
void WaveformDisplay::paint (juce::Graphics& g)
|
||||
{
|
||||
const auto b = getLocalBounds().toFloat();
|
||||
g.setColour (juce::Colours::black.withAlpha (0.4f));
|
||||
g.fillRoundedRectangle (b, 4.0f);
|
||||
g.setColour (theme::outline);
|
||||
g.drawRoundedRectangle (b.reduced (0.5f), 4.0f, 1.0f);
|
||||
|
||||
const float midY = b.getCentreY();
|
||||
g.setColour (theme::outline);
|
||||
g.drawLine (b.getX() + 4.0f, midY, b.getRight() - 4.0f, midY, 1.0f);
|
||||
|
||||
if (wtLib == nullptr || ! enabled)
|
||||
return;
|
||||
|
||||
const Wavetable& wt = wtLib->getTable (wave);
|
||||
if (! wt.isValid())
|
||||
return;
|
||||
|
||||
const int n = 512;
|
||||
juce::Path path;
|
||||
path.startNewSubPath (b.getX() + 4.0f, midY);
|
||||
|
||||
for (int i = 0; i <= n; ++i)
|
||||
{
|
||||
const float phase = (float) i / (float) n;
|
||||
const float sample = wt.readSafe (wtPos * 255.0f, phase);
|
||||
const float x = b.getX() + 4.0f + ((float) i / (float) n) * (b.getWidth() - 8.0f);
|
||||
const float y = midY - sample * (b.getHeight() * 0.42f);
|
||||
if (i == 0)
|
||||
path.startNewSubPath (x, y);
|
||||
else
|
||||
path.lineTo (x, y);
|
||||
}
|
||||
|
||||
g.setColour (theme::accent);
|
||||
g.strokePath (path, juce::PathStrokeType (1.5f));
|
||||
}
|
||||
|
||||
} // namespace serum
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "../Resources.h"
|
||||
#include "../Wavetable.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
// ===========================================================================
|
||||
// Single-cycle wavetable view (morphs with the WT position knob).
|
||||
// ===========================================================================
|
||||
class WaveformDisplay : public juce::Component
|
||||
{
|
||||
public:
|
||||
void setWavetables (const WavetableLibrary* lib) { wtLib = lib; }
|
||||
void setWaveIndex (int index) { wave = index; }
|
||||
void setFramePosition (float pos) { wtPos = pos; }
|
||||
void setEnabled (bool e) { enabled = e; }
|
||||
|
||||
void paint (juce::Graphics& g) override;
|
||||
|
||||
private:
|
||||
const WavetableLibrary* wtLib = nullptr;
|
||||
int wave = 0;
|
||||
float wtPos = 0.0f;
|
||||
bool enabled = true;
|
||||
};
|
||||
|
||||
} // namespace serum
|
||||
Reference in New Issue
Block a user