#include "PluginProcessor.h" #include "PluginEditor.h" #include "BuildInfo.h" static constexpr int Y_HEADER = 8; static constexpr int Y_SLABEL1 = 46; static constexpr int Y_ROW1 = 64; static constexpr int Y_SLABEL2 = 164; static constexpr int Y_ROW2 = 182; static constexpr int Y_SEP = 282; static constexpr int Y_VIZ = 286; static constexpr int SEC_TIME = 8; static constexpr int SEC_FREQUENCY = 254; static constexpr int SEC_DIFFUSION = 418; static constexpr int SEC_STEREO = 664; static constexpr int SEC_CHARACTER = 746; static constexpr int SEP_TF = 245; static constexpr int SEP_FD = 409; static constexpr int SEP_DS = 655; static constexpr int SEP_SC = 737; // ----------------------------------------------------------------------------- // EditorContent // ----------------------------------------------------------------------------- // constructor // ----------------------------------------------------------------------------- EditorContent::EditorContent(FDNReverbAudioProcessor& p, AmbivalenceLookAndFeel& laf) : vuIn("IN", VUMeter::Side::Input), vuOut("OUT", VUMeter::Side::Output) { setOpaque(true); lookAndFeel = &laf; // -- Title -- titleLabel.setText("AMBIVALENCE", juce::dontSendNotification); titleLabel.setFont(juce::Font(juce::FontOptions( "Helvetica Neue", 24.f, juce::Font::bold))); titleLabel.setColour(juce::Label::textColourId, AmbivalenceColors::TextPrimary); titleLabel.setJustificationType(juce::Justification::centredLeft); addAndMakeVisible(titleLabel); auto BK = [&](ArcKnob& k, const char* id, const char* lbl) { k.build(p.apvts, id, lbl, this, laf); }; BK(kPreDelay, "predelay", "PRE-DELAY"); BK(kRoomSize, "roomsize", "ROOM SIZE"); BK(kDecay, "decaytime", "DECAY"); BK(kHFDamp, "hfdamping", "HF DAMP"); BK(kLFAbsorb, "lfabsorption", "LF ABSORB"); BK(kDiffusion, "diffusion", "DIFFUSION"); BK(kModAmt, "modamount", "MOD AMT"); BK(kModRate, "modrate", "MOD RATE"); BK(kStereoW, "stereowidth", "WIDTH"); BK(kERLevel, "erlevel", "ER LEVEL"); BK(kSaturation, "saturation", "SATURATE"); BK(kWet, "wetlevel", "WET"); BK(kDry, "drylevel", "DRY"); BK(kDuckAmt, "duckamount", "AMOUNT"); BK(kDuckThr, "duckthresh", "THRESH"); BK(kDuckAtt, "duckattack", "ATTACK"); BK(kDuckRel, "duckrelease", "RELEASE"); BK(kLoCutNorm, "locut", "LO CUT"); BK(kHiCutNorm, "hicut", "HI CUT"); // -- ProMode button -- proModeButton.setButtonText("EQ"); proModeButton.setClickingTogglesState(true); proModeButton.setColour(juce::TextButton::buttonOnColourId, AmbivalenceColors::Accent); proModeButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface); proModeButton.setColour(juce::TextButton::textColourOnId, AmbivalenceColors::Background); proModeButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextSecondary); addAndMakeVisible(proModeButton); proModeAttachment.reset( new juce::AudioProcessorValueTreeState::ButtonAttachment( p.apvts, "promode", proModeButton)); // -- Zoom menu button -- zoomButton.setButtonText(juce::String( juce::roundToInt(EditorContent::kDefaultZoom * 100.f)) + "%"); zoomButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface); zoomButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextPrimary); zoomButton.setTooltip("UI zoom"); addAndMakeVisible(zoomButton); // -- ER SOLO button -- erSoloButton.setButtonText("ER ONLY"); erSoloButton.setClickingTogglesState(true); erSoloButton.setColour(juce::TextButton::buttonOnColourId, AmbivalenceColors::AccentBlue); erSoloButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface); erSoloButton.setColour(juce::TextButton::textColourOnId, AmbivalenceColors::Background); erSoloButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextSecondary); addAndMakeVisible(erSoloButton); erSoloAttachment.reset( new juce::AudioProcessorValueTreeState::ButtonAttachment( p.apvts, "ersolo", erSoloButton)); // -- ProMode: RT60 band -- static const char* rtBandIDs[] = { "rtband0","rtband1","rtband2","rtband3","rtband4", "rtband5","rtband6","rtband7","rtband8","rtband9" }; static const char* rtBandLbls[] = { "31Hz","63Hz","125Hz","250Hz","500Hz", "1kHz","2kHz","4kHz","8kHz","16kHz" }; for (int i = 0; i < 10; ++i) kRTBands[i].build(p.apvts, rtBandIDs[i], rtBandLbls[i], this, laf); // -- ProMode: SatType -- satTypeLabel.setText("SAT TYPE", juce::dontSendNotification); satTypeLabel.setFont(juce::Font(juce::FontOptions(9.f))); satTypeLabel.setColour(juce::Label::textColourId, AmbivalenceColors::TextSecondary); satTypeLabel.setJustificationType(juce::Justification::centred); addAndMakeVisible(satTypeLabel); satTypeCombo.addItemList({ "Warm","Tape","Tube","Hard" }, 1); satTypeCombo.setLookAndFeel(&laf); addAndMakeVisible(satTypeCombo); satTypeAttachment.reset( new juce::AudioProcessorValueTreeState::ComboBoxAttachment( p.apvts, "sattype", satTypeCombo)); // -- ProMode: Tilt EQ + Output EQ -- BK(kTiltLow, "tiltlow", "TILT LOW"); BK(kTiltMid, "tiltmid", "TILT MID"); BK(kTiltHigh, "tilthigh", "TILT HIGH"); BK(kLoCutPro, "locut", "LO CUT"); BK(kHiCutPro, "hicut", "HI CUT"); // -- Visualizers -- decayCurveViz.setProcessor(&p); addAndMakeVisible(decayCurveViz); addAndMakeVisible(vuIn); addAndMakeVisible(vuOut); // -- AcousticMetrics -- labelMetricsTitle.setText("ACOUSTICS", juce::dontSendNotification); labelMetricsTitle.setFont(juce::Font(juce::FontOptions( "Helvetica Neue", 8.5f, juce::Font::bold))); labelMetricsTitle.setColour(juce::Label::textColourId, AmbivalenceColors::Accent.withAlpha(0.75f)); labelMetricsTitle.setJustificationType(juce::Justification::centredLeft); addAndMakeVisible(labelMetricsTitle); auto setupCaption = [this](juce::Label& label, const juce::String& text) { label.setText(text, juce::dontSendNotification); label.setFont(juce::Font(juce::FontOptions( "Helvetica Neue", 8.0f, juce::Font::plain))); label.setColour(juce::Label::textColourId, AmbivalenceColors::TextSecondary.withAlpha(0.85f)); label.setJustificationType(juce::Justification::centredRight); addAndMakeVisible(label); }; auto setupValue = [this](juce::Label& label) { label.setText("--", juce::dontSendNotification); label.setFont(juce::Font(juce::FontOptions( "Helvetica Neue", 9.5f, juce::Font::bold))); label.setColour(juce::Label::textColourId, AmbivalenceColors::TextPrimary); label.setJustificationType(juce::Justification::centredLeft); addAndMakeVisible(label); }; setupCaption(labelD50Caption, "D50:"); setupCaption(labelC50Caption, "C50:"); setupCaption(labelC80Caption, "C80:"); setupCaption(labelEDTCaption, "EDT:"); setupValue(labelD50Value); setupValue(labelC50Value); setupValue(labelC80Value); setupValue(labelEDTValue); // -- Git build info -- juce::String gitInfo = "Git: " + juce::String(AMBIVALENCE_GIT_BRANCH) + " @ " + juce::String(AMBIVALENCE_GIT_COMMIT); #if AMBIVALENCE_GIT_DIRTY gitInfo += " (dirty)"; #endif gitInfo += " | Build: " + juce::String(__DATE__) + " " + juce::String(__TIME__) + " (local)"; statusLabel.setText(gitInfo, juce::dontSendNotification); statusLabel.setFont(juce::Font(juce::FontOptions( "Helvetica Neue", 8.5f, juce::Font::plain))); statusLabel.setColour(juce::Label::textColourId, AmbivalenceColors::TextSecondary.withAlpha(0.6f)); statusLabel.setJustificationType(juce::Justification::centredRight); addAndMakeVisible(statusLabel); // -- Preset UI (wiring is done by the editor) -- addAndMakeVisible(presetPrevButton); addAndMakeVisible(presetCombo); addAndMakeVisible(presetNextButton); addAndMakeVisible(presetSaveButton); addAndMakeVisible(presetLoadButton); addAndMakeVisible(presetDeleteButton); updatePanelVisibility(); } // ----------------------------------------------------------------------------- // EditorContent::updatePanelVisibility // ----------------------------------------------------------------------------- void EditorContent::updatePanelVisibility() { auto setKnob = [](ArcKnob& k, bool vis) { k.slider.setVisible(vis); k.label.setVisible(vis); }; const bool showNormal = !proMode; const bool showPro = proMode; setKnob(kPreDelay, showNormal); setKnob(kRoomSize, showNormal); setKnob(kDecay, showNormal); setKnob(kHFDamp, showNormal); setKnob(kLFAbsorb, showNormal); setKnob(kDiffusion, showNormal); setKnob(kModAmt, showNormal); setKnob(kModRate, showNormal); setKnob(kStereoW, showNormal); setKnob(kERLevel, showNormal); setKnob(kSaturation, showNormal); setKnob(kWet, showNormal); setKnob(kDry, showNormal); setKnob(kDuckAmt, showNormal); setKnob(kDuckThr, showNormal); setKnob(kDuckAtt, showNormal); setKnob(kDuckRel, showNormal); setKnob(kLoCutNorm, showNormal); setKnob(kHiCutNorm, showNormal); for (auto& k : kRTBands) setKnob(k, showPro); satTypeLabel.setVisible(showPro); satTypeCombo.setVisible(showPro); setKnob(kTiltLow, showPro); setKnob(kTiltMid, showPro); setKnob(kTiltHigh, showPro); setKnob(kLoCutPro, showPro); setKnob(kHiCutPro, showPro); // preset UI always visible presetPrevButton.setVisible(true); presetCombo.setVisible(true); presetNextButton.setVisible(true); presetSaveButton.setVisible(true); presetLoadButton.setVisible(true); presetDeleteButton.setVisible(true); } // ----------------------------------------------------------------------------- // EditorContent::setProMode // ----------------------------------------------------------------------------- void EditorContent::setProMode(bool pro) { if (proMode == pro) return; proMode = pro; updatePanelVisibility(); resized(); repaint(); } // ----------------------------------------------------------------------------- // EditorContent::resized // ----------------------------------------------------------------------------- void EditorContent::resized() { uiScale = getWidth() > 0 ? (float)getWidth() / (float)W : 1.0f; // scale fonts that are set explicitly (knob/button/combo fonts handled // via the LookAndFeel which already has the same uiScale pushed to it). auto labelFont = [this](float h, int style = juce::Font::plain) { return juce::Font(juce::FontOptions("Helvetica Neue", h * uiScale, style)); }; titleLabel.setFont(labelFont(24.f, juce::Font::bold)); satTypeLabel.setFont(labelFont(9.f)); labelMetricsTitle.setFont(labelFont(8.5f, juce::Font::bold)); labelD50Caption.setFont(labelFont(8.f)); labelC50Caption.setFont(labelFont(8.f)); labelC80Caption.setFont(labelFont(8.f)); labelEDTCaption.setFont(labelFont(8.f)); labelD50Value.setFont(labelFont(9.5f, juce::Font::bold)); labelC50Value.setFont(labelFont(9.5f, juce::Font::bold)); labelC80Value.setFont(labelFont(9.5f, juce::Font::bold)); labelEDTValue.setFont(labelFont(9.5f, juce::Font::bold)); statusLabel.setFont(labelFont(8.5f)); ArcKnob* knobs[] = { &kPreDelay, &kRoomSize, &kDecay, &kHFDamp, &kLFAbsorb, &kDiffusion, &kModAmt, &kModRate, &kStereoW, &kERLevel, &kSaturation, &kWet, &kDry, &kDuckAmt, &kDuckThr, &kDuckAtt, &kDuckRel, &kLoCutNorm, &kHiCutNorm, &kRTBands[0], &kRTBands[1], &kRTBands[2], &kRTBands[3], &kRTBands[4], &kRTBands[5], &kRTBands[6], &kRTBands[7], &kRTBands[8], &kRTBands[9], &kTiltLow, &kTiltMid, &kTiltHigh, &kLoCutPro, &kHiCutPro }; for (auto* k : knobs) k->label.setFont(labelFont(9.f)); if (lookAndFeel != nullptr) lookAndFeel->zoomScale = uiScale; decayCurveViz.setZoomScale(uiScale); // layout is written in fixed design coords (W x H); S() maps to the // actual component size so no host-hostile setTransform is needed. auto S = [this](int x, int y, int w, int h) { return juce::Rectangle(juce::roundToInt(x * uiScale), juce::roundToInt(y * uiScale), juce::roundToInt(w * uiScale), juce::roundToInt(h * uiScale)); }; titleLabel.setBounds(S(PAD, Y_HEADER - 6, 300, 44)); proModeButton.setBounds(S(316, Y_HEADER + 5, 52, 22)); erSoloButton.setBounds(S(376, Y_HEADER + 5, 72, 22)); vuIn.setBounds(S(W - 220, Y_HEADER + 2, 96, 28)); vuOut.setBounds(S(W - 120, Y_HEADER + 2, 96, 28)); zoomButton.setBounds(S(W - 272, Y_HEADER + 5, 48, 22)); auto place1 = [&](ArcKnob& k, int& x, int y) { k.label.setBounds(S(x, y, KNOB_W, KNOB_LBL_H)); k.slider.setBounds(S(x, y + KNOB_LBL_H, KNOB_W, KNOB_H)); x += KNOB_W + ROW1_GAP; }; auto place2 = [&](ArcKnob& k, int& x, int y) { k.label.setBounds(S(x, y, KNOB_W, KNOB_LBL_H)); k.slider.setBounds(S(x, y + KNOB_LBL_H, KNOB_W, KNOB_H)); x += KNOB_W + PAD; }; if (!proMode) { // -- Row 1 -- int kx = PAD; place1(kPreDelay, kx, Y_ROW1); place1(kRoomSize, kx, Y_ROW1); place1(kDecay, kx, Y_ROW1); place1(kHFDamp, kx, Y_ROW1); place1(kLFAbsorb, kx, Y_ROW1); place1(kDiffusion, kx, Y_ROW1); place1(kModAmt, kx, Y_ROW1); place1(kModRate, kx, Y_ROW1); place1(kStereoW, kx, Y_ROW1); place1(kERLevel, kx, Y_ROW1); place1(kSaturation, kx, Y_ROW1); // -- Row 2: MIX | OUT EQ | DUCKING -- kx = PAD; place2(kWet, kx, Y_ROW2); place2(kDry, kx, Y_ROW2); kx += 16; place2(kLoCutNorm, kx, Y_ROW2); place2(kHiCutNorm, kx, Y_ROW2); kx += 16; place2(kDuckAmt, kx, Y_ROW2); place2(kDuckThr, kx, Y_ROW2); place2(kDuckAtt, kx, Y_ROW2); place2(kDuckRel, kx, Y_ROW2); } else { // -- ProMode row 1 -- int kx = PAD; for (int i = 0; i < 10; ++i) place1(kRTBands[i], kx, Y_ROW1); // -- ProMode row 2 -- int kx2 = PAD; satTypeLabel.setBounds(S(kx2, Y_SLABEL2, KNOB_W, KNOB_LBL_H)); satTypeCombo.setBounds(S(kx2, Y_SLABEL2 + KNOB_LBL_H + 2, KNOB_W + PAD, 24)); kx2 += KNOB_W + PAD + PAD + 8; place2(kTiltLow, kx2, Y_ROW2); place2(kTiltMid, kx2, Y_ROW2); place2(kTiltHigh, kx2, Y_ROW2); kx2 += 16; place2(kLoCutPro, kx2, Y_ROW2); place2(kHiCutPro, kx2, Y_ROW2); } // ------------------------------------------------------------------------- // preset UI (always, mode-independent) // ------------------------------------------------------------------------- // layout (PRESET_PANEL_X=632 starting from ): // // top row Y=Y_ROW2: [<(26)] gap4 [combo(154)] gap4 [>(26)] -> right edge 848 // bottom row Y=Y_ROW2+34:[SAVE(104)] gap8 [DELETE(104)] -> right edge 848 // // separator vertical line : PRESET_PANEL_X - 9 = 623 // ------------------------------------------------------------------------- { const int px = PRESET_PANEL_X; const int btnH = 26; // top row presetPrevButton.setBounds(S(px, Y_ROW2, 26, btnH)); presetCombo.setBounds(S(px + 30, Y_ROW2, 154, btnH)); presetNextButton.setBounds(S(px + 188, Y_ROW2, 26, btnH)); // bottom row : [SAVE(68)] [LOAD(68)] [DELETE(68)] presetSaveButton.setBounds(S(px, Y_ROW2 + 34, 68, btnH)); presetLoadButton.setBounds(S(px + 72, Y_ROW2 + 34, 68, btnH)); presetDeleteButton.setBounds(S(px + 144, Y_ROW2 + 34, 68, btnH)); } // -- Visualizer -- const int decayHeight = H - Y_VIZ - STATUS_H - PAD; decayCurveViz.setBounds(S(PAD, Y_VIZ, W - PAD * 2, decayHeight)); // -- AcousticMetrics -- const int metricsRight = W - PAD - 8; const int metricsW = 280; const int metricsLeft = metricsRight - metricsW; const int metricsTop = Y_VIZ + 6; const int metricsRowH = 14; const int metricsRow1Y = metricsTop + 14; const int metricsRow2Y = metricsRow1Y + metricsRowH; const int captionW = 28; const int valueW = 52; const int colSpacing = 8; const int colW = captionW + valueW; labelMetricsTitle.setBounds(S(metricsLeft, metricsTop, 80, 12)); labelD50Caption.setBounds(S(metricsLeft, metricsRow1Y, captionW, metricsRowH)); labelD50Value.setBounds(S(metricsLeft + captionW, metricsRow1Y, valueW, metricsRowH)); labelC50Caption.setBounds(S(metricsLeft + colW + colSpacing, metricsRow1Y, captionW, metricsRowH)); labelC50Value.setBounds(S(metricsLeft + colW + colSpacing + captionW, metricsRow1Y, valueW, metricsRowH)); labelC80Caption.setBounds(S(metricsLeft, metricsRow2Y, captionW, metricsRowH)); labelC80Value.setBounds(S(metricsLeft + captionW, metricsRow2Y, valueW, metricsRowH)); labelEDTCaption.setBounds(S(metricsLeft + colW + colSpacing, metricsRow2Y, captionW, metricsRowH)); labelEDTValue.setBounds(S(metricsLeft + colW + colSpacing + captionW, metricsRow2Y, valueW, metricsRowH)); // -- Git build-info status bar -- statusLabel.setBounds(S(PAD, H - STATUS_H, W - PAD - 16, STATUS_H)); } // ----------------------------------------------------------------------------- // EditorContent::paint // ----------------------------------------------------------------------------- void EditorContent::paint(juce::Graphics& g) { g.fillAll(AmbivalenceColors::Background); g.addTransform(juce::AffineTransform::scale(uiScale)); juce::ColourGradient grad( AmbivalenceColors::Surface.withAlpha(0.12f), 0.f, 0.f, AmbivalenceColors::Background, 0.f, (float)H, false); g.setGradientFill(grad); g.fillRect(0.f, 0.f, (float)W, (float)H); g.setColour(AmbivalenceColors::Separator); g.drawHorizontalLine(Y_SEP, (float)PAD, (float)(W - PAD)); g.setFont(juce::Font(juce::FontOptions( "Helvetica Neue", 8.5f, juce::Font::bold))); auto sl = [&](int x, int y, const char* t) { g.drawText(t, x, y, 200, 14, juce::Justification::centredLeft); }; if (!proMode) { // -- Row 1 separator -- g.setColour(AmbivalenceColors::Separator); g.drawVerticalLine(SEP_TF, (float)Y_SLABEL1, (float)(Y_ROW1 + UNIT_H)); g.drawVerticalLine(SEP_FD, (float)Y_SLABEL1, (float)(Y_ROW1 + UNIT_H)); g.drawVerticalLine(SEP_DS, (float)Y_SLABEL1, (float)(Y_ROW1 + UNIT_H)); g.drawVerticalLine(SEP_SC, (float)Y_SLABEL1, (float)(Y_ROW1 + UNIT_H)); // -- Row 2 separator -- const int row2_outeq_x = PAD + 2 * (KNOB_W + PAD) + 16; const int row2_duck_x = row2_outeq_x + 2 * (KNOB_W + PAD) + 16; g.drawVerticalLine(row2_outeq_x - 9, (float)Y_SLABEL2, (float)(Y_ROW2 + UNIT_H)); g.drawVerticalLine(row2_duck_x - 9, (float)Y_SLABEL2, (float)(Y_ROW2 + UNIT_H)); // -- section -- g.setColour(AmbivalenceColors::Accent.withAlpha(0.75f)); sl(SEC_TIME, Y_SLABEL1, "TIME"); sl(SEC_FREQUENCY, Y_SLABEL1, "FREQUENCY"); sl(SEC_DIFFUSION, Y_SLABEL1, "DIFFUSION"); sl(SEC_STEREO, Y_SLABEL1, "STEREO"); sl(SEC_CHARACTER, Y_SLABEL1, "CHARACTER"); sl(PAD, Y_SLABEL2, "MIX"); sl(row2_outeq_x, Y_SLABEL2, "OUT EQ"); sl(row2_duck_x, Y_SLABEL2, "DUCKING"); } else { // -- ProMode -- g.setColour(AmbivalenceColors::Accent.withAlpha(0.75f)); sl(PAD, Y_SLABEL1, "RT60 PER BAND"); g.setColour(AmbivalenceColors::Separator.withAlpha(0.5f)); g.drawHorizontalLine(Y_SLABEL2 - 4, (float)PAD, (float)(W - PAD)); const int tilt_x = PAD + KNOB_W + PAD + PAD + 8; const int outeq_x = tilt_x + 3 * (KNOB_W + PAD) + 16; g.setColour(AmbivalenceColors::Separator); g.drawVerticalLine(outeq_x - 9, (float)Y_SLABEL2, (float)(Y_ROW2 + UNIT_H)); g.setColour(AmbivalenceColors::Accent.withAlpha(0.75f)); sl(tilt_x, Y_SLABEL2, "TILT EQ"); sl(outeq_x, Y_SLABEL2, "OUT EQ"); } // ------------------------------------------------------------------------- // preset section (always, mode-independent) // ------------------------------------------------------------------------- g.setColour(AmbivalenceColors::Separator); g.drawVerticalLine(PRESET_PANEL_X - 9, (float)Y_SLABEL2, (float)(Y_ROW2 + UNIT_H)); g.setColour(AmbivalenceColors::Accent.withAlpha(0.75f)); sl(PRESET_PANEL_X, Y_SLABEL2, "PRESET"); // -- Git build-info status bar divider -- g.setColour(AmbivalenceColors::Separator); g.drawHorizontalLine(H - STATUS_H - 1, (float)PAD, (float)(W - PAD)); } // ----------------------------------------------------------------------------- // EditorContent::Zoom control // ----------------------------------------------------------------------------- void EditorContent::showZoomMenu() { static constexpr float zoomLevels[] = { 0.75f, 1.0f, 1.5f, 2.0f, 2.5f, 3.0f }; juce::PopupMenu menu; for (int i = 0; i < 6; ++i) { const int pct = juce::roundToInt(zoomLevels[i] * 100.f); menu.addItem(juce::String(pct) + "%", true, juce::approximatelyEqual(zoomLevels[i], currentZoom), [this, level = zoomLevels[i]] { setZoomLevel(level); }); } menu.showMenuAsync(juce::PopupMenu::Options() .withTargetComponent(&zoomButton) .withStandardItemHeight(22)); } void EditorContent::setZoomLevel(float scale) { currentZoom = juce::jlimit(0.75f, 3.0f, scale); zoomButton.setButtonText( juce::String(juce::roundToInt(currentZoom * 100.f)) + "%"); if (onZoomChange) onZoomChange(currentZoom); } // ----------------------------------------------------------------------------- // constructor // ----------------------------------------------------------------------------- FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p) : AudioProcessorEditor(&p), audioProcessor(p) { setLookAndFeel(&laf); setSize(juce::roundToInt((float)EditorContent::W * EditorContent::kDefaultZoom), juce::roundToInt((float)EditorContent::H * EditorContent::kDefaultZoom)); content = std::make_unique(p, laf); addAndMakeVisible(content.get()); // setSize() in the constructor runs resized() while content is still // null, and hosts open the editor at exactly this size, so resized() // would otherwise never run again - leaving content at 0x0 (black // canvas). Lay it out explicitly here. resized(); // when the user picks a zoom level, resize the (untransformed) editor so // it exactly matches the scaled content. resized() derives the content // scale from the actual editor width, so hosts that clamp the window // still end up with a correct, fully-covered UI. content->onZoomChange = [this](float z) { setSize(juce::roundToInt((float)EditorContent::W * z), juce::roundToInt((float)EditorContent::H * z)); }; // -- Preset UI wiring -- presetManager = std::make_unique(p); content->zoomButton.onClick = [this] { content->showZoomMenu(); }; content->presetPrevButton.setButtonText("<"); content->presetPrevButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface); content->presetPrevButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextPrimary); content->presetPrevButton.onClick = [this] { presetManager->loadPrevPreset(); }; content->presetCombo.setLookAndFeel(&laf); content->presetCombo.onChange = [this] { int idx = content->presetCombo.getSelectedItemIndex() - FDNReverb::NUM_ALGORITHMS; auto names = presetManager->getPresetNames(); if (idx >= 0 && idx < names.size()) presetManager->loadPreset(names[idx]); }; content->presetNextButton.setButtonText(">"); content->presetNextButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface); content->presetNextButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextPrimary); content->presetNextButton.onClick = [this] { presetManager->loadNextPreset(); }; content->presetSaveButton.setButtonText("SAVE"); content->presetSaveButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Accent.withAlpha(0.75f)); content->presetSaveButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::Background); content->presetSaveButton.onClick = [this] { savePresetWithDialog(); }; content->presetLoadButton.setButtonText("LOAD"); content->presetLoadButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::AccentBlue.withAlpha(0.75f)); content->presetLoadButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::Background); content->presetLoadButton.onClick = [this] { int idx = content->presetCombo.getSelectedItemIndex() - FDNReverb::NUM_ALGORITHMS; auto names = presetManager->getPresetNames(); if (idx >= 0 && idx < names.size()) presetManager->loadPreset(names[idx]); }; content->presetDeleteButton.setButtonText("DELETE"); content->presetDeleteButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface); content->presetDeleteButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextSecondary); content->presetDeleteButton.onClick = [this] { deleteCurrentPreset(); }; // callback setup presetManager->onPresetListChanged = [this] { refreshPresetCombo(); }; // * fix : whenever the preset name changes Processor notify presetManager->onPresetLoaded = [this](const juce::String& name) { audioProcessor.setLastSavedPresetName(name); refreshPresetCombo(); }; refreshPresetCombo(); startTimerHz(60); } // ----------------------------------------------------------------------------- // destructor // ----------------------------------------------------------------------------- FDNReverbEditor::~FDNReverbEditor() { stopTimer(); setLookAndFeel(nullptr); content->satTypeCombo.setLookAndFeel(nullptr); content->presetCombo.setLookAndFeel(nullptr); } // ----------------------------------------------------------------------------- // timerCallback // ----------------------------------------------------------------------------- void FDNReverbEditor::timerCallback() { content->vuIn.setLevels(audioProcessor.getInputRMSL(), audioProcessor.getInputRMSR()); content->vuOut.setLevels(audioProcessor.getOutputRMSL(), audioProcessor.getOutputRMSR()); content->vuIn.repaint(); content->vuOut.repaint(); static int metricsCounter = 0; if (++metricsCounter >= 2) { metricsCounter = 0; content->labelD50Value.setText( juce::String(audioProcessor.getD50() * 100.0f, 1) + "%", juce::dontSendNotification); content->labelC50Value.setText( juce::String(audioProcessor.getC50(), 1) + "dB", juce::dontSendNotification); content->labelC80Value.setText( juce::String(audioProcessor.getC80(), 1) + "dB", juce::dontSendNotification); content->labelEDTValue.setText( juce::String(audioProcessor.getEDT(), 2) + "s", juce::dontSendNotification); } bool newProMode = (*audioProcessor.apvts.getRawParameterValue("promode") > 0.5f); content->setProMode(newProMode); } // ----------------------------------------------------------------------------- // resized // ----------------------------------------------------------------------------- void FDNReverbEditor::resized() { if (content == nullptr) return; // No setTransform here: scaling is done by EditorContent applying // uiScale to every child bounds and to its own paint() graphics, which // renders correctly in all hosts (some hosts, e.g. FL Studio, fail to // composite a transformed top-level child and show a blank frame). content->setBounds(getLocalBounds()); } // ----------------------------------------------------------------------------- // paint // ----------------------------------------------------------------------------- void FDNReverbEditor::paint(juce::Graphics& g) { g.fillAll(AmbivalenceColors::Background); } // ----------------------------------------------------------------------------- // Preset UI helpers // ----------------------------------------------------------------------------- void FDNReverbEditor::refreshPresetCombo() { content->presetCombo.clear(juce::dontSendNotification); auto names = presetManager->getPresetNames(); // --------------------------------------------------------------------- // * fix: restore the preset name saved in the Processor at startup // --------------------------------------------------------------------- // when the editor is closed the PresetManager is destroyed, // so currentPresetName becomes empty. // the name persisted in the Processor as lastSavedPresetName is // re-set in the new PresetManager, // so the combo box selection is restored correctly. // --------------------------------------------------------------------- if (presetManager->getCurrentPresetName().isEmpty()) { auto saved = audioProcessor.getLastSavedPresetName(); if (saved.isNotEmpty()) presetManager->setCurrentPresetName(saved); } // -- built-in algorithm presets -- content->presetCombo.addSectionHeading("ALGORITHM"); static const char* algoNames[FDNReverb::NUM_ALGORITHMS] = { "ROOM1", "ROOM2", "HALL1", "HALL2", "PLATE", "SPRING", "GOLDFOIL" }; for (int i = 0; i < FDNReverb::NUM_ALGORITHMS; ++i) content->presetCombo.addItem(algoNames[i], i + 1); // -- saved user presets -- if (names.isEmpty()) { const int placeholderId = FDNReverb::NUM_ALGORITHMS + 1; content->presetCombo.addItem("-- No Presets --", placeholderId); content->presetCombo.setSelectedId(placeholderId, juce::dontSendNotification); content->presetDeleteButton.setEnabled(false); content->presetLoadButton.setEnabled(false); content->presetPrevButton.setEnabled(false); content->presetNextButton.setEnabled(false); return; } // user-preset IDs start after the algorithm IDs so no two combo items // share an ID (JUCE ticks every item whose ID matches the selection). for (int i = 0; i < names.size(); ++i) content->presetCombo.addItem(names[i], FDNReverb::NUM_ALGORITHMS + i + 1); int idx = presetManager->getCurrentPresetIndex(); if (idx >= 0) content->presetCombo.setSelectedItemIndex(idx, juce::dontSendNotification); else content->presetCombo.setSelectedItemIndex(0, juce::dontSendNotification); content->presetDeleteButton.setEnabled(true); content->presetLoadButton.setEnabled(true); content->presetPrevButton.setEnabled(names.size() > 1); content->presetNextButton.setEnabled(names.size() > 1); } void FDNReverbEditor::savePresetWithDialog() { // --------------------------------------------------------------------- // AlertWindow preset name input dialog // uses enterModalState(async callback): no runModalLoop() needed // SafePointer handles the case where the editor is destroyed first // --------------------------------------------------------------------- auto* dialog = new juce::AlertWindow( "Save Preset", "Enter a name for this preset:", juce::MessageBoxIconType::NoIcon); dialog->addTextEditor("name", presetManager->getCurrentPresetName()); dialog->addButton("Save", 1, juce::KeyPress(juce::KeyPress::returnKey)); dialog->addButton("Cancel", 0, juce::KeyPress(juce::KeyPress::escapeKey)); juce::Component::SafePointer safeThis(this); dialog->enterModalState( true, juce::ModalCallbackFunction::create( [safeThis, dialog](int result) { if (safeThis != nullptr && result == 1) { auto name = dialog->getTextEditorContents("name").trim(); if (name.isNotEmpty()) safeThis->presetManager->savePreset(name); } }), true // deleteWhenDismissed ); } void FDNReverbEditor::deleteCurrentPreset() { auto name = presetManager->getCurrentPresetName(); if (name.isEmpty()) return; auto* dialog = new juce::AlertWindow( "Delete Preset", "Delete \"" + name + "\"?", juce::MessageBoxIconType::WarningIcon); dialog->addButton("Delete", 1); dialog->addButton("Cancel", 0, juce::KeyPress(juce::KeyPress::escapeKey)); juce::Component::SafePointer safeThis(this); dialog->enterModalState( true, juce::ModalCallbackFunction::create( [safeThis, name](int result) { if (safeThis != nullptr && result == 1) safeThis->presetManager->deletePreset(name); }), true ); }