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

@ -84,6 +84,7 @@ target_sources(Monostep PRIVATE
Source/PluginProcessor.cpp
Source/PluginEditor.cpp
Source/ui/SequencerMatrix.cpp
Source/dsp/FxProcessor.cpp
)
target_include_directories(Monostep PRIVATE Source)
@ -108,6 +109,7 @@ if(MONOSTEP_BUILD_TESTS)
target_include_directories(MonostepTestHost PRIVATE Source)
target_sources(MonostepTestHost PRIVATE
Source/PluginProcessor.cpp
Source/dsp/FxProcessor.cpp
tests/TestHost.cpp
)
target_compile_definitions(MonostepTestHost PRIVATE

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.

View file

@ -194,7 +194,7 @@ private:
ContentComponent content;
static constexpr int baseWidth = 1060;
static constexpr int baseWidth = 1280;
static constexpr int baseHeight = 780;
float zoomFactor = 1.0f;
@ -229,6 +229,13 @@ private:
Knob fAmountKnob;
Knob driveKnob;
Knob ringKnob;
Knob delayTimeKnob;
Knob delayStrengthKnob;
Knob reverbRoomKnob;
Knob reverbStrengthKnob;
Knob reverbDiffusionKnob;
juce::ToggleButton delaySyncToggle;
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> delaySyncAttachment;
Knob accentKnob;
ChoiceBar rateBar;

View file

@ -13,7 +13,9 @@ static const char* paramId (int index)
"seqLen", "seqRate", "seqRoot", "seqOctave", "seqSwing", "seqGateLen",
"drive", "ringmod", "accent", "fine1",
"filterAttack", "filterDecay", "filterSustain", "filterRelease", "filterEnvAmt",
"seqRetrig"
"seqRetrig",
"delayTime", "delayStrength", "delaySync",
"reverbRoom", "reverbStrength", "reverbDiffusion"
};
return ids[index];
}
@ -26,6 +28,8 @@ enum ParamIndex
pDrive, pRingMod, pAccent, pFine1,
pFilterAttack, pFilterDecay, pFilterSustain, pFilterRelease, pFilterEnvAmt,
pSeqRetrig,
pDelayTime, pDelayStrength, pDelaySync,
pReverbRoom, pReverbStrength, pReverbDiffusion,
numParams
};
@ -65,6 +69,12 @@ MonostepAudioProcessor::MonostepAudioProcessor()
ringModParam = apvts.getRawParameterValue (paramId (pRingMod));
accentParam = apvts.getRawParameterValue (paramId (pAccent));
seqRetrigParam = apvts.getRawParameterValue (paramId (pSeqRetrig));
delayTimeParam = apvts.getRawParameterValue (paramId (pDelayTime));
delayStrengthParam = apvts.getRawParameterValue (paramId (pDelayStrength));
delaySyncParam = apvts.getRawParameterValue (paramId (pDelaySync));
reverbRoomParam = apvts.getRawParameterValue (paramId (pReverbRoom));
reverbStrengthParam = apvts.getRawParameterValue (paramId (pReverbStrength));
reverbDiffusionParam = apvts.getRawParameterValue (paramId (pReverbDiffusion));
setDefaultPattern();
}
@ -172,6 +182,24 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonostepAudioProcessor::crea
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pAccent),
"Accent", juce::NormalisableRange<float> (0.0f, 1.0f), 0.7f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDelayTime),
"Delay Time", juce::NormalisableRange<float> (0.0f, 1.0f), 0.35f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDelayStrength),
"Delay Strength", juce::NormalisableRange<float> (0.0f, 1.0f), 0.0f));
layout.add (std::make_unique<juce::AudioParameterBool> (paramId (pDelaySync),
"Delay Sync", true));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pReverbRoom),
"Reverb Roomsize", juce::NormalisableRange<float> (0.0f, 1.0f), 0.5f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pReverbStrength),
"Reverb Strength", juce::NormalisableRange<float> (0.0f, 1.0f), 0.0f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pReverbDiffusion),
"Reverb Diffusion", juce::NormalisableRange<float> (0.0f, 1.0f), 0.6f));
return layout;
}
@ -391,6 +419,12 @@ void MonostepAudioProcessor::randomizeFx()
{
applyParamValue ("drive", random.nextFloat());
applyParamValue ("ringmod", random.nextFloat());
applyParamValue ("delayTime", random.nextFloat());
applyParamValue ("delayStrength", random.nextFloat() * 0.8f);
applyParamValue ("delaySync", random.nextFloat() < 0.8f ? 1.0f : 0.0f);
applyParamValue ("reverbRoom", 0.2f + random.nextFloat() * 0.7f);
applyParamValue ("reverbStrength", random.nextFloat() * 0.7f);
applyParamValue ("reverbDiffusion", random.nextFloat());
}
void MonostepAudioProcessor::randomizeParams()
@ -407,6 +441,7 @@ void MonostepAudioProcessor::prepareToPlay (double sr, int samplesPerBlock)
sampleRate = sr;
voice.prepare (sr);
scratch.setSize (1, samplesPerBlock);
fx.prepare (sr, juce::jmax (1, getTotalNumOutputChannels()));
lastStep = -1;
lastGateApplied = false;
lastFreqApplied = -1.0f;
@ -418,6 +453,7 @@ void MonostepAudioProcessor::prepareToPlay (double sr, int samplesPerBlock)
void MonostepAudioProcessor::releaseResources()
{
voice.reset();
fx.reset();
}
void MonostepAudioProcessor::updateVoiceParams()
@ -676,6 +712,17 @@ void MonostepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
for (int ch = 0; ch < numChannels; ++ch)
buffer.addFrom (ch, 0, scratch, 0, 0, numSamples);
monostep::FxProcessor::Params fp;
fp.delayTime = delayTimeParam->load();
fp.delayMix = delayStrengthParam->load();
fp.delaySync = delaySyncParam->load() >= 0.5f;
fp.reverbRoom = reverbRoomParam->load();
fp.reverbMix = reverbStrengthParam->load();
fp.reverbDiff = reverbDiffusionParam->load();
fp.bpm = bpm;
fx.setParams (fp);
fx.process (buffer, numSamples);
}
void MonostepAudioProcessor::setStepGate (int idx, bool gate)

