#include "PluginEditor.h" namespace serum { namespace { // ----------------------------------------------------------------------- // Consistent spacing system. Every panel, row and control position below is // derived from these constants so the tabs share identical margins, padding // and row pitches. // ----------------------------------------------------------------------- namespace layout { constexpr int margin = 10; // panels inset from the tab view edge constexpr int padding = 8; // controls inset inside panels + row rhythm constexpr int gap = 6; // horizontal spacing between standard knobs constexpr int knobSize = 76; // standard knob footprint (incl. label) constexpr int knobSmall = 44; // small knob width (env / LFO rows) constexpr int rowHeight = knobSize + padding; // vertical pitch of knob rows constexpr int panelSpacing = 10; // gap between adjacent panels // Derived from the visual style (panel title bar and control sizes) so the // content grid stays aligned across every tab. constexpr int titleHeight = 22; // panel title bar height constexpr int comboHeight = 24; // combo box height constexpr int buttonHeight = 26; // text button height constexpr int toggleWidth = 40; // "On"/"Sync" toggle width constexpr int toggleHeight = 36; // toggle height (LED + visible label) constexpr int toggleWide = 60; // wider toggle for a word like "Bipolar" constexpr int headerHeight = toggleHeight; // top control row height constexpr int depthKnobHeight = 60; // matrix / macro depth knob height // The header row is centred on the depth knob, which is taller than a // toggle, so the row centre follows the knob and not headerHeight. constexpr int headerCentreY = titleHeight + depthKnobHeight / 2; constexpr int displayHeight = 66; // waveform / filter display height constexpr int envDisplayHeight = 56; // envelope display height constexpr int lfoDisplayHeight = 64; // LFO display height constexpr int smallKnobHeight = 60; // height of 44px-wide small knobs constexpr int smallGap = 3; // horizontal spacing between small knobs constexpr int arrowWidth = 26; // FX up/down arrow button width constexpr int macroKnobSize = 120; // macro knob footprint // Tab content starts below the top bar, so every view shares one origin. constexpr int tabBarHeight = 60; // Panel grids. Each tab lays its panels out in a fixed number of equal // columns, so the column width lives here rather than beside the code // that happens to use it. constexpr int twoColW = 545; // two equal columns (OSC, FX) constexpr int threeColW = 360; // three equal columns (FILTER) constexpr int fourColW = 269; // four equal columns (MOD, MACRO) constexpr int fourColGap = 8; // MOD and MACRO pack their columns tighter constexpr int oscPanelH = 308; // oscillator panel height constexpr int smallPanelH = 150; // sub, noise and FX slot panel height constexpr int filterPanelH = 300; // filter panel height constexpr int envPanelH = 154; // envelope panel height constexpr int lfoPanelH = 206; // LFO panel height constexpr int macroPanelH = 158; // macro panel height } // Horizontal position of a grid column (0-based). inline int gridX (int col, int cellW, int cellGap) { return layout::margin + col * (cellW + cellGap); } // Y position of a combo box centred on a panel's header row. inline int comboRowY() { return layout::titleHeight + (layout::headerHeight - layout::comboHeight) / 2; } // Y position of the first content row below a panel's header row. inline int bodyTop() { return layout::titleHeight + layout::headerHeight + layout::padding; } juce::StringArray wavetableItems() { juce::StringArray a; for (auto n : kWavetableNames) a.add (n); return a; } juce::StringArray modSourceItems() { juce::StringArray a; for (int i = 0; i < kNumModSources; ++i) a.add (modSourceName ((ModSource) i)); return a; } juce::StringArray modTargetItems (std::vector& enums) { juce::StringArray a; for (int i = 0; i < kNumModTargets; ++i) { const auto t = (ModTarget) i; if (t == ModTarget::None) continue; a.add (modTargetName (t)); enums.push_back (t); } return a; } juce::String fmtUnison (float v) { return juce::String (1 + (int) std::llround (v * 15.0f)); } // ----------------------------------------------------------------------- // Display formatters. One per physical unit the editor shows. Each takes a // normalised 0..1 parameter and returns the value the DSP reads, with its // unit, so a readout and a tooltip can state the same range. // ----------------------------------------------------------------------- juce::String fmtGainDb (float v) { return juce::String ((v - 0.5f) * 30.0f, 1) + " dB"; } juce::String fmtGainDb12 (float v) { return juce::String ((v - 0.5f) * 24.0f, 1) + " dB"; } juce::String fmtCompThreshold (float v) { return juce::String (-40.0f + v * 40.0f, 1) + " dB"; } juce::String fmtCompRatio (float v) { return juce::String (2.0f + v * 18.0f, 1) + ":1"; } juce::String fmtCompAttack (float v) { return juce::String (2.0f + v * 98.0f, 0) + " ms"; } juce::String fmtCompRelease (float v) { return juce::String (30.0f + v * 470.0f, 0) + " ms"; } juce::String fmtDelayTime (float v) { return juce::String (maps::delayToMs (v), 0) + " ms"; } juce::String fmtLfoRate (float v) { return juce::String (maps::rateToHz (v), 2) + " Hz"; } juce::String fmtLfoFade (float v) { return juce::String (v * 8.0f, 2) + " s"; } juce::String fmtLfoDelay (float v) { return juce::String (v * 4.0f, 2) + " s"; } // Beat division, as the period the LFO holds for one cycle. juce::String fmtLfoBeat (float v) { static const char* const names[16] = { "1/32", "1/16", "1/8", "1/6", "1/4", "1/3", "1/2", "1", "1.5", "2", "3", "4", "6", "8", "12", "16" }; const int idx = juce::jlimit (0, 15, (int) std::llround (v * 15.0f)); return juce::String ("1 cycle = ") + names[idx] + " beat" + (idx == 7 ? "" : "s"); } // Phase and random phase are fractions of one cycle. juce::String fmtDegrees (float v) { return juce::String (juce::roundToInt (v * 360.0f)) + " deg"; } // Filter output is a gain of 0 to 1.5, so 100 % is 1.5x, not unity. juce::String fmtFilterOut (float v) { return juce::String (juce::roundToInt (v * 150.0f)) + " %"; } // Modulation depth spans -100 % to +100 %; a negative depth inverts. juce::String fmtModDepth (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + " %"; } // Beat divisions, indexed the same way as maps::beatToMultiplier. juce::String formatFrequency (float hz) { if (hz >= 1000.0f) return juce::String (hz / 1000.0f, 1) + " kHz"; return juce::String (hz, hz < 100.0f ? 1 : 0) + " Hz"; } juce::String fmtChorusRate (float v) { return formatFrequency (0.1f + v * 4.9f); } juce::String fmtFlangerRate (float v) { return formatFrequency (0.05f + v * 2.0f); } juce::String fmtPhaserRate (float v) { return formatFrequency (0.05f + v * 2.0f); } juce::String fmtDistTone (float v) { return formatFrequency (800.0f + v * 15200.0f); } juce::String fmtEqMidFreq (float v) { return formatFrequency (200.0f + v * 4800.0f); } // Chorus and flanger depth is a delay excursion in milliseconds. juce::String fmtChorusDepth (float v) { return juce::String (0.5f + v * 5.5f, 2) + " ms"; } juce::String fmtFlangerDepth (float v) { return juce::String (0.3f + v * 4.7f, 2) + " ms"; } juce::String fmtDistDrive (float v) { return juce::String (1.0f + v * 24.0f, 1) + "x"; } juce::String fmtDistOutput (float v) { return juce::String (0.4f + v * 1.2f, 2) + "x"; } juce::String fmtHyperOutput (float v) { return juce::String (0.5f + v, 2) + "x"; } juce::String fmtPhaserStages (float v) { return juce::String (juce::jlimit (2, 6, 2 + (int) (v * 4.0f + 0.5f))) + " stages"; } juce::String fmtPingPong (float v) { return v > 0.5f ? "On" : "Off"; } juce::String fmtPredelayMs (float v) { return juce::String (v * 150.0f, 0) + " ms"; } void placeKnobs (std::vector& knobs, int x, int y, int w, int h, int gap = layout::gap) { for (int i = 0; i < (int) knobs.size(); ++i) knobs[(size_t) i]->setBounds (x + i * (w + gap), y, w, h); } constexpr auto& kFxType = paramIds::fxType; constexpr auto& kFxMix = paramIds::fxMix; constexpr auto& kFxP1 = paramIds::fxP1; constexpr auto& kFxP2 = paramIds::fxP2; constexpr auto& kFxP3 = paramIds::fxP3; constexpr auto& kFxP4 = paramIds::fxP4; // How one FX slot parameter is presented: the name under the knob, the text // that converts its normalised value to physical units, and the tooltip that // states what it does and its range. struct FxParamDisplay { const char* name; const char* tooltip; std::function formatter; }; // Indexed by FxType then by parameter slot. Names and ranges come from the // effect that reads the parameter in Source/EffectUnits/. const FxParamDisplay kFxParamDisplay[(int) FxType::Count][4] = { // Off: the rack skips the slot entirely, so say so rather than showing // four knobs that do nothing. { { "Unused", "Effect is off, this slot does nothing", formatPercent }, { "Unused", "Effect is off, this slot does nothing", formatPercent }, { "Unused", "Effect is off, this slot does nothing", formatPercent }, { "Unused", "Effect is off, this slot does nothing", formatPercent } }, { { "Intensity", "How far the multiband up and down compression goes, 0 to 100 %", formatPercent }, { "Low Amount", "How much of the low band is fed back in, 40 to 100 %", formatPercent }, { "High Amount", "How much of the high band is fed back in, 40 to 100 %", formatPercent }, { "Output", "Output gain, 0.5 to 1.5x", fmtHyperOutput } }, { { "Rate", "Modulation speed, 0.1 Hz to 5 Hz", fmtChorusRate }, { "Depth", "Pitch swing, 0.5 ms to 6 ms of delay", fmtChorusDepth }, { "Width", "Stereo width of the modulated copy, 0 to 100 %", formatPercent }, { "Unused", "Not used by this effect", formatPercent } }, { { "Rate", "Modulation speed, 0.05 Hz to 2.05 Hz", fmtFlangerRate }, { "Depth", "Pitch swing, 0.3 ms to 5 ms of delay", fmtFlangerDepth }, { "Feedback", "How much output is fed back in, 0 to 90 %", formatPercent }, { "Unused", "Not used by this effect", formatPercent } }, { { "Rate", "Modulation speed, 0.05 Hz to 2.05 Hz", fmtPhaserRate }, { "Depth", "Sweep depth, 30 to 90 %", formatPercent }, { "Feedback", "How much output is fed back in, 0 to 70 %", formatPercent }, { "Stages", "Number of all-pass stages, 2 to 6", fmtPhaserStages } }, { { "Drive", "Input gain into the shaper, 1x to 25x", fmtDistDrive }, { "Shape", "Clipping shape. Low is soft, middle is hard, high folds.", formatPercent }, { "Tone", "Tone filter cutoff, 800 Hz to 16 kHz", fmtDistTone }, { "Output", "Output gain, 0.4x to 1.6x", fmtDistOutput } }, { { "Low Gain", "Low band gain, -15 dB to +15 dB", fmtGainDb }, { "Mid Gain", "Mid band gain, -12 dB to +12 dB", fmtGainDb12 }, { "High Gain", "High band gain, -15 dB to +15 dB", fmtGainDb }, { "Mid Freq", "Mid band centre frequency, 200 Hz to 5 kHz", fmtEqMidFreq } }, { { "Threshold", "Level where compression starts, -40 dB to 0 dB", fmtCompThreshold }, { "Ratio", "Input to output ratio above the threshold, 2:1 to 20:1", fmtCompRatio }, { "Attack", "How fast the compressor reacts, 2 ms to 100 ms", fmtCompAttack }, { "Release", "How fast the compressor lets go, 30 ms to 500 ms", fmtCompRelease } }, { { "Time", "Delay time, 1 ms to 2000 ms", fmtDelayTime }, { "Feedback", "How much output is fed back in, 0 to 92 %", formatPercent }, { "Damping", "How much the repeats lose their top end, 0 to 95 %", formatPercent }, { "Ping-Pong", "Bounces the repeats between left and right, on or off", fmtPingPong } }, { { "Size", "Reverb room size, 20 to 99 %", formatPercent }, { "Damping", "How much the tail loses its top end, 0 to 100 %", formatPercent }, { "Width", "Stereo width of the tail, 0 to 100 %", formatPercent }, { "Predelay", "Gap before the reverb starts, 0 ms to 150 ms", fmtPredelayMs } } }; } // --------------------------------------------------------------------------- PluginEditor::PluginEditor (SerumAltAudioProcessor& p) : AudioProcessorEditor (p), processor (p), tooltipWindow (this, 500), masterKnob ("Master", formatPercent) { // Tab / Shift+Tab cycling is handled by keyPressed(); this lets the editor // receive keyboard focus when the user clicks anywhere on it. setWantsKeyboardFocus (true); root.setBounds (0, 0, kBaseW, kBaseH); addAndMakeVisible (root); logo = createLogoDrawable(); buildTopBar(); buildOscTab(); buildFilterTab(); buildModTab(); buildFxTab(); buildMacroTab(); setTab (0); startTimerHz (30); } PluginEditor::~PluginEditor() { stopTimer(); } // --------------------------------------------------------------------------- void PluginEditor::paint (juce::Graphics& g) { g.fillAll (theme::bg); g.setGradientFill (juce::ColourGradient (theme::panel, 0.0f, 0.0f, theme::bg, 0.0f, (float) getHeight(), false)); g.fillAll(); if (logo != nullptr) logo->drawWithin (g, juce::Rectangle (10.0f, 8.0f, 180.0f, 44.0f), juce::RectanglePlacement::centred, 1.0f); } void PluginEditor::resized() { root.setBounds (0, 0, kBaseW, kBaseH); } // --------------------------------------------------------------------------- Knob* PluginEditor::makeKnob (juce::Component* parent, const juce::String& name, const juce::String& paramId, std::function fmt, const juce::String& tooltip) { auto control = std::make_unique (name, std::move (fmt)); auto* k = control.get(); ownedControls.push_back (std::move (control)); parent->addAndMakeVisible (k); if (auto* param = paramId.isNotEmpty() ? processor.parameters.getParameter (paramId) : nullptr) { // A double click restores the parameter's own default. Without this a // double click writes the knob's built-in 0.5 and silently changes any // parameter whose default is not 0.5. k->setDefaultValue (param->convertFrom0to1 (param->getDefaultValue())); sliderAttachments.push_back (std::make_unique ( processor.parameters, paramId, *k)); } k->setTooltip (tooltip.isNotEmpty() ? tooltip : name); return k; } juce::ComboBox* PluginEditor::makeCombo (juce::Component* parent, const juce::String& paramId, const juce::StringArray& items, const juce::String& tooltip) { auto control = std::make_unique(); auto* c = control.get(); ownedControls.push_back (std::move (control)); c->addItemList (items, 1); c->setSelectedItemIndex (0, juce::dontSendNotification); parent->addAndMakeVisible (c); if (paramId.isNotEmpty() && processor.parameters.getParameter (paramId) != nullptr) comboAttachments.push_back (std::make_unique ( processor.parameters, paramId, *c)); if (tooltip.isNotEmpty()) c->setTooltip (tooltip); return c; } ToggleButton* PluginEditor::makeToggle (juce::Component* parent, const juce::String& label, const juce::String& paramId, const juce::String& tooltip) { auto control = std::make_unique (label); auto* t = control.get(); ownedControls.push_back (std::move (control)); parent->addAndMakeVisible (t); if (paramId.isNotEmpty()) t->attach (processor.parameters, paramId); t->setTooltip (tooltip.isNotEmpty() ? tooltip : label); return t; } // --------------------------------------------------------------------------- void PluginEditor::buildTopBar() { // Preset selector. juce::StringArray presetNames; for (int i = 0; i < processor.getNumPrograms(); ++i) presetNames.add (processor.getProgramName (i)); presetCombo.addItemList (presetNames, 1); presetCombo.setSelectedItemIndex (processor.getCurrentProgram(), juce::dontSendNotification); presetCombo.onChange = [this] { processor.setCurrentProgram (presetCombo.getSelectedItemIndex()); }; root.addAndMakeVisible (presetCombo); presetCombo.setBounds (200, 16, 220, 24); presetCombo.setTooltip ("Loads a factory preset and replaces the current patch"); // UI scale. scaleCombo.addItemList ({ "75%", "100%", "125%", "150%", "200%" }, 1); scaleCombo.setSelectedItemIndex (processor.getUiScaleIndex(), juce::dontSendNotification); scaleCombo.onChange = [this] { processor.setUiScaleIndex (scaleCombo.getSelectedItemIndex()); }; root.addAndMakeVisible (scaleCombo); scaleCombo.setBounds (980, 16, 70, 24); scaleCombo.setTooltip ("Interface scale, 75 % to 200 %"); // RAVE. raveButton.setLabel ("Rave"); raveButton.setOnColour (theme::raveGlow); raveButton.setBounds (1062, 2, 50, 56); root.addAndMakeVisible (raveButton); raveButton.setTooltip ("Adds unison, width, drive, OTT and reverb to the current patch"); // RAVE uses a callback rather than a direct parameter attachment. raveButton.setOnClick ([this] (bool on) { processor.setRaveEnabled (on); }); // Tab bar. juce::TextButton* tabs[5] = { &tabOsc, &tabFilter, &tabMod, &tabFx, &tabMacro }; const char* tabNames[5] = { "Osc", "Filter", "Mod", "FX", "Macro" }; const char* tabTips[5] = { "Oscillators. Tab cycles sections.", "Filters. Tab cycles sections.", "Modulation. Envelopes, LFOs and the routing matrix.", "Effects rack. Tab cycles sections.", "Macros. Tab cycles sections." }; for (int i = 0; i < 5; ++i) { tabs[i]->setButtonText (tabNames[i]); tabs[i]->setClickingTogglesState (true); tabs[i]->setRadioGroupId (1001); tabs[i]->setBounds (10 + i * 92, 36, 84, 22); tabs[i]->onClick = [this, i] { setTab (i); }; tabs[i]->setTooltip (tabTips[i]); root.addAndMakeVisible (tabs[i]); } // Master. masterKnob.setBounds (906, 0, 64, 60); masterKnob.setTooltip ("Master output level, 0 % to 100 %"); root.addAndMakeVisible (masterKnob); sliderAttachments.push_back (std::make_unique ( processor.parameters, ids::master, masterKnob)); masterDisplay.setTitle ("MASTER"); root.addAndMakeVisible (masterDisplay); masterDisplay.setBounds (846, 8, 56, 44); } void PluginEditor::buildOscTab() { oscView.setBounds (0, layout::tabBarHeight, kBaseW, kBaseH - layout::tabBarHeight); root.addChildComponent (oscView); constexpr int colW = layout::twoColW; constexpr int oscH = layout::oscPanelH; constexpr int subH = layout::smallPanelH; oscAPanel.setBounds (gridX (0, colW, layout::panelSpacing), layout::margin, colW, oscH); oscBPanel.setBounds (gridX (1, colW, layout::panelSpacing), layout::margin, colW, oscH); subPanel.setBounds (gridX (0, colW, layout::panelSpacing), layout::margin + oscH + layout::panelSpacing, colW, subH); noisePanel.setBounds (gridX (1, colW, layout::panelSpacing), layout::margin + oscH + layout::panelSpacing, colW, subH); for (auto* panel : { &oscAPanel, &oscBPanel, &subPanel, &noisePanel }) oscView.addAndMakeVisible (panel); const int comboY = comboRowY(); const int displayY = bodyTop(); // below header row const int row1Y = displayY + layout::displayHeight + layout::padding; const int row2Y = row1Y + layout::rowHeight; // --- Oscillator A --- makeToggle (&oscAPanel, "On", ids::oscAOn, "Turns oscillator A on or off")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight); makeCombo (&oscAPanel, ids::oscAWave, wavetableItems(), "Selects the wavetable, 10 factory tables")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 200, layout::comboHeight); makeCombo (&oscAPanel, ids::oscAWarp, { "None", "Bend+", "Bend-", "Sync", "PWM", "Asym", "Mirror", "Fold" }, "Selects the wavetable warp mode") ->setBounds (layout::padding + layout::toggleWidth + layout::gap + 200 + layout::gap, comboY, 130, layout::comboHeight); oscAWave.setWavetables (&processor.engine.getWavetables()); oscAPanel.addAndMakeVisible (oscAWave); oscAWave.setBounds (layout::padding, displayY, colW - 2 * layout::padding, layout::displayHeight); std::vector rowA1 = { makeKnob (&oscAPanel, "Wavetable Position", ids::oscAWtPos, formatPercent, "Wavetable position (0-100%)"), makeKnob (&oscAPanel, "Warp Amount", ids::oscAWarpAmt, formatPercent, "Warp amount (0-100%)"), makeKnob (&oscAPanel, "Coarse", ids::oscACoarse, formatSemis, "Pitch coarse (-24 to +24 semitones)"), makeKnob (&oscAPanel, "Fine", ids::oscAFine, formatCents, "Pitch fine (-100 to +100 cents)"), makeKnob (&oscAPanel, "Level", ids::oscALevel, formatPercent, "Oscillator A level (0-100%)"), makeKnob (&oscAPanel, "Pan", ids::oscAPan, formatPan, "Oscillator A pan (L100-R100)") }; placeKnobs (rowA1, layout::padding, row1Y, layout::knobSize, layout::knobSize); std::vector rowA2 = { makeKnob (&oscAPanel, "Unison", ids::oscAUnison, fmtUnison, "Unison voices (1-16)"), makeKnob (&oscAPanel, "Detune", ids::oscADetune, formatCents, "Unison detune spread, 0 to 50 cents"), makeKnob (&oscAPanel, "Spread", ids::oscASpread, formatPercent, "Unison stereo spread (0-100%)"), makeKnob (&oscAPanel, "Phase", ids::oscAPhase, fmtDegrees, "Start phase at note on, 0 to 360 degrees"), makeKnob (&oscAPanel, "Rand phase", ids::oscARandPh, fmtDegrees, "Random start phase per voice, 0 to 360 degrees") }; placeKnobs (rowA2, layout::padding, row2Y, layout::knobSize, layout::knobSize); // Unison is a whole number of voices, so the dial should rest on a detent. rowA2[0]->setRange (0.0, 1.0, 1.0 / (kMaxUnison - 1)); // --- Oscillator B --- makeToggle (&oscBPanel, "On", ids::oscBOn, "Turns oscillator B on or off")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight); makeCombo (&oscBPanel, ids::oscBWave, wavetableItems(), "Selects the wavetable, 10 factory tables")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 200, layout::comboHeight); makeCombo (&oscBPanel, ids::oscBWarp, { "None", "Bend+", "Bend-", "Sync", "PWM", "Asym", "Mirror", "Fold" }, "Selects the wavetable warp mode") ->setBounds (layout::padding + layout::toggleWidth + layout::gap + 200 + layout::gap, comboY, 130, layout::comboHeight); oscBWave.setWavetables (&processor.engine.getWavetables()); oscBPanel.addAndMakeVisible (oscBWave); oscBWave.setBounds (layout::padding, displayY, colW - 2 * layout::padding, layout::displayHeight); std::vector rowB1 = { makeKnob (&oscBPanel, "Wavetable Position", ids::oscBWtPos, formatPercent, "Wavetable position (0-100%)"), makeKnob (&oscBPanel, "Warp Amount", ids::oscBWarpAmt, formatPercent, "Warp amount (0-100%)"), makeKnob (&oscBPanel, "Coarse", ids::oscBCoarse, formatSemis, "Pitch coarse (-24 to +24 semitones)"), makeKnob (&oscBPanel, "Fine", ids::oscBFine, formatCents, "Pitch fine (-100 to +100 cents)"), makeKnob (&oscBPanel, "Level", ids::oscBLevel, formatPercent, "Oscillator B level (0-100%)"), makeKnob (&oscBPanel, "Pan", ids::oscBPan, formatPan, "Oscillator B pan (L100-R100)") }; placeKnobs (rowB1, layout::padding, row1Y, layout::knobSize, layout::knobSize); std::vector rowB2 = { makeKnob (&oscBPanel, "Unison", ids::oscBUnison, fmtUnison, "Unison voices (1-16)"), makeKnob (&oscBPanel, "Detune", ids::oscBDetune, formatCents, "Unison detune spread, 0 to 50 cents"), makeKnob (&oscBPanel, "Spread", ids::oscBSpread, formatPercent, "Unison stereo spread (0-100%)"), makeKnob (&oscBPanel, "Phase", ids::oscBPhase, fmtDegrees, "Start phase at note on, 0 to 360 degrees"), makeKnob (&oscBPanel, "Rand phase", ids::oscBRandPh, fmtDegrees, "Random start phase per voice, 0 to 360 degrees") }; placeKnobs (rowB2, layout::padding, row2Y, layout::knobSize, layout::knobSize); rowB2[0]->setRange (0.0, 1.0, 1.0 / (kMaxUnison - 1)); // --- Sub --- makeToggle (&subPanel, "On", ids::subOn, "Turns the sub oscillator on or off")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight); makeCombo (&subPanel, ids::subShape, { "Sine", "Triangle" }, "Sub oscillator waveform, sine or triangle")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 130, layout::comboHeight); makeCombo (&subPanel, ids::subOct, { "-2 oct", "-1 oct", "0 oct" }, "Sub oscillator octave, 2 below to unison")->setBounds (layout::padding + layout::toggleWidth + layout::gap + 130 + layout::gap, comboY, 90, layout::comboHeight); makeKnob (&subPanel, "Level", ids::subLevel, formatPercent, "Sub oscillator level (0-100%)")->setBounds (layout::padding, displayY, layout::knobSize, layout::knobSize); // --- Noise --- makeToggle (&noisePanel, "On", ids::noiseOn, "Turns noise on or off")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight); makeCombo (&noisePanel, ids::noiseType, { "White", "Pink" }, "Noise colour, white or pink")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 130, layout::comboHeight); makeKnob (&noisePanel, "Level", ids::noiseLevel, formatPercent, "Noise level (0-100%)")->setBounds (layout::padding, displayY, layout::knobSize, layout::knobSize); } void PluginEditor::buildFilterTab() { filterView.setBounds (0, layout::tabBarHeight, kBaseW, kBaseH - layout::tabBarHeight); root.addChildComponent (filterView); constexpr int colW = layout::threeColW; constexpr int panelH = layout::filterPanelH; filter1Panel.setBounds (gridX (0, colW, layout::panelSpacing), layout::margin, colW, panelH); filter2Panel.setBounds (gridX (1, colW, layout::panelSpacing), layout::margin, colW, panelH); routingPanel.setBounds (gridX (2, colW, layout::panelSpacing), layout::margin, colW, panelH); for (auto* panel : { &filter1Panel, &filter2Panel, &routingPanel }) filterView.addAndMakeVisible (panel); const juce::StringArray filterTypes = { "Ladder LP", "Ladder HP", "Ladder BP", "Diode", "Comb", "Formant", "Screamer" }; const juce::StringArray slopes = { "6 dB", "12 dB", "24 dB" }; const int comboY = comboRowY(); const int displayY = bodyTop(); const int knobY = displayY + layout::displayHeight + layout::padding; // aligns with OSC row 1 // Filter 1 makeToggle (&filter1Panel, "On", ids::f1On, "Turns filter 1 on or off")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight); makeCombo (&filter1Panel, ids::f1Type, filterTypes, "Selects the filter 1 model")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 170, layout::comboHeight); makeCombo (&filter1Panel, ids::f1Slope, slopes, "Slope, 6, 12 or 24 dB per octave on the ladder and diode models")->setBounds (layout::padding + layout::toggleWidth + layout::gap + 170 + layout::gap, comboY, 100, layout::comboHeight); filter1Panel.addAndMakeVisible (filter1Display); filter1Display.setBounds (layout::padding, displayY, colW - 2 * layout::padding, layout::displayHeight); std::vector f1 = { makeKnob (&filter1Panel, "Cutoff", ids::f1Cutoff, formatHz, "Filter 1 cutoff, 20 Hz to 18 kHz"), makeKnob (&filter1Panel, "Resonance", ids::f1Res, formatPercent, "Filter 1 resonance, 0 % to 100 %"), makeKnob (&filter1Panel, "Drive", ids::f1Drive, formatPercent, "Filter 1 drive, 0 % to 100 %. Damps the comb model."), makeKnob (&filter1Panel, "Keytrack", ids::f1Key, formatPercent, "Filter 1 key tracking, 0 % to 100 %") }; placeKnobs (f1, layout::padding, knobY, layout::knobSize, layout::knobSize); // Filter 2 makeToggle (&filter2Panel, "On", ids::f2On, "Turns filter 2 on or off")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight); makeCombo (&filter2Panel, ids::f2Type, filterTypes, "Selects the filter 2 model")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 170, layout::comboHeight); makeCombo (&filter2Panel, ids::f2Slope, slopes, "Slope, 6, 12 or 24 dB per octave on the ladder and diode models")->setBounds (layout::padding + layout::toggleWidth + layout::gap + 170 + layout::gap, comboY, 100, layout::comboHeight); filter2Panel.addAndMakeVisible (filter2Display); filter2Display.setBounds (layout::padding, displayY, colW - 2 * layout::padding, layout::displayHeight); std::vector f2 = { makeKnob (&filter2Panel, "Cutoff", ids::f2Cutoff, formatHz, "Filter 2 cutoff, 20 Hz to 18 kHz"), makeKnob (&filter2Panel, "Resonance", ids::f2Res, formatPercent, "Filter 2 resonance, 0 % to 100 %"), makeKnob (&filter2Panel, "Drive", ids::f2Drive, formatPercent, "Filter 2 drive, 0 % to 100 %. Damps the comb model."), makeKnob (&filter2Panel, "Keytrack", ids::f2Key, formatPercent, "Filter 2 key tracking, 0 % to 100 %") }; placeKnobs (f2, layout::padding, knobY, layout::knobSize, layout::knobSize); // Routing makeCombo (&routingPanel, ids::fRoute, { "Serial", "Parallel", "Split" }, "Filter routing, serial, parallel or split")->setBounds (layout::padding, comboY, 160, layout::comboHeight); makeKnob (&routingPanel, "Mix", ids::fMix, formatPercent, "Blends filter 2 over filter 1 in parallel routing, 0 % to 100 %")->setBounds (layout::padding, displayY, layout::knobSize, layout::knobSize); makeKnob (&routingPanel, "Output", ids::fOut, fmtFilterOut, "Filter output gain, 0 % to 150 %")->setBounds (layout::padding + layout::knobSize + layout::gap, displayY, layout::knobSize, layout::knobSize); } void PluginEditor::buildModTab() { modView.setBounds (0, layout::tabBarHeight, kBaseW, kBaseH - layout::tabBarHeight); root.addChildComponent (modView); const juce::StringArray lfoShapes = { "Sine", "Triangle", "Saw", "Square", "S&H", "Step", "Freehand" }; constexpr int colW = layout::fourColW; constexpr int colGap = layout::fourColGap; constexpr int envH = layout::envPanelH; constexpr int lfoH = layout::lfoPanelH; // Envelopes (4 across). for (int i = 0; i < kNumEnvelopes; ++i) { envPanels[(size_t) i].setBounds (gridX (i, colW, colGap), layout::margin, colW, envH); modView.addAndMakeVisible (envPanels[(size_t) i]); envPanels[(size_t) i].addAndMakeVisible (envDisplays[(size_t) i]); envDisplays[(size_t) i].setBounds (layout::padding, layout::titleHeight, colW - 2 * layout::padding, layout::envDisplayHeight); const auto& idsA = paramIds::envAttack; const auto& idsD = paramIds::envDecay; const auto& idsS = paramIds::envSustain; const auto& idsR = paramIds::envRelease; const auto& idsC = paramIds::envCurve; std::vector knobs = { makeKnob (&envPanels[(size_t) i], "Attack", idsA[i], formatSeconds, "Attack time, 0.5 ms to 12 s"), makeKnob (&envPanels[(size_t) i], "Decay", idsD[i], formatSeconds, "Decay time, 0.5 ms to 12 s"), makeKnob (&envPanels[(size_t) i], "Sustain", idsS[i], formatPercent, "Sustain level, 0 % to 100 %"), makeKnob (&envPanels[(size_t) i], "Release", idsR[i], formatSeconds, "Release time, 0.5 ms to 12 s"), makeKnob (&envPanels[(size_t) i], "Curve", idsC[i], formatPercent, "Curve shape. Low is a fast attack and slow decay, high is the reverse.") }; placeKnobs (knobs, layout::padding, layout::titleHeight + layout::envDisplayHeight + layout::padding, layout::knobSmall, layout::smallKnobHeight, layout::smallGap); } // LFOs (4 across). const int lfoTop = layout::margin + envH + layout::panelSpacing; for (int i = 0; i < kNumLfos; ++i) { lfoPanels[(size_t) i].setBounds (gridX (i, colW, colGap), lfoTop, colW, lfoH); modView.addAndMakeVisible (lfoPanels[(size_t) i]); const auto& rate = paramIds::lfoRate; const auto& sync = paramIds::lfoSync; const auto& beat = paramIds::lfoBeat; const auto& shape = paramIds::lfoShape; const auto& phase = paramIds::lfoPhase; const auto& fade = paramIds::lfoFade; const auto& delay = paramIds::lfoDelay; makeCombo (&lfoPanels[(size_t) i], shape[i], lfoShapes, "Selects the LFO waveform")->setBounds (layout::padding, comboRowY(), 110, layout::comboHeight); makeToggle (&lfoPanels[(size_t) i], "Sync", sync[i], "Locks the LFO to host tempo and uses the Beat knob")->setBounds (layout::padding + 110 + layout::gap, layout::titleHeight, layout::toggleWidth, layout::toggleHeight); lfoPanels[(size_t) i].addAndMakeVisible (lfoDisplays[(size_t) i]); lfoDisplays[(size_t) i].setBounds (layout::padding, bodyTop(), colW - 2 * layout::padding, layout::lfoDisplayHeight); std::vector knobs = { makeKnob (&lfoPanels[(size_t) i], "Rate", rate[i], fmtLfoRate, "LFO rate, 0.02 Hz to 30 Hz"), makeKnob (&lfoPanels[(size_t) i], "Beat", beat[i], fmtLfoBeat, "Cycle length, 1/32 beat to 16 beats"), makeKnob (&lfoPanels[(size_t) i], "Phase", phase[i], fmtDegrees, "Start phase, 0 to 360 degrees"), makeKnob (&lfoPanels[(size_t) i], "Fade", fade[i], fmtLfoFade, "Fade in time, 0 s to 8 s"), makeKnob (&lfoPanels[(size_t) i], "Delay", delay[i], fmtLfoDelay, "Start delay, 0 s to 4 s") }; placeKnobs (knobs, layout::padding, bodyTop() + layout::lfoDisplayHeight + layout::padding, layout::knobSmall, layout::smallKnobHeight, layout::smallGap); lfoDisplays[(size_t) i].setOnShapeEdited ([this, i] (const std::vector& data, int steps) { processor.engine.setLfoShapeData (i, data, steps); }); } // Modulation matrix. const int matrixTop = lfoTop + lfoH + layout::panelSpacing; const int matrixH = kBaseH - layout::tabBarHeight - matrixTop - layout::margin; matrixPanel.setBounds (layout::margin, matrixTop, kBaseW - 2 * layout::margin, matrixH); modView.addAndMakeVisible (matrixPanel); std::vector targetEnums; const juce::StringArray targetItems = modTargetItems (targetEnums); const int mCenter = layout::headerCentreY; // centre line of the depth row modSourceCombo.addItemList (modSourceItems(), 1); modSourceCombo.setSelectedItemIndex (0, juce::dontSendNotification); modSourceCombo.setBounds (layout::padding, mCenter - layout::comboHeight / 2, 160, layout::comboHeight); modSourceCombo.setTooltip ("Selects the modulation source"); matrixPanel.addAndMakeVisible (modSourceCombo); modTargetCombo.addItemList (targetItems, 1); modTargetCombo.setSelectedItemIndex (0, juce::dontSendNotification); modTargetCombo.setBounds (layout::padding + 160 + layout::gap, mCenter - layout::comboHeight / 2, 200, layout::comboHeight); modTargetCombo.setTooltip ("Selects the modulation destination"); matrixPanel.addAndMakeVisible (modTargetCombo); const int depthX = layout::padding + 160 + layout::gap + 200 + layout::gap; modDepthKnob.setRange (-1.0, 1.0); modDepthKnob.setValue (0.5, juce::dontSendNotification); modDepthKnob.setDefaultValue (0.0); // a double click recentres the depth modDepthKnob.setFormatter (fmtModDepth); modDepthKnob.setBounds (depthX, layout::titleHeight, layout::knobSize, layout::depthKnobHeight); modDepthKnob.setTooltip ("Modulation amount for the next Add, -100 % to +100 % of the target range"); matrixPanel.addAndMakeVisible (modDepthKnob); modBipolarToggle.setLabel ("Bipolar"); modBipolarToggle.setBounds (depthX + layout::knobSize + layout::gap, mCenter - layout::toggleHeight / 2, layout::toggleWide, layout::toggleHeight); modBipolarToggle.setTooltip ("Recentres a one-sided source so the modulation swings both ways, for the next Add"); matrixPanel.addAndMakeVisible (modBipolarToggle); modBipolarToggle.setToggleState (true); const int addX = depthX + layout::knobSize + layout::gap + layout::toggleWide + layout::gap; modAddButton.setButtonText ("Add"); modAddButton.setBounds (addX, mCenter - layout::buttonHeight / 2, 70, layout::buttonHeight); modAddButton.setTooltip ("Adds the selected source and destination to the matrix"); modAddButton.onClick = [this, targetEnums] { const ModSource src = (ModSource) modSourceCombo.getSelectedItemIndex(); const int tid = modTargetCombo.getSelectedItemIndex(); if (tid >= 0 && tid < (int) targetEnums.size()) { bool added = false; { const juce::ScopedLock lock (processor.engine.getControlLock()); added = processor.engine.getMatrix().addConnection (src, targetEnums[(size_t) tid], (float) modDepthKnob.getValue(), modBipolarToggle.getToggleState()); } if (added) updateModList(); else modAddButton.setEnabled (false); // at cap: keep list text and selection unchanged } }; matrixPanel.addAndMakeVisible (modAddButton); modRemoveButton.setButtonText ("Remove Last"); modRemoveButton.setBounds (addX + 70 + layout::gap, mCenter - layout::buttonHeight / 2, 110, layout::buttonHeight); modRemoveButton.setTooltip ("Removes the newest matrix row, older rows stay"); modRemoveButton.onClick = [this] { { const juce::ScopedLock lock (processor.engine.getControlLock()); auto& matrix = processor.engine.getMatrix(); if (matrix.size() > 0) matrix.removeConnection (matrix.size() - 1); } updateModList(); }; matrixPanel.addAndMakeVisible (modRemoveButton); modClearButton.setButtonText ("Clear"); modClearButton.setBounds (addX + 70 + layout::gap + 110 + layout::gap, mCenter - layout::buttonHeight / 2, 70, layout::buttonHeight); modClearButton.setTooltip ("Removes every modulation connection"); modClearButton.onClick = [this] { { const juce::ScopedLock lock (processor.engine.getControlLock()); processor.engine.getMatrix().clear(); } updateModList(); }; matrixPanel.addAndMakeVisible (modClearButton); const int listTop = layout::titleHeight + layout::depthKnobHeight + layout::padding; modList.setBounds (layout::padding, listTop, kBaseW - 2 * layout::margin - 2 * layout::padding, matrixH - listTop - layout::padding); modList.setReadOnly (true); modList.setTooltip ("Every modulation connection, newest last. Depth is a fraction of the target range."); modList.setMultiLine (true, false); modList.setColour (juce::TextEditor::backgroundColourId, juce::Colours::transparentBlack); modList.setColour (juce::TextEditor::textColourId, theme::text); modList.setColour (juce::TextEditor::outlineColourId, juce::Colours::transparentBlack); matrixPanel.addAndMakeVisible (modList); } void PluginEditor::buildFxTab() { fxView.setBounds (0, layout::tabBarHeight, kBaseW, kBaseH - layout::tabBarHeight); root.addChildComponent (fxView); // Built from the one label function so the item order always follows FxType, // whose values are used directly as the combo selection / parameter value. juce::StringArray fxTypes; for (int t = 0; t < (int) FxType::Count; ++t) fxTypes.add (FXProcessor::fxTypeName (t)); constexpr int colW = layout::twoColW; constexpr int slotH = layout::smallPanelH; for (int i = 0; i < kNumFxSlots; ++i) { const int col = i % 2; const int row = i / 2; const int x = gridX (col, colW, layout::panelSpacing); const int y = layout::margin + row * (slotH + layout::panelSpacing); fxPanels[(size_t) i].setBounds (x, y, colW, slotH); fxView.addAndMakeVisible (fxPanels[(size_t) i]); fxTypeCombos[(size_t) i] = makeCombo (&fxPanels[(size_t) i], kFxType[i], fxTypes, "FX slot " + juce::String (i + 1) + " effect type"); fxTypeCombos[(size_t) i]->setBounds (layout::padding, comboRowY(), 160, layout::comboHeight); const int upX = layout::padding + 160 + layout::gap; auto up = std::make_unique ("\xe2\x86\x91"); fxUp[(size_t) i] = up.get(); ownedControls.push_back (std::move (up)); fxUp[(size_t) i]->setBounds (upX, comboRowY(), layout::arrowWidth, layout::comboHeight); fxUp[(size_t) i]->setTooltip ("Move effect up"); fxPanels[(size_t) i].addAndMakeVisible (fxUp[(size_t) i]); auto down = std::make_unique ("\xe2\x86\x93"); fxDown[(size_t) i] = down.get(); ownedControls.push_back (std::move (down)); fxDown[(size_t) i]->setBounds (upX + layout::arrowWidth + layout::gap, comboRowY(), layout::arrowWidth, layout::comboHeight); fxDown[(size_t) i]->setTooltip ("Move effect down"); fxPanels[(size_t) i].addAndMakeVisible (fxDown[(size_t) i]); fxUp[(size_t) i]->onClick = [this, i] { if (i > 0) swapFxSlots (i, i - 1); }; fxDown[(size_t) i]->onClick = [this, i] { if (i < kNumFxSlots - 1) swapFxSlots (i, i + 1); }; // The first slot cannot move up and the last cannot move down, so grey // the arrow out instead of leaving a click that does nothing. fxUp[(size_t) i]->setEnabled (i > 0); fxDown[(size_t) i]->setEnabled (i < kNumFxSlots - 1); std::vector knobs = { makeKnob (&fxPanels[(size_t) i], "Mix", kFxMix[i], formatPercent, "Dry and wet mix, 0 % to 100 %"), makeKnob (&fxPanels[(size_t) i], "P1", kFxP1[i], formatPercent, "Effect parameter 1, set by the effect type"), makeKnob (&fxPanels[(size_t) i], "P2", kFxP2[i], formatPercent, "Effect parameter 2, set by the effect type"), makeKnob (&fxPanels[(size_t) i], "P3", kFxP3[i], formatPercent, "Effect parameter 3, set by the effect type"), makeKnob (&fxPanels[(size_t) i], "P4", kFxP4[i], formatPercent, "Effect parameter 4, set by the effect type") }; placeKnobs (knobs, layout::padding, bodyTop(), layout::knobSize, layout::knobSize); fxKnobs[(size_t) i] = knobs; } } void PluginEditor::buildMacroTab() { macroView.setBounds (0, layout::tabBarHeight, kBaseW, kBaseH - layout::tabBarHeight); root.addChildComponent (macroView); constexpr int colW = layout::fourColW; constexpr int colGap = layout::fourColGap; constexpr int macroH = layout::macroPanelH; for (int i = 0; i < kNumMacros; ++i) { macroPanels[(size_t) i].setBounds (gridX (i, colW, colGap), layout::margin, colW, macroH); macroView.addAndMakeVisible (macroPanels[(size_t) i]); const auto& macroIds = paramIds::macros; macroKnobs[(size_t) i] = makeKnob (¯oPanels[(size_t) i], MacroControls::macroName (i), macroIds[i], formatPercent, MacroControls::macroName (i) + " macro (0-100%)"); macroKnobs[(size_t) i]->setBounds ((colW - layout::macroKnobSize) / 2, layout::titleHeight + layout::padding, layout::macroKnobSize, layout::macroKnobSize); } // Macro assignment editor. const int assignTop = layout::margin + macroH + layout::panelSpacing; const int assignH = kBaseH - layout::tabBarHeight - assignTop - layout::margin; macroAssignPanel.setBounds (layout::margin, assignTop, kBaseW - 2 * layout::margin, assignH); macroView.addAndMakeVisible (macroAssignPanel); std::vector targetEnums; const juce::StringArray targetItems = modTargetItems (targetEnums); const int mCenter = layout::headerCentreY; // centre line of the depth row juce::StringArray macroItems; for (int i = 0; i < kNumMacros; ++i) macroItems.add (MacroControls::macroName (i)); macroAssignIndex.addItemList (macroItems, 1); macroAssignIndex.setSelectedItemIndex (0, juce::dontSendNotification); macroAssignIndex.setBounds (layout::padding, mCenter - layout::comboHeight / 2, 140, layout::comboHeight); macroAssignIndex.setTooltip ("Selects the macro to edit"); macroAssignIndex.onChange = [this] { updateMacroList(); }; macroAssignPanel.addAndMakeVisible (macroAssignIndex); macroAssignTarget.addItemList (targetItems, 1); macroAssignTarget.setSelectedItemIndex (0, juce::dontSendNotification); macroAssignTarget.setBounds (layout::padding + 140 + layout::gap, mCenter - layout::comboHeight / 2, 220, layout::comboHeight); macroAssignTarget.setTooltip ("Selects the destination parameter"); macroAssignPanel.addAndMakeVisible (macroAssignTarget); const int depthX = layout::padding + 140 + layout::gap + 220 + layout::gap; // Half depth, so an Assign made without touching this knob still moves the // destination. A depth of zero would create a silent assignment. macroDepthKnob.setValue (0.5, juce::dontSendNotification); macroDepthKnob.setDefaultValue (0.5); macroDepthKnob.setFormatter (formatPercent); macroDepthKnob.setBounds (depthX, layout::titleHeight, layout::knobSize, layout::depthKnobHeight); macroDepthKnob.setTooltip ("Macro amount for the next Assign, 0 % to 100 % of the target range"); macroAssignPanel.addAndMakeVisible (macroDepthKnob); const int assignBtnX = depthX + layout::knobSize + layout::gap; macroAssignButton.setButtonText ("Assign"); macroAssignButton.setBounds (assignBtnX, mCenter - layout::buttonHeight / 2, 80, layout::buttonHeight); macroAssignButton.setTooltip ("Adds this destination to the selected macro"); macroAssignButton.onClick = [this, targetEnums] { const int macro = macroAssignIndex.getSelectedItemIndex(); const int tid = macroAssignTarget.getSelectedItemIndex(); if (macro >= 0 && macro < kNumMacros && tid >= 0 && tid < (int) targetEnums.size()) { bool added = false; { const juce::ScopedLock lock (processor.engine.getControlLock()); added = processor.engine.getMacros().addAssignment (macro, targetEnums[(size_t) tid], (float) macroDepthKnob.getValue()); } if (added) updateMacroList(); else macroAssignButton.setEnabled (false); // at cap: keep list text and selection unchanged } }; macroAssignPanel.addAndMakeVisible (macroAssignButton); macroClearButton.setButtonText ("Clear All"); macroClearButton.setBounds (assignBtnX + 80 + layout::gap, mCenter - layout::buttonHeight / 2, 80, layout::buttonHeight); macroClearButton.setTooltip ("Removes every macro assignment from all four macros"); macroClearButton.onClick = [this] { { const juce::ScopedLock lock (processor.engine.getControlLock()); processor.engine.getMacros().clear(); } updateMacroList(); }; macroAssignPanel.addAndMakeVisible (macroClearButton); const int listTop = layout::titleHeight + layout::depthKnobHeight + layout::padding; macroList.setBounds (layout::padding, listTop, kBaseW - 2 * layout::margin - 2 * layout::padding, assignH - listTop - layout::padding); macroList.setReadOnly (true); macroList.setTooltip ("Every macro assignment, grouped by macro. Depth is a fraction of the target range."); macroList.setMultiLine (true, false); macroList.setColour (juce::TextEditor::backgroundColourId, juce::Colours::transparentBlack); macroList.setColour (juce::TextEditor::textColourId, theme::text); macroList.setColour (juce::TextEditor::outlineColourId, juce::Colours::transparentBlack); macroAssignPanel.addAndMakeVisible (macroList); } void PluginEditor::swapFxSlots (int a, int b) { if (a < 0 || b < 0 || a >= kNumFxSlots || b >= kNumFxSlots || a == b) return; const juce::ScopedLock lock (processor.engine.getControlLock()); auto swapParam = [this] (const char* pa, const char* pb) { auto* p1 = processor.parameters.getParameter (pa); auto* p2 = processor.parameters.getParameter (pb); if (p1 == nullptr || p2 == nullptr) return; const float v1 = p1->getValue(); const float v2 = p2->getValue(); p1->setValueNotifyingHost (v2); p2->setValueNotifyingHost (v1); }; swapParam (kFxType[a], kFxType[b]); swapParam (kFxMix[a], kFxMix[b]); swapParam (kFxP1[a], kFxP1[b]); swapParam (kFxP2[a], kFxP2[b]); swapParam (kFxP3[a], kFxP3[b]); swapParam (kFxP4[a], kFxP4[b]); } // --------------------------------------------------------------------------- void PluginEditor::setTab (int index) { currentTab = juce::jlimit (0, 4, index); oscView.setVisible (currentTab == 0); filterView.setVisible(currentTab == 1); modView.setVisible (currentTab == 2); fxView.setVisible (currentTab == 3); macroView.setVisible (currentTab == 4); tabOsc.setToggleState (currentTab == 0, juce::dontSendNotification); tabFilter.setToggleState(currentTab == 1, juce::dontSendNotification); tabMod.setToggleState (currentTab == 2, juce::dontSendNotification); tabFx.setToggleState (currentTab == 3, juce::dontSendNotification); tabMacro.setToggleState (currentTab == 4, juce::dontSendNotification); timerCallback(); } void PluginEditor::cycleTab (int delta) { setTab ((currentTab + delta + 5) % 5); } bool PluginEditor::keyPressed (const juce::KeyPress& key) { // Only handle plain Tab / Shift+Tab (never Ctrl/Cmd/Alt+Tab). if (key.getKeyCode() == juce::KeyPress::tabKey && ! key.getModifiers().isCtrlDown() && ! key.getModifiers().isAltDown() && ! key.getModifiers().isCommandDown()) { // Leave Tab alone while a text editor or combo box is being edited so we // don't break typing / focus traversal. for (auto* c = juce::Component::getCurrentlyFocusedComponent(); c != nullptr; c = c->getParentComponent()) { if (dynamic_cast (c) != nullptr || dynamic_cast (c) != nullptr) return false; } cycleTab (key.getModifiers().isShiftDown() ? -1 : +1); return true; } return false; } void PluginEditor::applyUiScale() { const float scale = processor.getUiScale(); currentScaleIndex = processor.getUiScaleIndex(); root.setTransform (juce::AffineTransform::scale (scale)); setSize ((int) (kBaseW * scale), (int) (kBaseH * scale)); } // --------------------------------------------------------------------------- void PluginEditor::updateVisuals() { const auto& apvts = processor.parameters; auto gv = [&] (const char* id) { if (auto* value = apvts.getRawParameterValue (id)) return value->load(); return 0.0f; }; masterDisplay.setValue (formatPercent (gv (ids::master))); // Waveforms. if (currentTab == 0) { const int waveA = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (gv (ids::oscAWave) * (kNumWavetables - 1))); const int waveB = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (gv (ids::oscBWave) * (kNumWavetables - 1))); oscAWave.setWaveIndex (waveA); oscAWave.setFramePosition (gv (ids::oscAWtPos)); oscAWave.setEnabled (gv (ids::oscAOn) > 0.5f); oscBWave.setWaveIndex (waveB); oscBWave.setFramePosition (gv (ids::oscBWtPos)); oscBWave.setEnabled (gv (ids::oscBOn) > 0.5f); } // Filters. if (currentTab == 1) { filter1Display.setParams ((int) std::llround (gv (ids::f1Type) * 6.0f), gv (ids::f1Cutoff), gv (ids::f1Res), gv (ids::f1Drive), (int) std::llround (gv (ids::f1Slope) * 2.0f)); filter1Display.setEnabled (gv (ids::f1On) > 0.5f); filter2Display.setParams ((int) std::llround (gv (ids::f2Type) * 6.0f), gv (ids::f2Cutoff), gv (ids::f2Res), gv (ids::f2Drive), (int) std::llround (gv (ids::f2Slope) * 2.0f)); filter2Display.setEnabled (gv (ids::f2On) > 0.5f); } // Envelopes. if (currentTab == 2) { const auto& a = paramIds::envAttack; const auto& d = paramIds::envDecay; const auto& s = paramIds::envSustain; const auto& r = paramIds::envRelease; const auto& c = paramIds::envCurve; for (int i = 0; i < kNumEnvelopes; ++i) envDisplays[(size_t) i].setParams (gv (a[i]), gv (d[i]), gv (s[i]), gv (r[i]), gv (c[i])); } // LFOs. if (currentTab == 2) { const auto& lshape = paramIds::lfoShape; const juce::ScopedLock lock (processor.engine.getControlLock()); for (int i = 0; i < kNumLfos; ++i) { const auto& lfo = processor.engine.getLfos()[(size_t) i]; lfoDisplays[(size_t) i].setShape ((int) std::llround (gv (lshape[i]) * 6.0f)); lfoDisplays[(size_t) i].setShapeData (lfo.getShapeData(), lfo.getShapeSteps()); } } if (currentTab == 3) { for (int i = 0; i < kNumFxSlots; ++i) { const int type = juce::jlimit (0, (int) FxType::Count - 1, (int) std::llround (gv (kFxType[i]) * ((int) FxType::Count - 1))); for (int p = 0; p < 4; ++p) { auto* knob = fxKnobs[(size_t) i][(size_t) p + 1]; const auto& spec = kFxParamDisplay[type][p]; // Label, tooltip and readout unit all come from the one table, so // a renamed knob can never keep the previous effect's tooltip or // show a percentage for a value the DSP reads in Hz or dB. const juce::String name = spec.name; if (knob->getName() != name) { knob->setName (name); knob->setTooltip (spec.tooltip); knob->setFormatter (spec.formatter); knob->setEnabled (name != "Unused"); knob->repaint(); } } } } // RAVE state. raveButton.setToggleState (processor.isRaveEnabled()); const int program = processor.getCurrentProgram(); if (presetCombo.getSelectedItemIndex() != program) presetCombo.setSelectedItemIndex (program, juce::dontSendNotification); // Scale change. if (processor.getUiScaleIndex() != currentScaleIndex) applyUiScale(); if (scaleCombo.getSelectedItemIndex() != currentScaleIndex) scaleCombo.setSelectedItemIndex (currentScaleIndex, juce::dontSendNotification); } void PluginEditor::updateModList() { juce::String text; int count = 0; { const juce::ScopedLock lock (processor.engine.getControlLock()); const auto& cons = processor.engine.getMatrix().connections; count = (int) cons.size(); for (const auto& c : cons) text += modSourceName (c.source) + " -> " + modTargetName (c.target) + " [" + juce::String (c.depth, 2) + (c.bipolar ? ", bipolar]" : "]") + "\n"; } if (modList.getText() != text) modList.setText (text, false); modAddButton.setEnabled (count < ModulationMatrix::kMaxConnections); } void PluginEditor::updateMacroList() { juce::String text; int assigned = 0; const int macro = macroAssignIndex.getSelectedItemIndex(); { const juce::ScopedLock lock (processor.engine.getControlLock()); for (int m = 0; m < kNumMacros; ++m) { text += MacroControls::macroName (m) + ":\n"; const auto& assigns = processor.engine.getMacros().assignments[(size_t) m]; if (m == macro) assigned = (int) assigns.size(); if (assigns.empty()) text += " (none)\n"; for (const auto& a : assigns) text += " " + modTargetName (a.target) + " [" + juce::String (a.depth, 2) + "]\n"; } } if (macroList.getText() != text) macroList.setText (text, false); macroAssignButton.setEnabled (macro >= 0 && macro < kNumMacros && assigned < MacroControls::kMaxAssignments); } void PluginEditor::timerCallback() { updateVisuals(); if (currentTab == 2) updateModList(); else if (currentTab == 4) updateMacroList(); } } // namespace serum