chore: land janitor round 1 and graft integration
Round 1 findings and fixes, all previously uncommitted: - FXProcessor per-slot DSP history repair (HEAD does not compile without it) - SynthVoice::getModulationValue, editor control-lock guard - ExactCompare.h naming the deliberate exact float comparisons - KnobLookAndFeel replacing the never-installed SerumLookAndFeel - 55 first-party compiler warnings to 0 - AGENT.md renamed to AGENTS.md across Source/ - graft repository context graph and its hooks - JANITOR_TASKS.md and UI_UX_AUDIT.md records
This commit is contained in:
1 parent
5c942cad0c
commit
e4cea300db
63 files changed
+2360
-805
No files matched your search
+315
-136
@@ -19,7 +19,6 @@ namespace
|
||||
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
|
||||
constexpr int labelHeight = 16; // label strip height
|
||||
|
||||
// Derived from the visual style (panel title bar and control sizes) so the
|
||||
// content grid stays aligned across every tab.
|
||||
@@ -28,7 +27,12 @@ namespace
|
||||
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
|
||||
@@ -36,6 +40,24 @@ namespace
|
||||
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).
|
||||
@@ -85,8 +107,70 @@ namespace
|
||||
}
|
||||
|
||||
juce::String fmtUnison (float v) { return juce::String (1 + (int) std::llround (v * 15.0f)); }
|
||||
juce::String fmtSlope (float v) { const int s = (int) std::llround (v * 2.0f); return juce::String (s == 0 ? 6 : (s == 1 ? 12 : 24)) + " dB"; }
|
||||
juce::String fmtDepth (float v) { return juce::String (v, 2); }
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 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<Knob*>& knobs, int x, int y, int w, int h, int gap = layout::gap)
|
||||
{
|
||||
@@ -101,17 +185,61 @@ namespace
|
||||
constexpr auto& kFxP3 = paramIds::fxP3;
|
||||
constexpr auto& kFxP4 = paramIds::fxP4;
|
||||
|
||||
constexpr const char* kFxParamNames[(int) FxType::Count][4] = {
|
||||
{ "P1", "P2", "P3", "P4" },
|
||||
{ "Intensity", "Low Amount", "High Amount", "Output" },
|
||||
{ "Rate", "Depth", "Width", "Unused" },
|
||||
{ "Rate", "Depth", "Feedback", "Unused" },
|
||||
{ "Rate", "Depth", "Feedback", "Stages" },
|
||||
{ "Drive", "Shape", "Tone", "Output" },
|
||||
{ "Low Gain", "Mid Gain", "High Gain", "Mid Freq" },
|
||||
{ "Threshold", "Ratio", "Attack", "Release" },
|
||||
{ "Time", "Feedback", "Damping", "Ping-Pong" },
|
||||
{ "Size", "Damping", "Width", "Predelay" }
|
||||
// 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<juce::String (float)> 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 } }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -173,9 +301,15 @@ Knob* PluginEditor::makeKnob (juce::Component* parent, const juce::String& name,
|
||||
auto* k = control.get();
|
||||
ownedControls.push_back (std::move (control));
|
||||
parent->addAndMakeVisible (k);
|
||||
if (paramId.isNotEmpty() && processor.parameters.getParameter (paramId) != nullptr)
|
||||
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<juce::AudioProcessorValueTreeState::SliderAttachment> (
|
||||
processor.parameters, paramId, *k));
|
||||
}
|
||||
k->setTooltip (tooltip.isNotEmpty() ? tooltip : name);
|
||||
return k;
|
||||
}
|
||||
@@ -222,7 +356,7 @@ void PluginEditor::buildTopBar()
|
||||
presetCombo.onChange = [this] { processor.setCurrentProgram (presetCombo.getSelectedItemIndex()); };
|
||||
root.addAndMakeVisible (presetCombo);
|
||||
presetCombo.setBounds (200, 16, 220, 24);
|
||||
presetCombo.setTooltip ("Select a factory preset");
|
||||
presetCombo.setTooltip ("Loads a factory preset and replaces the current patch");
|
||||
|
||||
// UI scale.
|
||||
scaleCombo.addItemList ({ "75%", "100%", "125%", "150%", "200%" }, 1);
|
||||
@@ -230,25 +364,25 @@ void PluginEditor::buildTopBar()
|
||||
scaleCombo.onChange = [this] { processor.setUiScaleIndex (scaleCombo.getSelectedItemIndex()); };
|
||||
root.addAndMakeVisible (scaleCombo);
|
||||
scaleCombo.setBounds (980, 16, 70, 24);
|
||||
scaleCombo.setTooltip ("Interface scale (75% - 200%)");
|
||||
scaleCombo.setTooltip ("Interface scale, 75 % to 200 %");
|
||||
|
||||
// RAVE.
|
||||
raveButton.setLabel ("RAVE");
|
||||
raveButton.setLabel ("Rave");
|
||||
raveButton.setOnColour (theme::raveGlow);
|
||||
raveButton.setBounds (1062, 2, 50, 56);
|
||||
root.addAndMakeVisible (raveButton);
|
||||
raveButton.setTooltip ("RAVE one-click boost (unison, width, drive, OTT, reverb)");
|
||||
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 matrix (Tab cycles sections)",
|
||||
"Effects rack (Tab cycles sections)",
|
||||
"Macros (Tab cycles sections)" };
|
||||
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]);
|
||||
@@ -262,7 +396,7 @@ void PluginEditor::buildTopBar()
|
||||
|
||||
// Master.
|
||||
masterKnob.setBounds (906, 0, 64, 60);
|
||||
masterKnob.setTooltip ("Master output volume (0-100%)");
|
||||
masterKnob.setTooltip ("Master output level, 0 % to 100 %");
|
||||
root.addAndMakeVisible (masterKnob);
|
||||
sliderAttachments.push_back (std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (
|
||||
processor.parameters, ids::master, masterKnob));
|
||||
@@ -274,12 +408,12 @@ void PluginEditor::buildTopBar()
|
||||
|
||||
void PluginEditor::buildOscTab()
|
||||
{
|
||||
oscView.setBounds (0, 60, kBaseW, kBaseH - 60);
|
||||
oscView.setBounds (0, layout::tabBarHeight, kBaseW, kBaseH - layout::tabBarHeight);
|
||||
root.addChildComponent (oscView);
|
||||
|
||||
constexpr int colW = 545; // two equal columns filling kBaseW
|
||||
constexpr int oscH = 308;
|
||||
constexpr int subH = 150;
|
||||
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);
|
||||
@@ -294,9 +428,9 @@ void PluginEditor::buildOscTab()
|
||||
const int row2Y = row1Y + layout::rowHeight;
|
||||
|
||||
// --- Oscillator A ---
|
||||
makeToggle (&oscAPanel, "On", ids::oscAOn, "Enable oscillator A")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight);
|
||||
makeCombo (&oscAPanel, ids::oscAWave, wavetableItems(), "Oscillator A wavetable")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 200, layout::comboHeight);
|
||||
makeCombo (&oscAPanel, ids::oscAWarp, { "None", "Bend+", "Bend-", "Sync", "PWM", "Asym", "Mirror", "Fold" }, "Oscillator A warp mode")
|
||||
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());
|
||||
@@ -315,17 +449,19 @@ void PluginEditor::buildOscTab()
|
||||
|
||||
std::vector<Knob*> rowA2 = {
|
||||
makeKnob (&oscAPanel, "Unison", ids::oscAUnison, fmtUnison, "Unison voices (1-16)"),
|
||||
makeKnob (&oscAPanel, "Detune", ids::oscADetune, formatPercent, "Unison detune (0-100%)"),
|
||||
makeKnob (&oscAPanel, "Spread", ids::oscASpread, formatPercent, "Unison spread (0-100%)"),
|
||||
makeKnob (&oscAPanel, "Phase", ids::oscAPhase, formatPercent, "Phase (0-100%)"),
|
||||
makeKnob (&oscAPanel, "Random Phase", ids::oscARandPh, formatPercent, "Random phase (0-100%)")
|
||||
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, "Enable oscillator B")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight);
|
||||
makeCombo (&oscBPanel, ids::oscBWave, wavetableItems(), "Oscillator B wavetable")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 200, layout::comboHeight);
|
||||
makeCombo (&oscBPanel, ids::oscBWarp, { "None", "Bend+", "Bend-", "Sync", "PWM", "Asym", "Mirror", "Fold" }, "Oscillator B warp mode")
|
||||
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());
|
||||
@@ -344,32 +480,33 @@ void PluginEditor::buildOscTab()
|
||||
|
||||
std::vector<Knob*> rowB2 = {
|
||||
makeKnob (&oscBPanel, "Unison", ids::oscBUnison, fmtUnison, "Unison voices (1-16)"),
|
||||
makeKnob (&oscBPanel, "Detune", ids::oscBDetune, formatPercent, "Unison detune (0-100%)"),
|
||||
makeKnob (&oscBPanel, "Spread", ids::oscBSpread, formatPercent, "Unison spread (0-100%)"),
|
||||
makeKnob (&oscBPanel, "Phase", ids::oscBPhase, formatPercent, "Phase (0-100%)"),
|
||||
makeKnob (&oscBPanel, "Random Phase", ids::oscBRandPh, formatPercent, "Random phase (0-100%)")
|
||||
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, "Enable sub oscillator")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight);
|
||||
makeCombo (&subPanel, ids::subShape, { "Sine", "Triangle" }, "Sub oscillator waveform")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 130, layout::comboHeight);
|
||||
makeCombo (&subPanel, ids::subOct, { "-2 oct", "-1 oct", "0 oct" }, "Sub oscillator octave")->setBounds (layout::padding + layout::toggleWidth + layout::gap + 130 + layout::gap, comboY, 90, layout::comboHeight);
|
||||
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, "Enable noise")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight);
|
||||
makeCombo (&noisePanel, ids::noiseType, { "White", "Pink" }, "Noise type")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 130, layout::comboHeight);
|
||||
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, 60, kBaseW, kBaseH - 60);
|
||||
filterView.setBounds (0, layout::tabBarHeight, kBaseW, kBaseH - layout::tabBarHeight);
|
||||
root.addChildComponent (filterView);
|
||||
|
||||
constexpr int colW = 360;
|
||||
constexpr int panelH = 300; // retains the original panel height (content top-aligned)
|
||||
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);
|
||||
@@ -385,52 +522,52 @@ void PluginEditor::buildFilterTab()
|
||||
const int knobY = displayY + layout::displayHeight + layout::padding; // aligns with OSC row 1
|
||||
|
||||
// Filter 1
|
||||
makeToggle (&filter1Panel, "On", ids::f1On, "Enable filter 1")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight);
|
||||
makeCombo (&filter1Panel, ids::f1Type, filterTypes, "Filter 1 type")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 170, layout::comboHeight);
|
||||
makeCombo (&filter1Panel, ids::f1Slope, slopes, "Filter 1 slope")->setBounds (layout::padding + layout::toggleWidth + layout::gap + 170 + layout::gap, comboY, 100, layout::comboHeight);
|
||||
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<Knob*> f1 = {
|
||||
makeKnob (&filter1Panel, "Cutoff", ids::f1Cutoff, formatHz, "Filter 1 cutoff (20 Hz - 20 kHz)"),
|
||||
makeKnob (&filter1Panel, "Resonance", ids::f1Res, formatPercent, "Filter 1 resonance (0-100%)"),
|
||||
makeKnob (&filter1Panel, "Drive", ids::f1Drive, formatPercent, "Filter 1 drive (0-100%)"),
|
||||
makeKnob (&filter1Panel, "Keytrack", ids::f1Key, formatPercent, "Filter 1 keytrack (0-100%)")
|
||||
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, "Enable filter 2")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight);
|
||||
makeCombo (&filter2Panel, ids::f2Type, filterTypes, "Filter 2 type")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 170, layout::comboHeight);
|
||||
makeCombo (&filter2Panel, ids::f2Slope, slopes, "Filter 2 slope")->setBounds (layout::padding + layout::toggleWidth + layout::gap + 170 + layout::gap, comboY, 100, layout::comboHeight);
|
||||
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<Knob*> f2 = {
|
||||
makeKnob (&filter2Panel, "Cutoff", ids::f2Cutoff, formatHz, "Filter 2 cutoff (20 Hz - 20 kHz)"),
|
||||
makeKnob (&filter2Panel, "Resonance", ids::f2Res, formatPercent, "Filter 2 resonance (0-100%)"),
|
||||
makeKnob (&filter2Panel, "Drive", ids::f2Drive, formatPercent, "Filter 2 drive (0-100%)"),
|
||||
makeKnob (&filter2Panel, "Keytrack", ids::f2Key, formatPercent, "Filter 2 keytrack (0-100%)")
|
||||
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 / split)")->setBounds (layout::padding, comboY, 160, layout::comboHeight);
|
||||
makeKnob (&routingPanel, "Mix", ids::fMix, formatPercent, "Filter mix (0-100%)")->setBounds (layout::padding, displayY, layout::knobSize, layout::knobSize);
|
||||
makeKnob (&routingPanel, "Output", ids::fOut, formatPercent, "Filter output (0-100%)")->setBounds (layout::padding + layout::knobSize + layout::gap, displayY, layout::knobSize, layout::knobSize);
|
||||
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, 60, kBaseW, kBaseH - 60);
|
||||
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 = 269; // four equal columns: 4*269 + 3*8 = 1100
|
||||
constexpr int colGap = 8;
|
||||
constexpr int envH = 154;
|
||||
constexpr int lfoH = 206;
|
||||
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)
|
||||
@@ -449,11 +586,11 @@ void PluginEditor::buildModTab()
|
||||
const auto& idsC = paramIds::envCurve;
|
||||
|
||||
std::vector<Knob*> knobs = {
|
||||
makeKnob (&envPanels[(size_t) i], "Attack", idsA[i], formatSeconds, "Attack time (0.5 ms - 12 s)"),
|
||||
makeKnob (&envPanels[(size_t) i], "Decay", idsD[i], formatSeconds, "Decay time (0.5 ms - 12 s)"),
|
||||
makeKnob (&envPanels[(size_t) i], "Sustain", idsS[i], formatPercent, "Sustain level (0-100%)"),
|
||||
makeKnob (&envPanels[(size_t) i], "Release", idsR[i], formatSeconds, "Release time (0.5 ms - 12 s)"),
|
||||
makeKnob (&envPanels[(size_t) i], "Curve", idsC[i], formatPercent, "Envelope curve (0-100%)")
|
||||
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,
|
||||
@@ -475,18 +612,18 @@ void PluginEditor::buildModTab()
|
||||
const auto& fade = paramIds::lfoFade;
|
||||
const auto& delay = paramIds::lfoDelay;
|
||||
|
||||
makeCombo (&lfoPanels[(size_t) i], shape[i], lfoShapes, "LFO shape")->setBounds (layout::padding, comboRowY(), 110, layout::comboHeight);
|
||||
makeToggle (&lfoPanels[(size_t) i], "Sync", sync[i], "Tempo-sync LFO")->setBounds (layout::padding + 110 + layout::gap, layout::titleHeight, layout::toggleWidth, layout::toggleHeight);
|
||||
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<Knob*> knobs = {
|
||||
makeKnob (&lfoPanels[(size_t) i], "Rate", rate[i], formatPercent, "LFO rate (0.02 Hz - 30 Hz)"),
|
||||
makeKnob (&lfoPanels[(size_t) i], "Beat", beat[i], formatPercent, "Beat division (1/32 - 4 bars)"),
|
||||
makeKnob (&lfoPanels[(size_t) i], "Phase", phase[i], formatPercent, "Phase (0-100%)"),
|
||||
makeKnob (&lfoPanels[(size_t) i], "Fade", fade[i], formatPercent, "Fade in (0-100%)"),
|
||||
makeKnob (&lfoPanels[(size_t) i], "Delay", delay[i], formatPercent, "Start delay (0-100%)")
|
||||
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,
|
||||
@@ -500,65 +637,70 @@ void PluginEditor::buildModTab()
|
||||
|
||||
// Modulation matrix.
|
||||
const int matrixTop = lfoTop + lfoH + layout::panelSpacing;
|
||||
const int matrixH = kBaseH - 60 - matrixTop - layout::margin;
|
||||
const int matrixH = kBaseH - layout::tabBarHeight - matrixTop - layout::margin;
|
||||
matrixPanel.setBounds (layout::margin, matrixTop, kBaseW - 2 * layout::margin, matrixH);
|
||||
modView.addAndMakeVisible (matrixPanel);
|
||||
|
||||
std::vector<ModTarget> targetEnums;
|
||||
const juce::StringArray targetItems = modTargetItems (targetEnums);
|
||||
|
||||
const int mCenter = layout::titleHeight + 30; // vertical centre of the (60px) depth knob
|
||||
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 ("Modulation source");
|
||||
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 ("Modulation destination");
|
||||
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.setFormatter (fmtDepth);
|
||||
modDepthKnob.setBounds (depthX, layout::titleHeight, layout::knobSize, 60);
|
||||
modDepthKnob.setTooltip ("Modulation depth (-1 to +1)");
|
||||
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, 60, layout::toggleHeight);
|
||||
modBipolarToggle.setTooltip ("Bipolar modulation depth");
|
||||
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 + 60 + layout::gap;
|
||||
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 ("Add modulation connection");
|
||||
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());
|
||||
processor.engine.getMatrix().addConnection (src, targetEnums[(size_t) tid],
|
||||
(float) modDepthKnob.getValue(),
|
||||
modBipolarToggle.getToggleState());
|
||||
added = processor.engine.getMatrix().addConnection (src, targetEnums[(size_t) tid],
|
||||
(float) modDepthKnob.getValue(),
|
||||
modBipolarToggle.getToggleState());
|
||||
}
|
||||
updateModList();
|
||||
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 ("Remove last modulation connection");
|
||||
modRemoveButton.setTooltip ("Removes the newest matrix row, older rows stay");
|
||||
modRemoveButton.onClick = [this]
|
||||
{
|
||||
{
|
||||
@@ -573,7 +715,7 @@ void PluginEditor::buildModTab()
|
||||
|
||||
modClearButton.setButtonText ("Clear");
|
||||
modClearButton.setBounds (addX + 70 + layout::gap + 110 + layout::gap, mCenter - layout::buttonHeight / 2, 70, layout::buttonHeight);
|
||||
modClearButton.setTooltip ("Clear all modulation connections");
|
||||
modClearButton.setTooltip ("Removes every modulation connection");
|
||||
modClearButton.onClick = [this]
|
||||
{
|
||||
{
|
||||
@@ -584,11 +726,12 @@ void PluginEditor::buildModTab()
|
||||
};
|
||||
matrixPanel.addAndMakeVisible (modClearButton);
|
||||
|
||||
const int listTop = layout::titleHeight + 60 + layout::padding;
|
||||
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);
|
||||
@@ -598,14 +741,17 @@ void PluginEditor::buildModTab()
|
||||
|
||||
void PluginEditor::buildFxTab()
|
||||
{
|
||||
fxView.setBounds (0, 60, kBaseW, kBaseH - 60);
|
||||
fxView.setBounds (0, layout::tabBarHeight, kBaseW, kBaseH - layout::tabBarHeight);
|
||||
root.addChildComponent (fxView);
|
||||
|
||||
const juce::StringArray fxTypes = { "Off", "Hyper", "Chorus", "Flanger", "Phaser",
|
||||
"Distortion", "EQ", "Compressor", "Delay", "Reverb" };
|
||||
// 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 = 545;
|
||||
constexpr int slotH = 150;
|
||||
constexpr int colW = layout::twoColW;
|
||||
constexpr int slotH = layout::smallPanelH;
|
||||
|
||||
for (int i = 0; i < kNumFxSlots; ++i)
|
||||
{
|
||||
@@ -637,13 +783,17 @@ void PluginEditor::buildFxTab()
|
||||
|
||||
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<Knob*> knobs = {
|
||||
makeKnob (&fxPanels[(size_t) i], "Mix", kFxMix[i], formatPercent, "Effect mix (0-100%)"),
|
||||
makeKnob (&fxPanels[(size_t) i], "P1", kFxP1[i], formatPercent, "Effect parameter 1 (0-100%)"),
|
||||
makeKnob (&fxPanels[(size_t) i], "P2", kFxP2[i], formatPercent, "Effect parameter 2 (0-100%)"),
|
||||
makeKnob (&fxPanels[(size_t) i], "P3", kFxP3[i], formatPercent, "Effect parameter 3 (0-100%)"),
|
||||
makeKnob (&fxPanels[(size_t) i], "P4", kFxP4[i], formatPercent, "Effect parameter 4 (0-100%)")
|
||||
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;
|
||||
@@ -652,12 +802,12 @@ void PluginEditor::buildFxTab()
|
||||
|
||||
void PluginEditor::buildMacroTab()
|
||||
{
|
||||
macroView.setBounds (0, 60, kBaseW, kBaseH - 60);
|
||||
macroView.setBounds (0, layout::tabBarHeight, kBaseW, kBaseH - layout::tabBarHeight);
|
||||
root.addChildComponent (macroView);
|
||||
|
||||
constexpr int colW = 269; // four equal columns: 4*269 + 3*8 = 1100
|
||||
constexpr int colGap = 8;
|
||||
constexpr int macroH = 158;
|
||||
constexpr int colW = layout::fourColW;
|
||||
constexpr int colGap = layout::fourColGap;
|
||||
constexpr int macroH = layout::macroPanelH;
|
||||
|
||||
for (int i = 0; i < kNumMacros; ++i)
|
||||
{
|
||||
@@ -674,56 +824,68 @@ void PluginEditor::buildMacroTab()
|
||||
|
||||
// Macro assignment editor.
|
||||
const int assignTop = layout::margin + macroH + layout::panelSpacing;
|
||||
const int assignH = kBaseH - 60 - assignTop - layout::margin;
|
||||
const int assignH = kBaseH - layout::tabBarHeight - assignTop - layout::margin;
|
||||
macroAssignPanel.setBounds (layout::margin, assignTop, kBaseW - 2 * layout::margin, assignH);
|
||||
macroView.addAndMakeVisible (macroAssignPanel);
|
||||
|
||||
std::vector<ModTarget> targetEnums;
|
||||
const juce::StringArray targetItems = modTargetItems (targetEnums);
|
||||
|
||||
const int mCenter = layout::titleHeight + 30; // vertical centre of the (60px) depth knob
|
||||
const int mCenter = layout::headerCentreY; // centre line of the depth row
|
||||
|
||||
macroAssignIndex.addItemList ({ "Macro 1", "Macro 2", "Macro 3", "Macro 4" }, 1);
|
||||
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 ("Macro to assign");
|
||||
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 ("Destination parameter");
|
||||
macroAssignTarget.setTooltip ("Selects the destination parameter");
|
||||
macroAssignPanel.addAndMakeVisible (macroAssignTarget);
|
||||
|
||||
const int depthX = layout::padding + 140 + layout::gap + 220 + layout::gap;
|
||||
macroDepthKnob.setFormatter (fmtDepth);
|
||||
macroDepthKnob.setBounds (depthX, layout::titleHeight, layout::knobSize, 60);
|
||||
macroDepthKnob.setTooltip ("Macro assignment depth (0 to 1)");
|
||||
// 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 ("Assign destination to the selected macro");
|
||||
macroAssignButton.setTooltip ("Adds this destination to the selected macro");
|
||||
macroAssignButton.onClick = [this, targetEnums]
|
||||
{
|
||||
const int macro = macroAssignIndex.getSelectedItemIndex();
|
||||
const int tid = macroAssignTarget.getSelectedItemIndex();
|
||||
if (tid >= 0 && tid < (int) targetEnums.size())
|
||||
if (macro >= 0 && macro < kNumMacros && tid >= 0 && tid < (int) targetEnums.size())
|
||||
{
|
||||
bool added = false;
|
||||
{
|
||||
const juce::ScopedLock lock (processor.engine.getControlLock());
|
||||
processor.engine.getMacros().addAssignment (macro, targetEnums[(size_t) tid],
|
||||
(float) macroDepthKnob.getValue());
|
||||
added = processor.engine.getMacros().addAssignment (macro, targetEnums[(size_t) tid],
|
||||
(float) macroDepthKnob.getValue());
|
||||
}
|
||||
updateMacroList();
|
||||
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 ("Clear all macro assignments");
|
||||
macroClearButton.setTooltip ("Removes every macro assignment from all four macros");
|
||||
macroClearButton.onClick = [this]
|
||||
{
|
||||
{
|
||||
@@ -734,11 +896,12 @@ void PluginEditor::buildMacroTab()
|
||||
};
|
||||
macroAssignPanel.addAndMakeVisible (macroClearButton);
|
||||
|
||||
const int listTop = layout::titleHeight + 60 + layout::padding;
|
||||
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);
|
||||
@@ -751,6 +914,7 @@ 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);
|
||||
@@ -897,12 +1061,18 @@ void PluginEditor::updateVisuals()
|
||||
for (int p = 0; p < 4; ++p)
|
||||
{
|
||||
auto* knob = fxKnobs[(size_t) i][(size_t) p + 1];
|
||||
const juce::String name = kFxParamNames[type][p];
|
||||
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 (name == "Unused" ? "Not used by this effect"
|
||||
: name + " (normalised 0-100%)");
|
||||
knob->setTooltip (spec.tooltip);
|
||||
knob->setFormatter (spec.formatter);
|
||||
knob->setEnabled (name != "Unused");
|
||||
knob->repaint();
|
||||
}
|
||||
}
|
||||
@@ -926,26 +1096,33 @@ void PluginEditor::updateVisuals()
|
||||
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)
|
||||
@@ -954,6 +1131,8 @@ void PluginEditor::updateMacroList()
|
||||
}
|
||||
if (macroList.getText() != text)
|
||||
macroList.setText (text, false);
|
||||
macroAssignButton.setEnabled (macro >= 0 && macro < kNumMacros
|
||||
&& assigned < MacroControls::kMaxAssignments);
|
||||
}
|
||||
|
||||
void PluginEditor::timerCallback()
|
||||
|
||||
Reference in new issue
Block a user