View file

@ -3,6 +3,7 @@
#include <JuceHeader.h>
#include "dsp/SynthVoice.h"
#include "dsp/StepSequencer.h"
#include "dsp/FxProcessor.h"
class MonostepAudioProcessor final : public juce::AudioProcessor
{
@ -20,7 +21,7 @@ public:
const juce::String getName() const override { return "Monostep"; }
bool acceptsMidi() const override { return true; }
bool producesMidi() const override { return false; }
double getTailLengthSeconds() const override { return 0.0; }
double getTailLengthSeconds() const override { return 4.0; }
int getNumPrograms() override { return 1; }
int getCurrentProgram() override { return 0; }
@ -106,9 +107,16 @@ private:
std::atomic<float>* ringModParam = nullptr;
std::atomic<float>* accentParam = nullptr;
std::atomic<float>* seqRetrigParam = nullptr;
std::atomic<float>* delayTimeParam = nullptr;
std::atomic<float>* delayStrengthParam = nullptr;
std::atomic<float>* delaySyncParam = nullptr;
std::atomic<float>* reverbRoomParam = nullptr;
std::atomic<float>* reverbStrengthParam = nullptr;
std::atomic<float>* reverbDiffusionParam = nullptr;
monostep::StepSequencer sequencer;
monostep::SynthVoice voice;
monostep::FxProcessor fx;
juce::AudioBuffer<float> scratch;
juce::Random random;

231
Source/dsp/FxProcessor.cpp Normal file
View file

@ -0,0 +1,231 @@
#include "FxProcessor.h"
#include <algorithm>
#include <cmath>
namespace monostep
{
// Freeverb-style comb / allpass sizes, slightly offset per channel so the
// left and right reverb tails decorrelate.
static constexpr int combSizes[2][4] =
{
{ 1116, 1188, 1277, 1356 },
{ 1126, 1198, 1287, 1366 }
};
static constexpr int allpassSizes[2][2] =
{
{ 225, 556 },
{ 235, 566 }
};
float FxProcessor::delayTimeSeconds (float knob, bool sync, double bpm)
{
if (sync)
{
static const float divisions[] = { 0.25f, 0.5f, 0.75f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f };
const int idx = juce::roundToInt (juce::jlimit (0.0f, 1.0f, knob) * 7.0f);
return divisions[juce::jlimit (0, 7, idx)] * 60.0f / (float) juce::jmax (1.0, bpm);
}
return 0.005f * std::pow (200.0f, juce::jlimit (0.0f, 1.0f, knob));
}
juce::String FxProcessor::delayTimeLabel (float knob, bool sync, double bpm)
{
if (sync)
{
static const char* names[] = { "1/16", "1/8", "1/8D", "1/4", "1/4D", "1/2", "1/2D", "1W" };
const int idx = juce::roundToInt (juce::jlimit (0.0f, 1.0f, knob) * 7.0f);
return names[juce::jlimit (0, 7, idx)];
}
return juce::String (juce::roundToInt (delayTimeSeconds (knob, false, bpm) * 1000.0f)) + " ms";
}
void FxProcessor::prepare (double sr, int channels)
{
sampleRate = juce::jmax (44100.0, sr);
numChannels = juce::jmax (1, channels);
maxDelaySamples = (int) (sampleRate * 2.0);
delays.clear();
delays.resize (numChannels);
for (auto& d : delays)
{
d.memory.assign ((size_t) maxDelaySamples + 8, 0.0f);
d.writeIndex = 0;
d.smoothedDelay = 0.0f;
d.feedbackLp = 0.0f;
}
const float srScale = (float) (sampleRate / 44100.0);
for (int ch = 0; ch < 2; ++ch)
{
for (int i = 0; i < 4; ++i)
combDelaySamples[ch][i] = juce::roundToInt (combSizes[ch][i] * srScale);
for (int i = 0; i < 2; ++i)
allpassDelaySamples[ch][i] = juce::roundToInt (allpassSizes[ch][i] * srScale);
}
reverbs.clear();
reverbs.resize (numChannels);
for (int ch = 0; ch < numChannels; ++ch)
{
const int rc = ch & 1;
auto& r = reverbs[ch];
for (int i = 0; i < 4; ++i)
r.combs[i].assign ((size_t) combDelaySamples[rc][i], 0.0f);
for (int i = 0; i < 2; ++i)
r.allpasses[i].assign ((size_t) allpassDelaySamples[rc][i], 0.0f);
}
}
void FxProcessor::reset()
{
for (auto& d : delays)
{
std::fill (d.memory.begin(), d.memory.end(), 0.0f);
d.writeIndex = 0;
d.smoothedDelay = 0.0f;
d.feedbackLp = 0.0f;
}
for (auto& r : reverbs)
{
for (int i = 0; i < 4; ++i)
{
std::fill (r.combs[i].begin(), r.combs[i].end(), 0.0f);
r.combIndex[i] = 0;
}
for (int i = 0; i < 2; ++i)
{
std::fill (r.allpasses[i].begin(), r.allpasses[i].end(), 0.0f);
r.allpassIndex[i] = 0;
}
}
}
void FxProcessor::setParams (const Params& p)
{
params.delayTime = juce::jlimit (0.0f, 1.0f, p.delayTime);
params.delayMix = juce::jlimit (0.0f, 1.0f, p.delayMix);
params.delaySync = p.delaySync;
params.reverbRoom = juce::jlimit (0.0f, 1.0f, p.reverbRoom);
params.reverbMix = juce::jlimit (0.0f, 1.0f, p.reverbMix);
params.reverbDiff = juce::jlimit (0.0f, 1.0f, p.reverbDiff);
params.bpm = juce::jmax (20.0, p.bpm);
currentDelaySeconds = delayTimeSeconds (params.delayTime, params.delaySync, params.bpm);
}
void FxProcessor::process (juce::AudioBuffer<float>& buffer, int numSamples)
{
if (numSamples <= 0 || buffer.getNumChannels() == 0)
return;
processDelay (buffer, numSamples);
processReverb (buffer, numSamples);
}
void FxProcessor::processDelay (juce::AudioBuffer<float>& buffer, int numSamples)
{
if (params.delayMix <= 0.0001f)
return;
const int n = juce::jmin (numChannels, buffer.getNumChannels());
const float targetDelay = currentDelaySeconds * (float) sampleRate;
const float mix = params.delayMix;
for (int ch = 0; ch < n; ++ch)
{
auto& d = delays[ch];
auto* out = buffer.getWritePointer (ch);
const int size = (int) d.memory.size();
const int maxD = juce::jmax (1, size - 8);
for (int i = 0; i < numSamples; ++i)
{
const float x = out[i];
d.smoothedDelay += (targetDelay - d.smoothedDelay) * 0.0008f;
const float delay = juce::jlimit (8.0f, (float) maxD, d.smoothedDelay);
float readPos = (float) d.writeIndex - delay;
if (readPos < 0.0f)
readPos += (float) size;
const int idxA = (int) readPos;
const int idxB = (idxA + 1) % size;
const float frac = readPos - (float) idxA;
const float delayed = d.memory[idxA] + frac * (d.memory[idxB] - d.memory[idxA]);
// Feedback with a little high-frequency damping.
d.feedbackLp += 0.35f * (delayed - d.feedbackLp);
d.memory[d.writeIndex] = x + 0.42f * d.feedbackLp;
out[i] = x + delayed * mix;
if (++d.writeIndex >= size)
d.writeIndex = 0;
}
}
}
void FxProcessor::processReverb (juce::AudioBuffer<float>& buffer, int numSamples)
{
if (params.reverbMix <= 0.0001f)
return;
const int n = juce::jmin (numChannels, buffer.getNumChannels());
const float combGain = 0.70f + 0.28f * params.reverbRoom; // 0.70 .. 0.98
const float allpassGain = 0.75f * params.reverbDiff; // 0 .. 0.75
const float mix = params.reverbMix;
for (int ch = 0; ch < n; ++ch)
{
auto& r = reverbs[ch];
auto* out = buffer.getWritePointer (ch);
for (int i = 0; i < numSamples; ++i)
{
const float x = out[i];
float combSum = 0.0f;
for (int c = 0; c < 4; ++c)
{
auto& buf = r.combs[c];
const int size = (int) buf.size();
int& pos = r.combIndex[c];
const float delayed = buf[pos];
buf[pos] = x + combGain * delayed;
combSum += delayed;
if (++pos >= size)
pos = 0;
}
float wet = combSum * 0.012f;
for (int a = 0; a < 2; ++a)
{
auto& buf = r.allpasses[a];
const int size = (int) buf.size();
int& pos = r.allpassIndex[a];
const float delayed = buf[pos];
const float outAp = -allpassGain * wet + delayed;
buf[pos] = wet + allpassGain * delayed;
wet = outAp;
if (++pos >= size)
pos = 0;
}
out[i] = x * (1.0f - mix) + wet * mix;
}
}
}
} // namespace monostep

71
Source/dsp/FxProcessor.h Normal file
View file

@ -0,0 +1,71 @@
#pragma once
#include <JuceHeader.h>
#include <vector>
namespace monostep
{
// Tempo-synced delay + Schroeder reverb, applied after the voice.
class FxProcessor
{
public:
FxProcessor() = default;
void prepare (double sampleRate, int numChannels);
void reset();
struct Params
{
float delayTime = 0.35f; // normalized knob 0..1
float delayMix = 0.0f; // 0..1 dry/wet
bool delaySync = true; // tempo-synced vs. free milliseconds
float reverbRoom = 0.5f; // 0..1
float reverbMix = 0.0f; // 0..1 dry/wet
float reverbDiff = 0.6f; // 0..1 allpass diffusion
double bpm = 120.0;
};
void setParams (const Params& p);
void process (juce::AudioBuffer<float>& buffer, int numSamples);
// Shared knob-to-time mapping so the editor displays the same values the DSP uses.
static float delayTimeSeconds (float knobNormalized, bool sync, double bpm);
static juce::String delayTimeLabel (float knobNormalized, bool sync, double bpm);
private:
struct DelayChannel
{
std::vector<float> memory;
int writeIndex = 0;
float smoothedDelay = 0.0f;
float feedbackLp = 0.0f;
};
struct ReverbChannel
{
std::vector<float> combs[4];
std::vector<float> allpasses[2];
int combIndex[4] = {};
int allpassIndex[2] = {};
};
void processDelay (juce::AudioBuffer<float>& buffer, int numSamples);
void processReverb (juce::AudioBuffer<float>& buffer, int numSamples);
double sampleRate = 44100.0;
int numChannels = 2;
int maxDelaySamples = 0;
int combDelaySamples[2][4] = {};
int allpassDelaySamples[2][2] = {};
Params params;
float currentDelaySeconds = 0.0f;
std::vector<DelayChannel> delays;
std::vector<ReverbChannel> reverbs;
};
} // namespace monostep

View file

@ -299,6 +299,84 @@ static int testProcessorRendering()
std::cout << "Master=0 silences output OK\n";
// delay + reverb FX: after the note releases, the FX tail must ring out
// (delay repeats / reverb decay) instead of going silent, with no NaN/blow-up.
*processor.getAPVTS().getRawParameterValue ("master") = 1.0f;
*processor.getAPVTS().getRawParameterValue ("release") = 0.01f;
*processor.getAPVTS().getRawParameterValue ("delayStrength") = 0.0f;
*processor.getAPVTS().getRawParameterValue ("reverbStrength") = 0.0f;
const int releaseBlock = numBlocks / 2;
const auto renderFxTail = [&] () -> double
{
processor.reset();
processor.prepareToPlay (44100.0, 512);
juce::AudioBuffer<float> fxBlock (2, blockSize);
juce::MidiBuffer fxMidi;
double tailSum = 0.0;
int tailSamples = 0;
bool finite = true;
for (int b = 0; b < numBlocks; ++b)
{
fxBlock.clear();
fxMidi.clear();
if (b == 0)
fxMidi.addEvent (juce::MidiMessage::noteOn (1, 60, 0.9f), 0);
else if (b == releaseBlock)
fxMidi.addEvent (juce::MidiMessage::noteOff (1, 60), 0);
processor.processBlock (fxBlock, fxMidi);
if (b > releaseBlock)
for (int c = 0; c < 2; ++c)
for (int i = 0; i < blockSize; ++i)
{
const float s = fxBlock.getSample (c, i);
if (! std::isfinite (s))
finite = false;
tailSum += (double) s * s;
++tailSamples;
}
}
if (! finite)
return -1.0;
return std::sqrt (tailSum / (double) std::max (1, tailSamples));
};
const double dryTail = renderFxTail();
if (dryTail > 1e-3)
{
std::cout << "FAILED: dry tail after release should be silent, got " << dryTail << "\n";
return 1;
}
*processor.getAPVTS().getRawParameterValue ("delayStrength") = 0.8f;
*processor.getAPVTS().getRawParameterValue ("delayTime") = 0.4f;
*processor.getAPVTS().getRawParameterValue ("delaySync") = 1.0f;
*processor.getAPVTS().getRawParameterValue ("reverbStrength") = 0.8f;
*processor.getAPVTS().getRawParameterValue ("reverbRoom") = 0.9f;
*processor.getAPVTS().getRawParameterValue ("reverbDiffusion") = 1.0f;
const double wetTail = renderFxTail();
if (wetTail < 0.0)
return 1;
if (wetTail < 1e-3)
{
std::cout << "FAILED: delay/reverb tail is inaudible (wet=" << wetTail << ")\n";
return 1;
}
std::cout << "Delay + reverb FX OK (dryTail=" << dryTail << " wetTail=" << wetTail << ")\n";
return 0;
}