#include "PluginEditor.h" #include #include namespace { juce::String formatHz (float v) { return juce::String (juce::roundToInt (v)) + " Hz"; } juce::String formatSec (float v) { return juce::String (v, 1) + " s"; } juce::String formatSize (float v) { return juce::String (v, 2) + "x"; } juce::String formatPct (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + " %"; } juce::String formatSemis (float v) { const int semis = juce::roundToInt (v); if (semis > 0) return "+" + juce::String (semis) + " st"; if (semis < 0) return juce::String (semis) + " st"; return "0 st"; } juce::StringArray presetNames() { juce::StringArray names; for (const auto& p : getPresets()) names.add (p.name); return names; } constexpr float scalePresets[] = { 0.75f, 0.9f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f }; constexpr int numScalePresets = (int) (sizeof (scalePresets) / sizeof (scalePresets[0])); juce::StringArray scaleNames() { juce::StringArray names; for (float v : scalePresets) names.add (juce::String (juce::roundToInt (v * 100.0f)) + " %"); return names; } juce::String buildFooterText() { #ifndef HORIZONT_VERSION #define HORIZONT_VERSION "0.0.0" #endif #ifndef HORIZONT_GIT_REVISION #define HORIZONT_GIT_REVISION "unknown" #endif #ifndef HORIZONT_BUILD_TIMESTAMP #define HORIZONT_BUILD_TIMESTAMP "unknown" #endif juce::String text ("HORIZONT v" + juce::String (HORIZONT_VERSION)); text << " · git " << HORIZONT_GIT_REVISION; text << " · build " << HORIZONT_BUILD_TIMESTAMP; return text; } } HorizontAudioProcessorEditor::HorizontAudioProcessorEditor (HorizontAudioProcessor& p, juce::AudioProcessorValueTreeState& s) : AudioProcessorEditor (&p), processor (p), apvts (s), display (p) { lookAndFeel = std::make_unique(); setLookAndFeel (lookAndFeel.get()); titleLabel.setText ("HORIZONT", juce::dontSendNotification); titleLabel.setFont (juce::Font (juce::FontOptions (22.0f, juce::Font::bold))); titleLabel.setColour (juce::Label::textColourId, juce::Colour (0xFFE8F6F4)); addAndMakeVisible (titleLabel); subtitleLabel.setText ("Algorithmic Spatial Processor", juce::dontSendNotification); subtitleLabel.setFont (juce::Font (juce::FontOptions (11.0f))); subtitleLabel.setColour (juce::Label::textColourId, juce::Colour (0xFF5E8688)); addAndMakeVisible (subtitleLabel); presetBox.addItemList (presetNames(), 1); presetBox.setSelectedItemIndex (0); presetBox.addListener (this); presetBox.setTooltip ("Recall a factory preset"); addAndMakeVisible (presetBox); scaleBox.addItemList (scaleNames(), 1); scaleBox.setSelectedItemIndex (2); scaleBox.addListener (this); scaleBox.setTooltip ("Set the UI scale"); addAndMakeVisible (scaleBox); footerLabel.setText (buildFooterText(), juce::dontSendNotification); footerLabel.setJustificationType (juce::Justification::centred); footerLabel.setInterceptsMouseClicks (false, false); addAndMakeVisible (footerLabel); addAndMakeVisible (display); auto makeKnob = [this] (const juce::String& title, const juce::String& id, const juce::String& tip, std::function fmt, float defVal) -> std::unique_ptr { return std::make_unique (title, apvts, id, defVal, fmt, tip); }; knobs.push_back (makeKnob ("LOW CUT", "lowCut", "Input high-pass filter (20 Hz - 500 Hz)", formatHz, 40.0f)); knobs.push_back (makeKnob ("HIGH CUT", "highCut", "Input low-pass filter (800 Hz - 20 kHz)", formatHz, 14000.0f)); knobs.push_back (makeKnob ("DECAY", "decay", "Length of the reverb tail (RT60)", formatSec, 3.5f)); knobs.push_back (makeKnob ("SIZE", "size", "Room size scaling", formatSize, 1.0f)); knobs.push_back (makeKnob ("DIFFUSION", "diffusion", "Echo diffusion and smearing", formatPct, 0.6f)); knobs.push_back (makeKnob ("BRIGHTNESS", "brightness", "High-frequency damping of the tail", formatPct, 0.75f)); knobs.push_back (makeKnob ("FEEDBACK", "feedback", "Recirculation through the pitch shifter", formatPct, 0.6f)); knobs.push_back (makeKnob ("PITCH", "pitch", "Pitch shift applied per feedback pass", formatSemis, 0.0f)); knobs.push_back (makeKnob ("DRY / WET", "dryWet", "Dry / wet mix", formatPct, 0.35f)); for (auto& k : knobs) addAndMakeVisible (k.get()); setSize (juce::roundToInt (baseWidth), juce::roundToInt (baseHeight)); setResizeLimits (juce::roundToInt (baseWidth * 0.75f), juce::roundToInt (baseHeight * 0.75f), juce::roundToInt (baseWidth * 2.0f), juce::roundToInt (baseHeight * 2.0f)); } HorizontAudioProcessorEditor::~HorizontAudioProcessorEditor() { } void HorizontAudioProcessorEditor::setScaleFactor (float newScale) { newScale = juce::jlimit (0.6f, 2.0f, newScale); setSize (juce::roundToInt (baseWidth * newScale), juce::roundToInt (baseHeight * newScale)); } void HorizontAudioProcessorEditor::updateScaleFactor() { const float newScale = juce::jmin ((float) getWidth() / baseWidth, (float) getHeight() / baseHeight); if (juce::approximatelyEqual (scaleFactor, newScale)) return; scaleFactor = newScale; for (auto& k : knobs) k->setScaleFactor (scaleFactor); display.setScaleFactor (scaleFactor); } void HorizontAudioProcessorEditor::comboBoxChanged (juce::ComboBox* box) { if (box == &presetBox) { const int idx = presetBox.getSelectedItemIndex(); const auto& presets = processor.getPresets(); if (idx >= 0 && idx < (int) presets.size()) processor.applyPreset (presets[(size_t) idx]); } else if (box == &scaleBox) { const int idx = scaleBox.getSelectedItemIndex(); if (idx >= 0 && idx < numScalePresets) setScaleFactor (scalePresets[idx]); } } void HorizontAudioProcessorEditor::updateScaleSelection() { int best = 0; float bestDiff = std::numeric_limits::max(); for (int i = 0; i < numScalePresets; ++i) { const float diff = std::abs (scaleFactor - scalePresets[i]); if (diff < bestDiff) { bestDiff = diff; best = i; } } scaleBox.setSelectedItemIndex (best, juce::dontSendNotification); } void HorizontAudioProcessorEditor::layoutKnobRow (juce::Rectangle area, int startIndex, int count) { juce::FlexBox fb; fb.flexDirection = juce::FlexBox::Direction::row; fb.justifyContent = juce::FlexBox::JustifyContent::center; fb.alignItems = juce::FlexBox::AlignItems::stretch; const float minW = 80.0f * scaleFactor; const float minH = 96.0f * scaleFactor; for (int i = startIndex; i < startIndex + count; ++i) fb.items.add (juce::FlexItem (*knobs[(size_t) i]).withMinWidth (minW).withMinHeight (minH).withFlex (1.0f)); fb.performLayout (area); } void HorizontAudioProcessorEditor::mouseWheelMove (const juce::MouseEvent&, const juce::MouseWheelDetails& wheel) { if (wheel.deltaY != 0.0f) { const float zoomStep = 0.1f; setScaleFactor (scaleFactor - wheel.deltaY * zoomStep); } } void HorizontAudioProcessorEditor::resized() { updateScaleFactor(); updateScaleSelection(); auto area = getLocalBounds(); const float s = scaleFactor; const int headerH = juce::roundToInt (64.0f * s); const int footerH = juce::roundToInt (26.0f * s); const int presetBoxW = juce::roundToInt (180.0f * s); const int scaleBoxW = juce::roundToInt (72.0f * s); const int boxGap = juce::roundToInt (10.0f * s); const int presetBoxMargin = juce::roundToInt (20.0f * s); const int titleH = juce::roundToInt (32.0f * s); const int titleMarginH = juce::roundToInt (18.0f * s); const int titleMarginV = juce::roundToInt (4.0f * s); const int subtitleMarginH = juce::roundToInt (18.0f * s); const int subtitleMarginV = juce::roundToInt (2.0f * s); const int rowsMarginH = juce::roundToInt (16.0f * s); const int rowsMarginV = juce::roundToInt (4.0f * s); const int gap = juce::roundToInt (10.0f * s); footerArea = area.removeFromBottom (footerH); footerLabel.setBounds (footerArea.reduced (rowsMarginH, 0)); footerLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (9.0f * s)))); footerLabel.setColour (juce::Label::textColourId, juce::Colour (0xFF5E8688)); auto header = area.removeFromTop (headerH); const int comboW = presetBoxW + boxGap + scaleBoxW; auto comboArea = header.removeFromRight (comboW).reduced (0, presetBoxMargin); auto scaleArea = comboArea.removeFromLeft (scaleBoxW); scaleBox.setBounds (scaleArea); presetBox.setBounds (comboArea); auto titleArea = header.removeFromTop (titleH).reduced (titleMarginH, titleMarginV); titleLabel.setBounds (titleArea); subtitleLabel.setBounds (header.reduced (subtitleMarginH, subtitleMarginV)); titleLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (22.0f * s), juce::Font::bold))); subtitleLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (11.0f * s)))); auto modules = area.reduced (rowsMarginH, rowsMarginV); const int panelH = (modules.getHeight() - 2 * gap) / 3; auto displayPanel = modules.removeFromTop (panelH); display.setBounds (displayPanel); knobRow1Area = modules.removeFromTop (panelH + gap); knobRow2Area = modules.removeFromTop (panelH + gap); layoutKnobRow (knobRow1Area, 0, 5); layoutKnobRow (knobRow2Area, 5, 4); } void HorizontAudioProcessorEditor::paint (juce::Graphics& g) { const auto bounds = getLocalBounds(); g.setGradientFill (juce::ColourGradient (juce::Colour (0xFF0E2A2E), { 0.0f, 0.0f }, juce::Colour (0xFF060B0D), { 0.0f, (float) bounds.getHeight() }, false)); g.fillRect (bounds); g.setColour (juce::Colour (0x1AFFFFFF)); g.fillRect (0, 0, bounds.getWidth(), 1); g.setColour (juce::Colour (0x14000000)); g.fillRect (0, bounds.getHeight() - 1, bounds.getWidth(), 1); if (footerArea.getHeight() > 0) { g.setColour (juce::Colour (0x1AFFFFFF)); g.fillRect (0, footerArea.getY(), bounds.getWidth(), 1); g.setColour (juce::Colour (0x14000000)); g.fillRect (0, footerArea.getY() + 1, bounds.getWidth(), 1); } auto drawModule = [&g] (juce::Rectangle moduleArea) { const auto r = moduleArea.toFloat(); auto shRect = r.expanded (4.0f, 3.0f).translated (0.0f, 3.0f); auto shadowGrad = juce::ColourGradient (juce::Colour (0x00000000), { 0.0f, shRect.getY() }, juce::Colour (0x00000000), { 0.0f, shRect.getBottom() }, false); shadowGrad.addColour (0.45f, juce::Colour (0x70000000)); g.setGradientFill (shadowGrad); g.fillRoundedRectangle (shRect, 13.0f); g.setGradientFill (juce::ColourGradient (juce::Colour (0xFF24323A), { 0.0f, r.getY() }, juce::Colour (0xFF0B1216), { 0.0f, r.getBottom() }, false)); g.fillRoundedRectangle (r, 12.0f); g.setColour (juce::Colour (0x2A3CE0C8)); g.drawRoundedRectangle (r.expanded (1.0f), 13.0f, 1.0f); g.setColour (juce::Colour (0x38FFFFFF)); g.drawHorizontalLine ((int) r.getY() + 1, (int) r.getX() + 3, (int) r.getRight() - 3); g.drawVerticalLine ((int) r.getX() + 1, (int) r.getY() + 3, (int) r.getBottom() - 3); g.setColour (juce::Colour (0x50000000)); g.drawHorizontalLine ((int) r.getBottom() - 1, (int) r.getX() + 3, (int) r.getRight() - 3); g.drawVerticalLine ((int) r.getRight() - 1, (int) r.getY() + 3, (int) r.getBottom() - 3); g.saveState(); g.reduceClipRegion (r.toNearestInt().reduced (1)); auto refl = juce::ColourGradient (juce::Colour (0x28FFFFFF), { 0.0f, r.getY() }, juce::Colour (0x00FFFFFF), { 0.0f, r.getY() + r.getHeight() * 0.45f }, false); g.setGradientFill (refl); g.fillRect (r); g.setColour (juce::Colour (0x45FFFFFF)); g.fillRect (r.getX() + 2.0f, r.getY() + 1.0f, r.getWidth() - 4.0f, 1.0f); g.restoreState(); }; drawModule (knobRow1Area); drawModule (knobRow2Area); }