mirror of
https://codeberg.org/armin/monostep.git
synced 2026-09-01 04:10:46 +02:00
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:
parent
a45006c201
commit
342d413563
8 changed files with 505 additions and 23 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
231
Source/dsp/FxProcessor.cpp
Normal 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
71
Source/dsp/FxProcessor.h
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue