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
|
|
@ -84,6 +84,7 @@ target_sources(Monostep PRIVATE
|
||||||
Source/PluginProcessor.cpp
|
Source/PluginProcessor.cpp
|
||||||
Source/PluginEditor.cpp
|
Source/PluginEditor.cpp
|
||||||
Source/ui/SequencerMatrix.cpp
|
Source/ui/SequencerMatrix.cpp
|
||||||
|
Source/dsp/FxProcessor.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(Monostep PRIVATE Source)
|
target_include_directories(Monostep PRIVATE Source)
|
||||||
|
|
@ -108,6 +109,7 @@ if(MONOSTEP_BUILD_TESTS)
|
||||||
target_include_directories(MonostepTestHost PRIVATE Source)
|
target_include_directories(MonostepTestHost PRIVATE Source)
|
||||||
target_sources(MonostepTestHost PRIVATE
|
target_sources(MonostepTestHost PRIVATE
|
||||||
Source/PluginProcessor.cpp
|
Source/PluginProcessor.cpp
|
||||||
|
Source/dsp/FxProcessor.cpp
|
||||||
tests/TestHost.cpp
|
tests/TestHost.cpp
|
||||||
)
|
)
|
||||||
target_compile_definitions(MonostepTestHost PRIVATE
|
target_compile_definitions(MonostepTestHost PRIVATE
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "PluginProcessor.h"
|
#include "PluginProcessor.h"
|
||||||
#include "Theme.h"
|
#include "Theme.h"
|
||||||
#include "BuildInfo.h"
|
#include "BuildInfo.h"
|
||||||
|
#include "dsp/FxProcessor.h"
|
||||||
|
|
||||||
using namespace monostep;
|
using namespace monostep;
|
||||||
|
|
||||||
|
|
@ -657,6 +658,11 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess
|
||||||
fAmountKnob ("Amt"),
|
fAmountKnob ("Amt"),
|
||||||
driveKnob ("Drive"),
|
driveKnob ("Drive"),
|
||||||
ringKnob ("Ring"),
|
ringKnob ("Ring"),
|
||||||
|
delayTimeKnob ("Time"),
|
||||||
|
delayStrengthKnob ("Strength"),
|
||||||
|
reverbRoomKnob ("Room"),
|
||||||
|
reverbStrengthKnob ("Strength"),
|
||||||
|
reverbDiffusionKnob ("Diff"),
|
||||||
accentKnob ("Accent"),
|
accentKnob ("Accent"),
|
||||||
rateBar (processor.getAPVTS(), "seqRate", { "1/4", "1/8", "1/8T", "1/16", "1/32" }),
|
rateBar (processor.getAPVTS(), "seqRate", { "1/4", "1/8", "1/8T", "1/16", "1/32" }),
|
||||||
octaveBar (processor.getAPVTS(), "seqOctave", { "-2", "-1", "0", "+1", "+2" }),
|
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 (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 (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 (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)) + "%"; });
|
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);
|
fAmountKnob.setDragSensitivity (800);
|
||||||
|
|
||||||
content.addAndMakeVisible (matrix);
|
content.addAndMakeVisible (matrix);
|
||||||
|
|
@ -895,11 +915,20 @@ void MonostepAudioProcessorEditor::paintContent (juce::Graphics& g)
|
||||||
return r;
|
return r;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Draw a knob group: subtle gradient behind the title legend + a slight border
|
// Draw a knob group frame: a slight border around the knobs themselves.
|
||||||
// around the knobs themselves. leftPad extends the frame to the left so it can
|
// leftPad extends the frame to the left so it can enclose side labels.
|
||||||
// enclose side labels.
|
auto drawFrame = [&] (const juce::Component& first, const juce::Component& last,
|
||||||
auto drawGroup = [&] (const juce::Component& first, const juce::Component& last,
|
float pad, float leftPad = 0.0f)
|
||||||
const juce::String& title, 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 capX = first.getX() - (int) leftPad;
|
||||||
const int capW = last.getRight() - capX;
|
const int capW = last.getRight() - capX;
|
||||||
|
|
@ -912,19 +941,24 @@ void MonostepAudioProcessorEditor::paintContent (juce::Graphics& g)
|
||||||
g.setFont (font (9.0f).boldened());
|
g.setFont (font (9.0f).boldened());
|
||||||
g.setColour (colours::bg);
|
g.setColour (colours::bg);
|
||||||
g.drawText (title, legend, juce::Justification::centred);
|
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));
|
drawFrame (wave1Box, mixKnob, 6.0f, (float) (oscLabelW - 21 + 9));
|
||||||
drawGroup (glideKnob, accentKnob, "NOTE", 6.0f);
|
drawPill (wave1Box, mixKnob, "OSC", (float) (oscLabelW - 21 + 9));
|
||||||
drawGroup (driveKnob, ringKnob, "DIST", 6.0f);
|
drawFrame (glideKnob, accentKnob, 6.0f);
|
||||||
drawGroup (filterTypeBox, resKnob, "FILTER", 6.0f, 6.0f);
|
drawPill (glideKnob, accentKnob, "NOTE");
|
||||||
drawGroup (attackKnob, releaseKnob, "AMP ENV", 6.0f);
|
|
||||||
drawGroup (fAttackKnob, fAmountKnob, "FILTER ENV", 6.0f);
|
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;
|
const int footY = baseHeight - 20;
|
||||||
g.setColour (colours::grid);
|
g.setColour (colours::grid);
|
||||||
|
|
@ -993,7 +1027,7 @@ void MonostepAudioProcessorEditor::resized()
|
||||||
const int oscColW = oscLabelW + oscDropW + oscGapW; // labels + dropdowns + gap to knobs
|
const int oscColW = oscLabelW + oscDropW + oscGapW; // labels + dropdowns + gap to knobs
|
||||||
|
|
||||||
// Both rows are centered over the full bottom bar width.
|
// 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 oscX = pad + ((w - 2 * pad) - blockW) / 2 + oscColW;
|
||||||
const int row1Y = bottomY + 24;
|
const int row1Y = bottomY + 24;
|
||||||
const int row2Y = row1Y + knobH + 34; // row 1 + a caption band
|
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.
|
// Row 1: coarse / detune / mix / glide / accent / FX (drive / ring / delay / reverb).
|
||||||
// NOTE and DIST are pushed apart to clear the group frames.
|
|
||||||
placeRow (row1Y, oscX, { &coarseKnob, &detuneKnob, &mixKnob });
|
placeRow (row1Y, oscX, { &coarseKnob, &detuneKnob, &mixKnob });
|
||||||
placeRow (row1Y, oscX + 3 * step + 24, { &glideKnob, &accentKnob });
|
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.
|
// Row 2: Filter (dropdown + cutoff, res) [gap] Amp Envelope [gap] Filter Env.
|
||||||
// The three sections are centered as one block with equal frame gaps.
|
// The three sections are centered as one block with equal frame gaps.
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,7 @@ private:
|
||||||
|
|
||||||
ContentComponent content;
|
ContentComponent content;
|
||||||
|
|
||||||
static constexpr int baseWidth = 1060;
|
static constexpr int baseWidth = 1280;
|
||||||
static constexpr int baseHeight = 780;
|
static constexpr int baseHeight = 780;
|
||||||
|
|
||||||
float zoomFactor = 1.0f;
|
float zoomFactor = 1.0f;
|
||||||
|
|
@ -229,6 +229,13 @@ private:
|
||||||
Knob fAmountKnob;
|
Knob fAmountKnob;
|
||||||
Knob driveKnob;
|
Knob driveKnob;
|
||||||
Knob ringKnob;
|
Knob ringKnob;
|
||||||
|
Knob delayTimeKnob;
|
||||||
|
Knob delayStrengthKnob;
|
||||||
|
Knob reverbRoomKnob;
|
||||||
|
Knob reverbStrengthKnob;
|
||||||
|
Knob reverbDiffusionKnob;
|
||||||
|
juce::ToggleButton delaySyncToggle;
|
||||||
|
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> delaySyncAttachment;
|
||||||
Knob accentKnob;
|
Knob accentKnob;
|
||||||
|
|
||||||
ChoiceBar rateBar;
|
ChoiceBar rateBar;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ static const char* paramId (int index)
|
||||||
"seqLen", "seqRate", "seqRoot", "seqOctave", "seqSwing", "seqGateLen",
|
"seqLen", "seqRate", "seqRoot", "seqOctave", "seqSwing", "seqGateLen",
|
||||||
"drive", "ringmod", "accent", "fine1",
|
"drive", "ringmod", "accent", "fine1",
|
||||||
"filterAttack", "filterDecay", "filterSustain", "filterRelease", "filterEnvAmt",
|
"filterAttack", "filterDecay", "filterSustain", "filterRelease", "filterEnvAmt",
|
||||||
"seqRetrig"
|
"seqRetrig",
|
||||||
|
"delayTime", "delayStrength", "delaySync",
|
||||||
|
"reverbRoom", "reverbStrength", "reverbDiffusion"
|
||||||
};
|
};
|
||||||
return ids[index];
|
return ids[index];
|
||||||
}
|
}
|
||||||
|
|
@ -26,6 +28,8 @@ enum ParamIndex
|
||||||
pDrive, pRingMod, pAccent, pFine1,
|
pDrive, pRingMod, pAccent, pFine1,
|
||||||
pFilterAttack, pFilterDecay, pFilterSustain, pFilterRelease, pFilterEnvAmt,
|
pFilterAttack, pFilterDecay, pFilterSustain, pFilterRelease, pFilterEnvAmt,
|
||||||
pSeqRetrig,
|
pSeqRetrig,
|
||||||
|
pDelayTime, pDelayStrength, pDelaySync,
|
||||||
|
pReverbRoom, pReverbStrength, pReverbDiffusion,
|
||||||
numParams
|
numParams
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -65,6 +69,12 @@ MonostepAudioProcessor::MonostepAudioProcessor()
|
||||||
ringModParam = apvts.getRawParameterValue (paramId (pRingMod));
|
ringModParam = apvts.getRawParameterValue (paramId (pRingMod));
|
||||||
accentParam = apvts.getRawParameterValue (paramId (pAccent));
|
accentParam = apvts.getRawParameterValue (paramId (pAccent));
|
||||||
seqRetrigParam = apvts.getRawParameterValue (paramId (pSeqRetrig));
|
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();
|
setDefaultPattern();
|
||||||
}
|
}
|
||||||
|
|
@ -172,6 +182,24 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonostepAudioProcessor::crea
|
||||||
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pAccent),
|
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pAccent),
|
||||||
"Accent", juce::NormalisableRange<float> (0.0f, 1.0f), 0.7f));
|
"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;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,6 +419,12 @@ void MonostepAudioProcessor::randomizeFx()
|
||||||
{
|
{
|
||||||
applyParamValue ("drive", random.nextFloat());
|
applyParamValue ("drive", random.nextFloat());
|
||||||
applyParamValue ("ringmod", 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()
|
void MonostepAudioProcessor::randomizeParams()
|
||||||
|
|
@ -407,6 +441,7 @@ void MonostepAudioProcessor::prepareToPlay (double sr, int samplesPerBlock)
|
||||||
sampleRate = sr;
|
sampleRate = sr;
|
||||||
voice.prepare (sr);
|
voice.prepare (sr);
|
||||||
scratch.setSize (1, samplesPerBlock);
|
scratch.setSize (1, samplesPerBlock);
|
||||||
|
fx.prepare (sr, juce::jmax (1, getTotalNumOutputChannels()));
|
||||||
lastStep = -1;
|
lastStep = -1;
|
||||||
lastGateApplied = false;
|
lastGateApplied = false;
|
||||||
lastFreqApplied = -1.0f;
|
lastFreqApplied = -1.0f;
|
||||||
|
|
@ -418,6 +453,7 @@ void MonostepAudioProcessor::prepareToPlay (double sr, int samplesPerBlock)
|
||||||
void MonostepAudioProcessor::releaseResources()
|
void MonostepAudioProcessor::releaseResources()
|
||||||
{
|
{
|
||||||
voice.reset();
|
voice.reset();
|
||||||
|
fx.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MonostepAudioProcessor::updateVoiceParams()
|
void MonostepAudioProcessor::updateVoiceParams()
|
||||||
|
|
@ -676,6 +712,17 @@ void MonostepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
|
||||||
|
|
||||||
for (int ch = 0; ch < numChannels; ++ch)
|
for (int ch = 0; ch < numChannels; ++ch)
|
||||||
buffer.addFrom (ch, 0, scratch, 0, 0, numSamples);
|
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)
|
void MonostepAudioProcessor::setStepGate (int idx, bool gate)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include <JuceHeader.h>
|
#include <JuceHeader.h>
|
||||||
#include "dsp/SynthVoice.h"
|
#include "dsp/SynthVoice.h"
|
||||||
#include "dsp/StepSequencer.h"
|
#include "dsp/StepSequencer.h"
|
||||||
|
#include "dsp/FxProcessor.h"
|
||||||
|
|
||||||
class MonostepAudioProcessor final : public juce::AudioProcessor
|
class MonostepAudioProcessor final : public juce::AudioProcessor
|
||||||
{
|
{
|
||||||
|
|
@ -20,7 +21,7 @@ public:
|
||||||
const juce::String getName() const override { return "Monostep"; }
|
const juce::String getName() const override { return "Monostep"; }
|
||||||
bool acceptsMidi() const override { return true; }
|
bool acceptsMidi() const override { return true; }
|
||||||
bool producesMidi() const override { return false; }
|
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 getNumPrograms() override { return 1; }
|
||||||
int getCurrentProgram() override { return 0; }
|
int getCurrentProgram() override { return 0; }
|
||||||
|
|
@ -106,9 +107,16 @@ private:
|
||||||
std::atomic<float>* ringModParam = nullptr;
|
std::atomic<float>* ringModParam = nullptr;
|
||||||
std::atomic<float>* accentParam = nullptr;
|
std::atomic<float>* accentParam = nullptr;
|
||||||
std::atomic<float>* seqRetrigParam = 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::StepSequencer sequencer;
|
||||||
monostep::SynthVoice voice;
|
monostep::SynthVoice voice;
|
||||||
|
monostep::FxProcessor fx;
|
||||||
juce::AudioBuffer<float> scratch;
|
juce::AudioBuffer<float> scratch;
|
||||||
juce::Random random;
|
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
|
||||||
|
|
@ -299,6 +299,84 @@ static int testProcessorRendering()
|
||||||
|
|
||||||
std::cout << "Master=0 silences output OK\n";
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue