#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)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(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(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& 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(); }