Add delay and reverb FX section with full DSP

Rename the DIST section to FX and expand it with tempo-synced delay
(Time/Strength/Sync) and Schroeder reverb (Roomsize/Strength/Diffusion).
New FxProcessor runs post-voice on the stereo bus; widen the editor to
1280px to fit the new section. Add a TestHost check that the FX tail
rings out after note release.
This commit is contained in:
Armin 2026-08-06 16:35:34 +02:00
commit 342d413563
8 changed files with 505 additions and 23 deletions

View file

@ -3,6 +3,7 @@
#include "PluginProcessor.h"
#include "Theme.h"
#include "BuildInfo.h"
#include "dsp/FxProcessor.h"
using namespace monostep;
@ -657,6 +658,11 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess
fAmountKnob ("Amt"),
driveKnob ("Drive"),
ringKnob ("Ring"),
delayTimeKnob ("Time"),
delayStrengthKnob ("Strength"),
reverbRoomKnob ("Room"),
reverbStrengthKnob ("Strength"),
reverbDiffusionKnob ("Diff"),
accentKnob ("Accent"),
rateBar (processor.getAPVTS(), "seqRate", { "1/4", "1/8", "1/8T", "1/16", "1/32" }),
octaveBar (processor.getAPVTS(), "seqOctave", { "-2", "-1", "0", "+1", "+2" }),
@ -759,8 +765,22 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess
addKnob (fAmountKnob, "filterEnvAmt", [] (float v) { return (v >= 0.0f ? "+" : "") + juce::String (v, 2) + " oct"; });
addKnob (driveKnob, "drive", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
addKnob (ringKnob, "ringmod", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
addKnob (delayTimeKnob, "delayTime", [this] (float v)
{
const bool sync = processorRef.getAPVTS().getRawParameterValue ("delaySync")->load() >= 0.5f;
return monostep::FxProcessor::delayTimeLabel (v, sync, 120.0);
});
addKnob (delayStrengthKnob, "delayStrength", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
addKnob (reverbRoomKnob, "reverbRoom", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
addKnob (reverbStrengthKnob, "reverbStrength", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
addKnob (reverbDiffusionKnob, "reverbDiffusion", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
addKnob (accentKnob, "accent", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
delaySyncToggle.setButtonText ("Sync");
delaySyncToggle.setTooltip ("Snap the delay time to the host tempo (off: free milliseconds)");
delaySyncAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment> (apvts, "delaySync", delaySyncToggle);
content.addAndMakeVisible (delaySyncToggle);
fAmountKnob.setDragSensitivity (800);
content.addAndMakeVisible (matrix);
@ -895,11 +915,20 @@ void MonostepAudioProcessorEditor::paintContent (juce::Graphics& g)
return r;
};
// Draw a knob group: subtle gradient behind the title legend + a slight border
// around the knobs themselves. leftPad extends the frame to the left so it can
// enclose side labels.
auto drawGroup = [&] (const juce::Component& first, const juce::Component& last,
const juce::String& title, float pad, float leftPad = 0.0f)
// Draw a knob group frame: a slight border around the knobs themselves.
// leftPad extends the frame to the left so it can enclose side labels.
auto drawFrame = [&] (const juce::Component& first, const juce::Component& last,
float pad, float leftPad = 0.0f)
{
auto box = groupBox (first, last, pad);
box.setLeft (box.getX() - leftPad);
g.setColour (colours::section.withAlpha (0.30f));
g.drawRoundedRectangle (box, 3.0f, 1.0f);
};
// A gradient legend pill spanning the given group, with the title centred.
auto drawPill = [&] (const juce::Component& first, const juce::Component& last,
const juce::String& title, float leftPad = 0.0f)
{
const int capX = first.getX() - (int) leftPad;
const int capW = last.getRight() - capX;
@ -912,19 +941,24 @@ void MonostepAudioProcessorEditor::paintContent (juce::Graphics& g)
g.setFont (font (9.0f).boldened());
g.setColour (colours::bg);
g.drawText (title, legend, juce::Justification::centred);
auto box = groupBox (first, last, pad);
box.setLeft (box.getX() - leftPad);
g.setColour (colours::section.withAlpha (0.30f));
g.drawRoundedRectangle (box, 3.0f, 1.0f);
};
drawGroup (wave1Box, mixKnob, "OSC", 6.0f, (float) (oscLabelW - 21 + 9));
drawGroup (glideKnob, accentKnob, "NOTE", 6.0f);
drawGroup (driveKnob, ringKnob, "DIST", 6.0f);
drawGroup (filterTypeBox, resKnob, "FILTER", 6.0f, 6.0f);
drawGroup (attackKnob, releaseKnob, "AMP ENV", 6.0f);
drawGroup (fAttackKnob, fAmountKnob, "FILTER ENV", 6.0f);
drawFrame (wave1Box, mixKnob, 6.0f, (float) (oscLabelW - 21 + 9));
drawPill (wave1Box, mixKnob, "OSC", (float) (oscLabelW - 21 + 9));
drawFrame (glideKnob, accentKnob, 6.0f);
drawPill (glideKnob, accentKnob, "NOTE");
drawFrame (driveKnob, reverbDiffusionKnob, 6.0f);
drawPill (driveKnob, ringKnob, "FX");
drawPill (delayTimeKnob, delaySyncToggle, "DELAY");
drawPill (reverbRoomKnob, reverbDiffusionKnob, "REVERB");
drawFrame (filterTypeBox, resKnob, 6.0f, 6.0f);
drawPill (filterTypeBox, resKnob, "FILTER", 6.0f);
drawFrame (attackKnob, releaseKnob, 6.0f);
drawPill (attackKnob, releaseKnob, "AMP ENV");
drawFrame (fAttackKnob, fAmountKnob, 6.0f);
drawPill (fAttackKnob, fAmountKnob, "FILTER ENV");
const int footY = baseHeight - 20;
g.setColour (colours::grid);
@ -993,7 +1027,7 @@ void MonostepAudioProcessorEditor::resized()
const int oscColW = oscLabelW + oscDropW + oscGapW; // labels + dropdowns + gap to knobs
// Both rows are centered over the full bottom bar width.
const int blockW = oscColW + 6 * step + knobW + 48; // +48 = the two group gaps
const int blockW = oscColW + 12 * step + 104; // osc col + FX (8 widgets + subgroup gaps)
const int oscX = pad + ((w - 2 * pad) - blockW) / 2 + oscColW;
const int row1Y = bottomY + 24;
const int row2Y = row1Y + knobH + 34; // row 1 + a caption band
@ -1013,11 +1047,15 @@ void MonostepAudioProcessorEditor::resized()
}
};
// Row 1: coarse / detune / mix / glide / accent / drive / ring.
// NOTE and DIST are pushed apart to clear the group frames.
// Row 1: coarse / detune / mix / glide / accent / FX (drive / ring / delay / reverb).
placeRow (row1Y, oscX, { &coarseKnob, &detuneKnob, &mixKnob });
placeRow (row1Y, oscX + 3 * step + 24, { &glideKnob, &accentKnob });
placeRow (row1Y, oscX + 5 * step + 48, { &driveKnob, &ringKnob });
const int fxX = oscX + 5 * step + 48;
placeRow (row1Y, fxX, { &driveKnob, &ringKnob });
placeRow (row1Y, fxX + 2 * step + 24, { &delayTimeKnob, &delayStrengthKnob });
placeRow (row1Y, fxX + 5 * step + 48, { &reverbRoomKnob, &reverbStrengthKnob, &reverbDiffusionKnob });
delaySyncToggle.setBounds (fxX + 4 * step + 24, row1Y + (knobH - 24) / 2, knobW, 24);
// Row 2: Filter (dropdown + cutoff, res) [gap] Amp Envelope [gap] Filter Env.
// The three sections are centered as one block with equal frame gaps.