mirror of
https://codeberg.org/armin/ambivalence.git
synced 2026-09-01 04:10:48 +02:00
init
This commit is contained in:
parent
29c860587e
commit
42cf8432bf
57 changed files with 5782 additions and 0 deletions
354
Source/GUI/AmbivalenceUI.cpp
Normal file
354
Source/GUI/AmbivalenceUI.cpp
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
#include "AmbivalenceUI.h"
|
||||
#include "../PluginProcessor.h"
|
||||
|
||||
// --- AmbivalenceLookAndFeel ---------------------------------------------
|
||||
AmbivalenceLookAndFeel::AmbivalenceLookAndFeel()
|
||||
{
|
||||
setColour(juce::Slider::backgroundColourId, AmbivalenceColors::ArcTrack);
|
||||
setColour(juce::Slider::thumbColourId, AmbivalenceColors::Accent);
|
||||
setColour(juce::Slider::trackColourId, AmbivalenceColors::ArcFill);
|
||||
setColour(juce::Label::textColourId, AmbivalenceColors::TextSecondary);
|
||||
setColour(juce::ComboBox::backgroundColourId, AmbivalenceColors::Surface);
|
||||
setColour(juce::ComboBox::textColourId, AmbivalenceColors::TextPrimary);
|
||||
setColour(juce::ComboBox::outlineColourId, AmbivalenceColors::Border);
|
||||
mainFont = juce::Font(juce::FontOptions("Helvetica Neue", 11.f, juce::Font::plain));
|
||||
}
|
||||
|
||||
void AmbivalenceLookAndFeel::drawRotarySlider(juce::Graphics& g,
|
||||
int x, int y, int w, int h,
|
||||
float sliderPos, float startAngle, float endAngle, juce::Slider&)
|
||||
{
|
||||
auto b = juce::Rectangle<float>((float)x, (float)y, (float)w, (float)h).reduced(4.f);
|
||||
float cx = b.getCentreX(), cy = b.getCentreY();
|
||||
float r = juce::jmin(b.getWidth(), b.getHeight()) * 0.45f;
|
||||
float th = r * 0.22f;
|
||||
|
||||
juce::Path track;
|
||||
track.addCentredArc(cx, cy, r, r, 0.f, startAngle, endAngle, true);
|
||||
g.setColour(AmbivalenceColors::ArcTrack);
|
||||
g.strokePath(track, juce::PathStrokeType(th,
|
||||
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
|
||||
|
||||
float angle = startAngle + sliderPos * (endAngle - startAngle);
|
||||
juce::Path fill;
|
||||
fill.addCentredArc(cx, cy, r, r, 0.f, startAngle, angle, true);
|
||||
juce::ColourGradient grad(AmbivalenceColors::AccentBlue, cx - r, cy,
|
||||
AmbivalenceColors::Accent, cx + r, cy, false);
|
||||
g.setGradientFill(grad);
|
||||
g.strokePath(fill, juce::PathStrokeType(th,
|
||||
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
|
||||
|
||||
g.setColour(AmbivalenceColors::Panel);
|
||||
g.fillEllipse(cx - r * 0.28f, cy - r * 0.28f, r * 0.56f, r * 0.56f);
|
||||
|
||||
float ix = cx + r * 0.6f * std::sin(angle);
|
||||
float iy = cy - r * 0.6f * std::cos(angle);
|
||||
g.setColour(AmbivalenceColors::TextPrimary);
|
||||
g.drawLine(cx, cy, ix, iy, 2.f);
|
||||
}
|
||||
|
||||
void AmbivalenceLookAndFeel::drawLinearSlider(juce::Graphics& g,
|
||||
int x, int y, int w, int h,
|
||||
float sliderPos, float, float, juce::Slider::SliderStyle, juce::Slider&)
|
||||
{
|
||||
auto b = juce::Rectangle<int>(x, y, w, h).toFloat();
|
||||
float ty = b.getCentreY() - 2.f;
|
||||
g.setColour(AmbivalenceColors::ArcTrack);
|
||||
g.fillRoundedRectangle(b.getX(), ty, b.getWidth(), 4.f, 2.f);
|
||||
g.setColour(AmbivalenceColors::Accent);
|
||||
g.fillRoundedRectangle(b.getX(), ty, sliderPos - b.getX(), 4.f, 2.f);
|
||||
float r = 7.f;
|
||||
g.setColour(AmbivalenceColors::TextPrimary);
|
||||
g.fillEllipse(sliderPos - r, b.getCentreY() - r, r * 2.f, r * 2.f);
|
||||
}
|
||||
|
||||
void AmbivalenceLookAndFeel::drawComboBox(juce::Graphics& g,
|
||||
int w, int h, bool isDown, int, int, int, int, juce::ComboBox&)
|
||||
{
|
||||
auto b = juce::Rectangle<int>(0, 0, w, h).toFloat();
|
||||
g.setColour(isDown ? AmbivalenceColors::Panel : AmbivalenceColors::Surface);
|
||||
g.fillRoundedRectangle(b, 3.f);
|
||||
g.setColour(AmbivalenceColors::Border);
|
||||
g.drawRoundedRectangle(b.reduced(0.5f), 3.f, 1.f);
|
||||
juce::Path arrow;
|
||||
arrow.addTriangle(w - 16.f, h * 0.5f - 3.f,
|
||||
w - 8.f, h * 0.5f - 3.f,
|
||||
w - 12.f, h * 0.5f + 3.f);
|
||||
g.setColour(AmbivalenceColors::TextSecondary);
|
||||
g.fillPath(arrow);
|
||||
}
|
||||
|
||||
void AmbivalenceLookAndFeel::positionComboBoxText(juce::ComboBox& box, juce::Label& label) {
|
||||
label.setBounds(6, 1, box.getWidth() - 22, box.getHeight() - 2);
|
||||
label.setFont(getComboBoxFont(box));
|
||||
}
|
||||
|
||||
juce::Font AmbivalenceLookAndFeel::getLabelFont(juce::Label&) { return mainFont.withHeight(10.f); }
|
||||
juce::Font AmbivalenceLookAndFeel::getComboBoxFont(juce::ComboBox&) { return mainFont.withHeight(11.f); }
|
||||
|
||||
void AmbivalenceLookAndFeel::drawGroupComponentOutline(juce::Graphics& g,
|
||||
int w, int h, const juce::String& text,
|
||||
const juce::Justification&, juce::GroupComponent&)
|
||||
{
|
||||
float textH = 12.f, indent = 8.f, yOff = textH * 0.5f;
|
||||
juce::Path p;
|
||||
p.startNewSubPath(indent + 4.f, yOff); p.lineTo(indent, yOff);
|
||||
p.lineTo(indent, (float)h - 1.f);
|
||||
p.lineTo((float)w - indent, (float)h - 1.f);
|
||||
p.lineTo((float)w - indent, yOff);
|
||||
|
||||
// * redraw after changes
|
||||
juce::GlyphArrangement ga;
|
||||
ga.addLineOfText(mainFont.withHeight(textH), text, 0.f, 0.f);
|
||||
float tw = ga.getBoundingBox(0, -1, true).getWidth() + 6.f;
|
||||
|
||||
p.lineTo(indent + 14.f + tw, yOff);
|
||||
g.setColour(AmbivalenceColors::Border);
|
||||
g.strokePath(p, juce::PathStrokeType(1.f));
|
||||
g.setColour(AmbivalenceColors::TextSecondary);
|
||||
g.setFont(mainFont.withHeight(textH).boldened());
|
||||
g.drawText(text, (int)(indent + 14.f), 0, (int)tw, (int)textH,
|
||||
juce::Justification::centredLeft);
|
||||
}
|
||||
|
||||
// --- RT60Visualizer --------------------------------------------------
|
||||
RT60Visualizer::RT60Visualizer() {
|
||||
displayRT60.fill(1.0f);
|
||||
startTimerHz(30);
|
||||
}
|
||||
RT60Visualizer::~RT60Visualizer() { stopTimer(); }
|
||||
|
||||
void RT60Visualizer::timerCallback() {
|
||||
if (!processor) return;
|
||||
|
||||
auto live = processor->getRT60ForDisplay();
|
||||
for (int i = 0; i < FDNReverb::NUM_BANDS; ++i)
|
||||
displayRT60[i] += 0.25f * (live[i] - displayRT60[i]);
|
||||
|
||||
// * dynamic Y axis above : current maximum RT60 value x 1.3 smoothly
|
||||
float maxVal = *std::max_element(displayRT60.begin(), displayRT60.end());
|
||||
float targetMax = std::max(MAX_RT60_DISPLAY_FLOOR, maxVal * 1.3f);
|
||||
// exponential ( rise quickly , fall gentle -> frequently )
|
||||
float smoothFactor = (targetMax > dynamicMaxRT60) ? 0.15f : 0.03f;
|
||||
dynamicMaxRT60 += smoothFactor * (targetMax - dynamicMaxRT60);
|
||||
|
||||
repaint();
|
||||
}
|
||||
|
||||
void RT60Visualizer::paint(juce::Graphics& g)
|
||||
{
|
||||
auto b = getLocalBounds().toFloat().reduced(2.f);
|
||||
float W = b.getWidth(), H = b.getHeight();
|
||||
float x0 = b.getX(), y0 = b.getY();
|
||||
|
||||
g.setColour(AmbivalenceColors::Surface);
|
||||
g.fillRoundedRectangle(b, 4.f);
|
||||
g.setColour(AmbivalenceColors::Border);
|
||||
g.drawRoundedRectangle(b.reduced(0.5f), 4.f, 1.f);
|
||||
|
||||
// * dynamic Y axis
|
||||
float logMin = std::log10(MIN_RT60_DISPLAY);
|
||||
float logMax = std::log10(dynamicMaxRT60);
|
||||
|
||||
// grid value dynamic Y axis
|
||||
// fixed value dynamicMaxRT60 below drawing
|
||||
static constexpr float kAllGridVals[] = {
|
||||
0.1f, 0.3f, 0.5f, 1.0f, 2.0f, 4.0f,
|
||||
8.0f, 12.0f, 16.0f, 20.0f
|
||||
};
|
||||
|
||||
// grid
|
||||
g.setColour(AmbivalenceColors::Separator);
|
||||
for (float v : kAllGridVals) {
|
||||
if (v > dynamicMaxRT60 * 1.05f) break;
|
||||
float ny = 1.f - (std::log10(v) - logMin) / (logMax - logMin);
|
||||
g.drawHorizontalLine((int)(y0 + ny * H), x0 + 36.f, x0 + W - 4.f);
|
||||
}
|
||||
|
||||
// frequency label (X axis )
|
||||
g.setFont(8.5f);
|
||||
g.setColour(AmbivalenceColors::TextSecondary);
|
||||
static const char* fLbls[] = {
|
||||
"31","63","125","250","500","1k","2k","4k","8k","16k"
|
||||
};
|
||||
for (int i = 0; i < FDNReverb::NUM_BANDS; ++i) {
|
||||
float px = x0 + 36.f + (float)i / (FDNReverb::NUM_BANDS - 1) * (W - 40.f);
|
||||
g.drawText(fLbls[i], (int)(px - 12.f), (int)(y0 + H - 14.f),
|
||||
24, 13, juce::Justification::centred);
|
||||
}
|
||||
|
||||
// seconds label (Y axis ) - dynamic
|
||||
for (float v : kAllGridVals) {
|
||||
if (v > dynamicMaxRT60 * 1.05f) break;
|
||||
float ny = 1.f - (std::log10(v) - logMin) / (logMax - logMin);
|
||||
float py = y0 + ny * H;
|
||||
juce::String lbl = (v < 1.f)
|
||||
? juce::String(v, 1) + "s"
|
||||
: (v < 10.f ? juce::String(v, 1) : juce::String((int)v)) + "s";
|
||||
g.drawText(lbl, (int)(x0 + 2.f), (int)(py - 7.f), 32, 14,
|
||||
juce::Justification::centredLeft);
|
||||
}
|
||||
|
||||
auto plotCurve = [&](const std::array<float, FDNReverb::NUM_BANDS>& rt60,
|
||||
juce::Colour col, float thick)
|
||||
{
|
||||
juce::Path path;
|
||||
bool first = true;
|
||||
for (int i = 0; i < FDNReverb::NUM_BANDS; ++i) {
|
||||
float v = std::clamp(rt60[i], MIN_RT60_DISPLAY, dynamicMaxRT60);
|
||||
float ny = 1.f - (std::log10(v) - logMin) / (logMax - logMin);
|
||||
float px = x0 + 36.f + (float)i / (FDNReverb::NUM_BANDS - 1) * (W - 40.f);
|
||||
float py = y0 + ny * H;
|
||||
if (first) { path.startNewSubPath(px, py); first = false; }
|
||||
else path.lineTo(px, py);
|
||||
}
|
||||
g.setColour(col);
|
||||
g.strokePath(path, juce::PathStrokeType(thick,
|
||||
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
|
||||
|
||||
for (int i = 0; i < FDNReverb::NUM_BANDS; ++i) {
|
||||
float v = std::clamp(rt60[i], MIN_RT60_DISPLAY, dynamicMaxRT60);
|
||||
float ny = 1.f - (std::log10(v) - logMin) / (logMax - logMin);
|
||||
float px = x0 + 36.f + (float)i / (FDNReverb::NUM_BANDS - 1) * (W - 40.f);
|
||||
float py = y0 + ny * H;
|
||||
g.fillEllipse(px - 3.f, py - 3.f, 6.f, 6.f);
|
||||
}
|
||||
};
|
||||
|
||||
// source preset curve (from the selected algorithm)
|
||||
if (processor) {
|
||||
int algo = (int)*processor->apvts.getRawParameterValue("algorithm");
|
||||
auto& preset = *FDNReverb::ALL_PRESETS[
|
||||
juce::jlimit(0, FDNReverb::NUM_ALGORITHMS - 1, algo)];
|
||||
plotCurve(preset.acoustics.rt60,
|
||||
AmbivalenceColors::TextSecondary.withAlpha(0.5f), 1.f);
|
||||
}
|
||||
|
||||
// current RT60 curve (measured)
|
||||
plotCurve(displayRT60, AmbivalenceColors::Accent, 2.f);
|
||||
|
||||
// top right title
|
||||
g.setColour(AmbivalenceColors::TextSecondary);
|
||||
g.setFont(9.f);
|
||||
g.drawText("RT60 (s) per band",
|
||||
(int)x0 + 36, (int)y0 + 3, (int)W - 40, 12,
|
||||
juce::Justification::right);
|
||||
}
|
||||
|
||||
// --- VUMeter ---------------------------------------------------------
|
||||
VUMeter::VUMeter(const juce::String& lbl, Side s) : label(lbl), side(s) {}
|
||||
|
||||
void VUMeter::paint(juce::Graphics& g)
|
||||
{
|
||||
auto b = getLocalBounds().toFloat().reduced(1.f);
|
||||
g.setColour(AmbivalenceColors::Surface);
|
||||
g.fillRoundedRectangle(b, 3.f);
|
||||
|
||||
float bx = b.getX() + 22.f, bw = b.getWidth() - 22.f;
|
||||
auto bar = [&](float y, float level) {
|
||||
float n = juce::jlimit(0.f, 1.f, juce::jmap(
|
||||
juce::Decibels::gainToDecibels(level + 1e-9f), -60.f, 0.f, 0.f, 1.f));
|
||||
g.setColour(AmbivalenceColors::ArcTrack);
|
||||
g.fillRoundedRectangle(bx, y, bw, 7.f, 2.f);
|
||||
juce::ColourGradient gr(AmbivalenceColors::AccentBlue, bx, y,
|
||||
AmbivalenceColors::Accent, bx + bw, y, false);
|
||||
g.setGradientFill(gr);
|
||||
g.fillRoundedRectangle(bx, y, bw * n, 7.f, 2.f);
|
||||
};
|
||||
bar(b.getY() + 2.f, levelL);
|
||||
bar(b.getY() + 11.f, levelR);
|
||||
|
||||
g.setColour(AmbivalenceColors::TextSecondary);
|
||||
g.setFont(8.f);
|
||||
g.drawText(label, (int)b.getX(), (int)b.getY(), 20, (int)b.getHeight(),
|
||||
juce::Justification::centredLeft);
|
||||
}
|
||||
|
||||
// --- ArcKnob ---------------------------------------------------------
|
||||
void ArcKnob::build(juce::AudioProcessorValueTreeState& apvts,
|
||||
const juce::String& paramID,
|
||||
const juce::String& labelText,
|
||||
juce::Component* parent,
|
||||
AmbivalenceLookAndFeel& laf)
|
||||
{
|
||||
slider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag);
|
||||
slider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 62, 14);
|
||||
slider.setLookAndFeel(&laf);
|
||||
slider.setColour(juce::Slider::textBoxTextColourId,
|
||||
AmbivalenceColors::TextSecondary);
|
||||
slider.setColour(juce::Slider::textBoxOutlineColourId,
|
||||
juce::Colours::transparentBlack);
|
||||
parent->addAndMakeVisible(slider);
|
||||
|
||||
label.setText(labelText, juce::dontSendNotification);
|
||||
label.setJustificationType(juce::Justification::centred);
|
||||
label.setFont(juce::Font(juce::FontOptions(9.f)));
|
||||
label.setColour(juce::Label::textColourId, AmbivalenceColors::TextSecondary);
|
||||
parent->addAndMakeVisible(label);
|
||||
|
||||
// * after changes
|
||||
attachment.reset(
|
||||
new juce::AudioProcessorValueTreeState::SliderAttachment(
|
||||
apvts, paramID, slider));
|
||||
}
|
||||
|
||||
// --- AlgorithmSelector -----------------------------------------------
|
||||
AlgorithmSelector::AlgorithmSelector(juce::AudioProcessorValueTreeState& a)
|
||||
: apvts(a)
|
||||
{
|
||||
static const char* names[] = {
|
||||
"ROOM1","ROOM2","HALL1","HALL2","PLATE","SPRING","GOLDFOIL"
|
||||
};
|
||||
for (int i = 0; i < FDNReverb::NUM_ALGORITHMS; ++i) {
|
||||
buttons[i].setButtonText(names[i]);
|
||||
addAndMakeVisible(buttons[i]);
|
||||
int idx = i;
|
||||
buttons[i].onClick = [this, idx] {
|
||||
if (auto* param = apvts.getParameter("algorithm"))
|
||||
param->setValueNotifyingHost(
|
||||
(float)idx / (float)(FDNReverb::NUM_ALGORITHMS - 1));
|
||||
};
|
||||
}
|
||||
apvts.addParameterListener("algorithm", this);
|
||||
currentAlgo = juce::roundToInt(
|
||||
*apvts.getRawParameterValue("algorithm")
|
||||
* (FDNReverb::NUM_ALGORITHMS - 1));
|
||||
}
|
||||
|
||||
AlgorithmSelector::~AlgorithmSelector() {
|
||||
apvts.removeParameterListener("algorithm", this);
|
||||
}
|
||||
|
||||
void AlgorithmSelector::parameterChanged(const juce::String&, float newVal) {
|
||||
int newAlgo = juce::jlimit(0, FDNReverb::NUM_ALGORITHMS - 1,
|
||||
juce::roundToInt(newVal));
|
||||
juce::MessageManager::callAsync([this, newAlgo] {
|
||||
currentAlgo = newAlgo;
|
||||
updateButtonColors();
|
||||
});
|
||||
}
|
||||
|
||||
void AlgorithmSelector::updateButtonColors() {
|
||||
for (int i = 0; i < FDNReverb::NUM_ALGORITHMS; ++i) {
|
||||
bool on = (i == currentAlgo);
|
||||
buttons[i].setColour(juce::TextButton::buttonColourId,
|
||||
on ? AmbivalenceColors::Accent : AmbivalenceColors::Surface);
|
||||
buttons[i].setColour(juce::TextButton::textColourOffId,
|
||||
on ? AmbivalenceColors::Background : AmbivalenceColors::TextSecondary);
|
||||
buttons[i].repaint();
|
||||
}
|
||||
}
|
||||
|
||||
void AlgorithmSelector::paint(juce::Graphics& g) {
|
||||
g.setColour(AmbivalenceColors::Surface);
|
||||
g.fillRoundedRectangle(getLocalBounds().toFloat(), 4.f);
|
||||
}
|
||||
|
||||
void AlgorithmSelector::resized() {
|
||||
auto area = getLocalBounds().reduced(2);
|
||||
int btnW = area.getWidth() / FDNReverb::NUM_ALGORITHMS;
|
||||
for (int i = 0; i < FDNReverb::NUM_ALGORITHMS; ++i)
|
||||
buttons[i].setBounds(area.getX() + i * btnW, area.getY(),
|
||||
btnW - 1, area.getHeight());
|
||||
updateButtonColors();
|
||||
}
|
||||
110
Source/GUI/AmbivalenceUI.h
Normal file
110
Source/GUI/AmbivalenceUI.h
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "../AlgorithmPresets.h"
|
||||
|
||||
class FDNReverbAudioProcessor;
|
||||
|
||||
// --- Ambivalence Design System ------------------------------------------
|
||||
namespace AmbivalenceColors {
|
||||
const juce::Colour Background{ 0xFF1A1A1A };
|
||||
const juce::Colour Surface{ 0xFF242424 };
|
||||
const juce::Colour Panel{ 0xFF2C2C2C };
|
||||
const juce::Colour Border{ 0xFF3C3C3C };
|
||||
const juce::Colour Accent{ 0xFFFF6B00 };
|
||||
const juce::Colour AccentBlue{ 0xFF4090FF };
|
||||
const juce::Colour TextPrimary{ 0xFFE8E8E8 };
|
||||
const juce::Colour TextSecondary{ 0xFF888888 };
|
||||
const juce::Colour ArcTrack{ 0xFF3A3A3A };
|
||||
const juce::Colour ArcFill{ 0xFFFF6B00 };
|
||||
const juce::Colour Separator{ 0xFF383838 };
|
||||
}
|
||||
|
||||
// --- Ambivalence LookAndFeel --------------------------------------------
|
||||
class AmbivalenceLookAndFeel : public juce::LookAndFeel_V4
|
||||
{
|
||||
public:
|
||||
AmbivalenceLookAndFeel();
|
||||
void drawRotarySlider(juce::Graphics&, int x, int y, int w, int h,
|
||||
float sliderPos, float startAngle, float endAngle,
|
||||
juce::Slider&) override;
|
||||
void drawLinearSlider(juce::Graphics&, int x, int y, int w, int h,
|
||||
float sliderPos, float, float,
|
||||
juce::Slider::SliderStyle, juce::Slider&) override;
|
||||
void drawComboBox(juce::Graphics&, int w, int h, bool isDown,
|
||||
int, int, int, int, juce::ComboBox&) override;
|
||||
void positionComboBoxText(juce::ComboBox&, juce::Label&) override;
|
||||
juce::Font getLabelFont(juce::Label&) override;
|
||||
juce::Font getComboBoxFont(juce::ComboBox&) override;
|
||||
void drawGroupComponentOutline(juce::Graphics&, int w, int h,
|
||||
const juce::String&, const juce::Justification&,
|
||||
juce::GroupComponent&) override;
|
||||
private:
|
||||
juce::Font mainFont;
|
||||
};
|
||||
|
||||
// --- RT60 Visualizer -------------------------------------------------
|
||||
class RT60Visualizer : public juce::Component, private juce::Timer
|
||||
{
|
||||
public:
|
||||
RT60Visualizer();
|
||||
~RT60Visualizer() override;
|
||||
void setProcessor(FDNReverbAudioProcessor* p) { processor = p; }
|
||||
void paint(juce::Graphics&) override;
|
||||
private:
|
||||
void timerCallback() override;
|
||||
FDNReverbAudioProcessor* processor{ nullptr };
|
||||
std::array<float, FDNReverb::NUM_BANDS> displayRT60;
|
||||
|
||||
static constexpr float MIN_RT60_DISPLAY = 0.05f;
|
||||
static constexpr float MAX_RT60_DISPLAY_FLOOR = 4.0f; // Y axis above value
|
||||
|
||||
// * dynamic Y axis above : effectiveRT60 maximum value smoothly
|
||||
float dynamicMaxRT60{ MAX_RT60_DISPLAY_FLOOR };
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(RT60Visualizer)
|
||||
};
|
||||
|
||||
// --- VU Meter --------------------------------------------------------
|
||||
class VUMeter : public juce::Component
|
||||
{
|
||||
public:
|
||||
enum class Side { Input, Output };
|
||||
VUMeter(const juce::String& label, Side side);
|
||||
void paint(juce::Graphics&) override;
|
||||
void setLevels(float l, float r) noexcept { levelL = l; levelR = r; }
|
||||
private:
|
||||
juce::String label;
|
||||
Side side;
|
||||
float levelL{ 0.f }, levelR{ 0.f };
|
||||
};
|
||||
|
||||
// --- Labelled Arc Knob -----------------------------------------------
|
||||
struct ArcKnob {
|
||||
juce::Slider slider;
|
||||
juce::Label label;
|
||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> attachment;
|
||||
|
||||
void build(juce::AudioProcessorValueTreeState& apvts,
|
||||
const juce::String& paramID,
|
||||
const juce::String& labelText,
|
||||
juce::Component* parent,
|
||||
AmbivalenceLookAndFeel& laf);
|
||||
};
|
||||
|
||||
// --- Algorithm Selector ----------------------------------------------
|
||||
class AlgorithmSelector : public juce::Component,
|
||||
private juce::AudioProcessorValueTreeState::Listener
|
||||
{
|
||||
public:
|
||||
AlgorithmSelector(juce::AudioProcessorValueTreeState& apvts);
|
||||
~AlgorithmSelector() override;
|
||||
void paint(juce::Graphics&) override;
|
||||
void resized() override;
|
||||
private:
|
||||
void parameterChanged(const juce::String&, float) override;
|
||||
void updateButtonColors();
|
||||
std::array<juce::TextButton, FDNReverb::NUM_ALGORITHMS> buttons;
|
||||
juce::AudioProcessorValueTreeState& apvts;
|
||||
int currentAlgo{ 0 };
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AlgorithmSelector)
|
||||
};
|
||||
324
Source/GUI/DecayCurveViz.cpp
Normal file
324
Source/GUI/DecayCurveViz.cpp
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
#include "DecayCurveViz.h"
|
||||
|
||||
DecayCurveViz::DecayCurveViz() {
|
||||
cachedERDelayMs.fill(0.0f);
|
||||
cachedERGains.fill(0.0f);
|
||||
startTimerHz(15);
|
||||
}
|
||||
|
||||
DecayCurveViz::~DecayCurveViz() {
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
void DecayCurveViz::timerCallback() {
|
||||
if (processor == nullptr) return;
|
||||
const auto& engine = processor->getEngine();
|
||||
|
||||
auto rt60 = engine.getEffectiveRT60();
|
||||
cachedRT60Mid = std::max(0.1f, rt60[4]);
|
||||
|
||||
cachedERBypassed = engine.isERBypassed();
|
||||
cachedERTapCount = engine.getERTapCount();
|
||||
if (cachedERTapCount > MAX_DISPLAY_TAPS)
|
||||
cachedERTapCount = MAX_DISPLAY_TAPS;
|
||||
|
||||
double sr = engine.getSampleRate();
|
||||
if (sr < 1.0) sr = 48000.0;
|
||||
|
||||
for (int i = 0; i < cachedERTapCount; ++i) {
|
||||
float delaySamples = engine.getERTapDelaySamples(i);
|
||||
cachedERDelayMs[i] = delaySamples / static_cast<float>(sr) * 1000.0f;
|
||||
cachedERGains[i] = engine.getERTapGain(i);
|
||||
}
|
||||
repaint();
|
||||
}
|
||||
|
||||
void DecayCurveViz::resized() {}
|
||||
|
||||
void DecayCurveViz::paint(juce::Graphics& g)
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat();
|
||||
if (bounds.getWidth() < 10.0f || bounds.getHeight() < 10.0f) return;
|
||||
|
||||
g.fillAll(AmbivalenceColors::Background);
|
||||
|
||||
const float topMargin = 10.0f;
|
||||
const float bottomMargin = 18.0f;
|
||||
const float leftMargin = 30.0f;
|
||||
const float rightMargin = 8.0f;
|
||||
const float plotX = bounds.getX() + leftMargin;
|
||||
const float plotY = bounds.getY() + topMargin;
|
||||
const float plotW = bounds.getWidth() - leftMargin - rightMargin;
|
||||
const float plotH = bounds.getHeight() - topMargin - bottomMargin;
|
||||
|
||||
const float maxTimeSec = juce::jlimit(0.5f, 8.0f, cachedRT60Mid * 1.5f);
|
||||
const float minDB = -60.0f;
|
||||
const float maxDB = 0.0f;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// time axis
|
||||
// 0~splitSec -> full width splitRatio expanded (ER zone )
|
||||
// splitSec~max -> width (Late zone )
|
||||
// -------------------------------------------------------------------------
|
||||
constexpr float splitSec = 0.20f; // 200ms expanded
|
||||
constexpr float splitRatio = 0.30f; // full width 30% ER zone
|
||||
|
||||
auto timeToX = [&](float timeSec) -> float {
|
||||
if (timeSec <= splitSec) {
|
||||
const float ratio = timeSec / splitSec;
|
||||
return plotX + ratio * plotW * splitRatio;
|
||||
}
|
||||
else {
|
||||
const float lateRange = maxTimeSec - splitSec;
|
||||
if (lateRange <= 0.0f) return plotX + plotW;
|
||||
const float ratio = (timeSec - splitSec) / lateRange;
|
||||
return plotX + plotW * splitRatio + ratio * plotW * (1.0f - splitRatio);
|
||||
}
|
||||
};
|
||||
|
||||
auto dbToY = [&](float db) -> float {
|
||||
const float normalized = (db - minDB) / (maxDB - minDB);
|
||||
return plotY + (1.0f - normalized) * plotH;
|
||||
};
|
||||
|
||||
// --- ER zone background ---
|
||||
{
|
||||
const float erZoneW = plotW * splitRatio;
|
||||
g.setColour(juce::Colour(0xFF1A2535));
|
||||
g.fillRect(plotX, plotY, erZoneW, plotH);
|
||||
}
|
||||
|
||||
// --- grid : horizontal (dB) ---
|
||||
g.setColour(AmbivalenceColors::Separator.withAlpha(0.3f));
|
||||
for (float db = 0.0f; db >= -60.0f; db -= 20.0f)
|
||||
g.drawHorizontalLine((int)dbToY(db), plotX, plotX + plotW);
|
||||
|
||||
// --- grid : ER zone vertical (ms) ---
|
||||
{
|
||||
static const float erGridMs[] = { 20.0f, 50.0f, 100.0f, 150.0f, 200.0f };
|
||||
g.setColour(AmbivalenceColors::Separator.withAlpha(0.5f));
|
||||
for (float ms : erGridMs) {
|
||||
const float t = ms * 0.001f;
|
||||
if (t >= maxTimeSec) break;
|
||||
g.drawVerticalLine((int)timeToX(t), plotY, plotY + plotH);
|
||||
}
|
||||
}
|
||||
|
||||
// --- grid : Late zone vertical (s) ---
|
||||
float timeStep;
|
||||
if (maxTimeSec <= 2.0f) timeStep = 0.5f;
|
||||
else if (maxTimeSec <= 4.0f) timeStep = 1.0f;
|
||||
else timeStep = 2.0f;
|
||||
|
||||
g.setColour(AmbivalenceColors::Separator.withAlpha(0.3f));
|
||||
for (float t = splitSec + timeStep; t <= maxTimeSec; t += timeStep)
|
||||
g.drawVerticalLine((int)timeToX(t), plotY, plotY + plotH);
|
||||
|
||||
// --- ---
|
||||
{
|
||||
const float splitX = timeToX(splitSec);
|
||||
g.setColour(AmbivalenceColors::Separator.withAlpha(0.9f));
|
||||
g.drawVerticalLine((int)splitX, plotY, plotY + plotH);
|
||||
}
|
||||
|
||||
// --- axis label ---
|
||||
g.setFont(juce::Font(juce::FontOptions(8.5f)));
|
||||
g.setColour(AmbivalenceColors::TextSecondary.withAlpha(0.6f));
|
||||
|
||||
for (float db = 0.0f; db >= -60.0f; db -= 20.0f) {
|
||||
const float y = dbToY(db);
|
||||
g.drawText(juce::String((int)db) + "dB",
|
||||
(int)(plotX - leftMargin + 2), (int)(y - 6),
|
||||
(int)(leftMargin - 4), 12,
|
||||
juce::Justification::centredRight);
|
||||
}
|
||||
|
||||
// ER zone time label (ms)
|
||||
{
|
||||
static const float erGridMs[] = { 20.0f, 50.0f, 100.0f, 150.0f, 200.0f };
|
||||
for (float ms : erGridMs) {
|
||||
const float t = ms * 0.001f;
|
||||
if (t >= maxTimeSec) break;
|
||||
const float x = timeToX(t);
|
||||
g.drawText(juce::String((int)ms) + "ms",
|
||||
(int)(x - 20), (int)(plotY + plotH + 2),
|
||||
40, 14, juce::Justification::centred);
|
||||
}
|
||||
}
|
||||
|
||||
// Late zone time label (s)
|
||||
for (float t = splitSec + timeStep; t <= maxTimeSec; t += timeStep) {
|
||||
const float x = timeToX(t);
|
||||
g.drawText(juce::String(t, 1) + "s",
|
||||
(int)(x - 20), (int)(plotY + plotH + 2),
|
||||
40, 14, juce::Justification::centred);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Late Reverb decay curve ( 2D gradient )
|
||||
// -------------------------------------------------------------------------
|
||||
{
|
||||
const int numPoints = 80;
|
||||
juce::Colour orangeColor = AmbivalenceColors::Accent;
|
||||
|
||||
juce::Path latePath;
|
||||
latePath.startNewSubPath(plotX, dbToY(maxDB));
|
||||
for (int i = 0; i <= numPoints; ++i) {
|
||||
const float t = (i / static_cast<float>(numPoints)) * maxTimeSec;
|
||||
const float db = std::max(minDB, -60.0f * t / cachedRT60Mid);
|
||||
latePath.lineTo(timeToX(t), dbToY(db));
|
||||
}
|
||||
latePath.lineTo(timeToX(maxTimeSec), dbToY(minDB));
|
||||
latePath.lineTo(plotX, dbToY(minDB));
|
||||
latePath.closeSubPath();
|
||||
|
||||
juce::ColourGradient lateGrad(
|
||||
orangeColor.withAlpha(0.55f), plotX, plotY,
|
||||
orangeColor.withAlpha(0.0f), plotX + plotW, plotY + plotH,
|
||||
false);
|
||||
g.setGradientFill(lateGrad);
|
||||
g.fillPath(latePath);
|
||||
|
||||
juce::Path lateOutline;
|
||||
lateOutline.startNewSubPath(plotX, dbToY(maxDB));
|
||||
for (int i = 0; i <= numPoints; ++i) {
|
||||
const float t = (i / static_cast<float>(numPoints)) * maxTimeSec;
|
||||
const float db = std::max(minDB, -60.0f * t / cachedRT60Mid);
|
||||
lateOutline.lineTo(timeToX(t), dbToY(db));
|
||||
}
|
||||
g.setColour(orangeColor.withAlpha(0.75f));
|
||||
g.strokePath(lateOutline, juce::PathStrokeType(1.5f,
|
||||
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ER drawing (* )
|
||||
// : vertical line 2px + marker
|
||||
// : wide 5px + marker + envelope fill
|
||||
// -------------------------------------------------------------------------
|
||||
if (!cachedERBypassed && cachedERTapCount > 0) {
|
||||
const juce::Colour blueColor = juce::Colour::fromRGB(80, 160, 230);
|
||||
|
||||
// -- ER envelope fill region --
|
||||
if (cachedERTapCount >= 2) {
|
||||
juce::Path erFill;
|
||||
bool started = false;
|
||||
float lastX = plotX;
|
||||
|
||||
for (int t = 0; t < cachedERTapCount; ++t) {
|
||||
const float timeSec = cachedERDelayMs[t] * 0.001f;
|
||||
if (timeSec > maxTimeSec) continue;
|
||||
|
||||
float gainDB = (cachedERGains[t] > 1e-6f)
|
||||
? juce::Decibels::gainToDecibels(cachedERGains[t]) : minDB;
|
||||
gainDB = juce::jlimit(minDB, maxDB, gainDB);
|
||||
|
||||
const float x = timeToX(timeSec);
|
||||
const float y = dbToY(gainDB);
|
||||
|
||||
if (!started) {
|
||||
erFill.startNewSubPath(plotX, dbToY(minDB));
|
||||
erFill.lineTo(x, y);
|
||||
started = true;
|
||||
}
|
||||
else {
|
||||
erFill.lineTo(x, y);
|
||||
}
|
||||
lastX = x;
|
||||
}
|
||||
|
||||
if (started) {
|
||||
erFill.lineTo(lastX, dbToY(minDB));
|
||||
erFill.closeSubPath();
|
||||
|
||||
juce::ColourGradient erAreaGrad(
|
||||
blueColor.withAlpha(0.20f), plotX, plotY,
|
||||
blueColor.withAlpha(0.03f), plotX + plotW * splitRatio, plotY + plotH,
|
||||
false);
|
||||
g.setGradientFill(erAreaGrad);
|
||||
g.fillPath(erFill);
|
||||
}
|
||||
}
|
||||
|
||||
// -- : wide + marker --
|
||||
for (int t = 0; t < cachedERTapCount; ++t) {
|
||||
const float timeSec = cachedERDelayMs[t] * 0.001f;
|
||||
if (timeSec > maxTimeSec) continue;
|
||||
|
||||
float gainDB = (cachedERGains[t] > 1e-6f)
|
||||
? juce::Decibels::gainToDecibels(cachedERGains[t]) : minDB;
|
||||
gainDB = juce::jlimit(minDB, maxDB, gainDB);
|
||||
|
||||
const float x = timeToX(timeSec);
|
||||
const float yTop = dbToY(gainDB);
|
||||
const float yBottom = dbToY(minDB);
|
||||
const float barW = 5.0f;
|
||||
|
||||
juce::ColourGradient tapGrad(
|
||||
blueColor.withAlpha(0.90f), x, yTop,
|
||||
blueColor.withAlpha(0.10f), x, yBottom,
|
||||
false);
|
||||
g.setGradientFill(tapGrad);
|
||||
g.fillRect(x - barW * 0.5f, yTop, barW, yBottom - yTop);
|
||||
|
||||
// diamond marker
|
||||
g.setColour(blueColor);
|
||||
juce::Path diamond;
|
||||
diamond.startNewSubPath(x, yTop - 5.0f);
|
||||
diamond.lineTo(x + 4.0f, yTop);
|
||||
diamond.lineTo(x, yTop + 3.0f);
|
||||
diamond.lineTo(x - 4.0f, yTop);
|
||||
diamond.closeSubPath();
|
||||
g.fillPath(diamond);
|
||||
}
|
||||
|
||||
// -- ER envelope outline --
|
||||
if (cachedERTapCount >= 2) {
|
||||
juce::Path erOutline;
|
||||
bool started = false;
|
||||
|
||||
for (int t = 0; t < cachedERTapCount; ++t) {
|
||||
const float timeSec = cachedERDelayMs[t] * 0.001f;
|
||||
if (timeSec > maxTimeSec) continue;
|
||||
|
||||
float gainDB = (cachedERGains[t] > 1e-6f)
|
||||
? juce::Decibels::gainToDecibels(cachedERGains[t]) : minDB;
|
||||
gainDB = juce::jlimit(minDB, maxDB, gainDB);
|
||||
|
||||
const float x = timeToX(timeSec);
|
||||
const float y = dbToY(gainDB);
|
||||
|
||||
if (!started) { erOutline.startNewSubPath(x, y); started = true; }
|
||||
else erOutline.lineTo(x, y);
|
||||
}
|
||||
|
||||
g.setColour(blueColor.withAlpha(0.65f));
|
||||
g.strokePath(erOutline, juce::PathStrokeType(1.5f,
|
||||
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
|
||||
}
|
||||
}
|
||||
|
||||
// --- zone label ---
|
||||
g.setFont(juce::Font(juce::FontOptions(
|
||||
"Helvetica Neue", 8.0f, juce::Font::bold)));
|
||||
|
||||
g.setColour(juce::Colour::fromRGB(80, 160, 230).withAlpha(0.9f));
|
||||
g.drawText("ER",
|
||||
(int)(plotX + 4), (int)(plotY + 2),
|
||||
30, 12, juce::Justification::centredLeft);
|
||||
|
||||
{
|
||||
const float splitX = timeToX(splitSec);
|
||||
g.setColour(AmbivalenceColors::Accent.withAlpha(0.9f));
|
||||
g.drawText("LATE",
|
||||
(int)(splitX + 6), (int)(plotY + 2),
|
||||
40, 12, juce::Justification::centredLeft);
|
||||
}
|
||||
|
||||
g.setFont(juce::Font(juce::FontOptions(7.5f)));
|
||||
g.setColour(AmbivalenceColors::TextSecondary.withAlpha(0.4f));
|
||||
g.drawText("0-200ms (x2)",
|
||||
(int)(plotX + 2), (int)(plotY + plotH - 14),
|
||||
(int)(plotW * splitRatio - 4), 12,
|
||||
juce::Justification::centredLeft);
|
||||
}
|
||||
42
Source/GUI/DecayCurveViz.h
Normal file
42
Source/GUI/DecayCurveViz.h
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "../PluginProcessor.h"
|
||||
#include "AmbivalenceUI.h"
|
||||
|
||||
class DecayCurveViz : public juce::Component, private juce::Timer {
|
||||
public:
|
||||
DecayCurveViz();
|
||||
~DecayCurveViz() override;
|
||||
|
||||
void setProcessor(FDNReverbAudioProcessor* p) noexcept { processor = p; }
|
||||
void paint(juce::Graphics& g) override;
|
||||
void resized() override;
|
||||
|
||||
private:
|
||||
void timerCallback() override;
|
||||
|
||||
// --- time axis -----------------------------------------------
|
||||
// time axis :
|
||||
// 0~splitSec : plotW x splitRatio width expanded
|
||||
// splitSec~max: width
|
||||
// ER(0~200ms) 2
|
||||
float timeToX(float timeSec, float plotX, float plotW,
|
||||
float maxTimeSec) const noexcept;
|
||||
|
||||
FDNReverbAudioProcessor* processor{ nullptr };
|
||||
|
||||
float cachedRT60Mid{ 1.0f };
|
||||
int cachedERTapCount{ 0 };
|
||||
bool cachedERBypassed{ false };
|
||||
|
||||
static constexpr int MAX_DISPLAY_TAPS = 12;
|
||||
std::array<float, MAX_DISPLAY_TAPS> cachedERDelayMs;
|
||||
std::array<float, MAX_DISPLAY_TAPS> cachedERGains;
|
||||
|
||||
// --- time axis setting ---
|
||||
// splitSec below time splitRatio width expanded
|
||||
static constexpr float splitSec = 0.20f; // 0~200ms expanded
|
||||
static constexpr float splitRatio = 0.30f; // full width 30% ER zone
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DecayCurveViz)
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue