Add delay/reverb dry-wet, granular randomize toggles, rework accent DSP

- Add delay and reverb Dry/Wet parameters and knobs, wired into presets
- Add Accent/Slide/Fine/Rate toggles to the Randomize panel with new
  per-aspect pattern randomizers
- Soften accent: modest volume lift plus small cutoff and filter-attack
  modulation instead of the heavy 5x gain boost
- Halve reverb comb/allpass delays so the wet speaks sooner
- Regenerate a fresh build stamp on every build
This commit is contained in:
Armin 2026-08-06 18:38:32 +02:00
commit 662a1f7383
11 changed files with 310 additions and 97 deletions

View file

@ -89,6 +89,17 @@ target_sources(Monostep PRIVATE
target_include_directories(Monostep PRIVATE Source) target_include_directories(Monostep PRIVATE Source)
# Regenerate BuildStamp.h on every build so the UI's build date is always fresh.
set(MONOSTEP_BUILD_STAMP_HEADER "${CMAKE_CURRENT_BINARY_DIR}/generated/BuildStamp.h")
add_custom_target(MonostepBuildStamp ALL
COMMAND ${CMAKE_COMMAND}
-DMONOSTEP_BUILD_STAMP_HEADER="${MONOSTEP_BUILD_STAMP_HEADER}"
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/WriteBuildStamp.cmake"
COMMENT "Writing build stamp"
)
add_dependencies(Monostep MonostepBuildStamp)
target_include_directories(Monostep PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated")
target_compile_definitions(Monostep PRIVATE target_compile_definitions(Monostep PRIVATE
JUCE_WEB_BROWSER=0 JUCE_WEB_BROWSER=0
JUCE_VST3_CAN_REPLACE_VST2=0 JUCE_VST3_CAN_REPLACE_VST2=0

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <JuceHeader.h> #include <JuceHeader.h>
#include "BuildStamp.h"
namespace monostep namespace monostep
{ {
@ -19,6 +20,9 @@ inline juce::String gitInfo()
inline juce::String buildDateString() inline juce::String buildDateString()
{ {
#if defined (MONOSTEP_BUILD_DATE)
return juce::String (MONOSTEP_BUILD_DATE);
#else
static const char* months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", static const char* months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
@ -36,6 +40,7 @@ inline juce::String buildDateString()
const int year = d.substring (7).getIntValue(); const int year = d.substring (7).getIntValue();
return juce::String::formatted ("%04d-%02d-%02d__%s", year, month, day, __TIME__); return juce::String::formatted ("%04d-%02d-%02d__%s", year, month, day, __TIME__);
#endif
} }
inline juce::String footerText() inline juce::String footerText()

View file

@ -504,6 +504,14 @@ StepPanel::StepPanel (MonostepAudioProcessor& processorRef)
processor.randomizeSeqSettings(); processor.randomizeSeqSettings();
if (randomizeToggles[RandomFx].getToggleState()) if (randomizeToggles[RandomFx].getToggleState())
processor.randomizeFx(); processor.randomizeFx();
if (randomizeToggles[RandomAccent].getToggleState())
processor.randomizeAccent();
if (randomizeToggles[RandomSlide].getToggleState())
processor.randomizeSlide();
if (randomizeToggles[RandomFine].getToggleState())
processor.randomizeFine();
if (randomizeToggles[RandomRate].getToggleState())
processor.randomizeRate();
}; };
addAndMakeVisible (randomizeButton); addAndMakeVisible (randomizeButton);
@ -513,7 +521,8 @@ StepPanel::StepPanel (MonostepAudioProcessor& processorRef)
randomizeCaption.setColour (juce::Label::textColourId, colours::textDim); randomizeCaption.setColour (juce::Label::textColourId, colours::textDim);
addAndMakeVisible (randomizeCaption); addAndMakeVisible (randomizeCaption);
static const char* groupNames[] = { "Pattern", "Osc", "Filter", "Env", "Length", "FX" }; static const char* groupNames[] = { "Pattern", "Osc", "Filter", "Env", "Length", "FX",
"Accent", "Slide", "Fine", "Rate" };
for (int i = 0; i < RandomGroupCount; ++i) for (int i = 0; i < RandomGroupCount; ++i)
{ {
@ -590,41 +599,41 @@ void StepPanel::resized()
const int w = getWidth(); const int w = getWidth();
titleLabel.setBounds (0, 4, w, 16); titleLabel.setBounds (0, 4, w, 16);
noteLabel.setBounds (0, 22, w, 28); noteLabel.setBounds (0, 22, w, 26);
gateLabel.setBounds (0, 50, w, 14); gateLabel.setBounds (0, 50, w, 14);
const int knobX = (w - 74) / 2; const int knobX = (w - 74) / 2;
const int knobY = 68; const int knobY = 66;
fineKnob.setBounds (knobX, knobY, 74, 94); fineKnob.setBounds (knobX, knobY, 74, 84);
const int btnY = knobY + 102; const int btnY = knobY + 84 + 6;
prevButton.setBounds (knobX - 32, btnY, 24, 24); prevButton.setBounds (knobX - 32, btnY, 24, 24);
nextButton.setBounds (knobX + 74 + 8, btnY, 24, 24); nextButton.setBounds (knobX + 74 + 8, btnY, 24, 24);
const int shiftW = 22; const int shiftW = 22;
shiftLeftButton.setBounds (10, btnY + 50, shiftW, 26); shiftLeftButton.setBounds (10, btnY + 40, shiftW, 24);
clearButton.setBounds (10 + shiftW + 4, btnY + 50, w - 20 - shiftW * 2 - 8, 26); clearButton.setBounds (10 + shiftW + 4, btnY + 40, w - 20 - shiftW * 2 - 8, 24);
shiftRightButton.setBounds (10 + w - 20 - shiftW, btnY + 50, shiftW, 26); shiftRightButton.setBounds (10 + w - 20 - shiftW, btnY + 40, shiftW, 24);
toolsCaption.setBounds (10, btnY + 86, w - 20, 14); toolsCaption.setBounds (10, btnY + 70, w - 20, 12);
presetCombo.setBounds (10, btnY + 102, w - 20, 26); presetCombo.setBounds (10, btnY + 84, w - 20, 24);
randomizeButton.setBounds (10, btnY + 134, w - 20, 26); randomizeButton.setBounds (10, btnY + 112, w - 20, 24);
randomizeCaption.setBounds (10, btnY + 166, w - 20, 12); randomizeCaption.setBounds (10, btnY + 140, w - 20, 12);
const int colW = (w - 20 - 4) / 2; const int colW = (w - 20 - 4) / 2;
const int rowH = 20; const int rowH = 18;
for (int i = 0; i < RandomGroupCount; ++i) for (int i = 0; i < RandomGroupCount; ++i)
{ {
const int col = i / 3; const int col = i % 2;
const int row = i % 3; const int row = i / 2;
randomizeToggles[i].setBounds (10 + col * (colW + 4), btnY + 180 + row * (rowH + 2), colW, rowH); randomizeToggles[i].setBounds (10 + col * (colW + 4), btnY + 154 + row * (rowH + 2), colW, rowH);
} }
const int retrigW = 100; const int retrigW = 100;
retrigToggle.setBounds (10, btnY + 252, retrigW, 26); retrigToggle.setBounds (10, btnY + 254, retrigW, 24);
resetFineButton.setBounds (10 + retrigW + 4, btnY + 252, w - 20 - retrigW - 4, 26); resetFineButton.setBounds (10 + retrigW + 4, btnY + 254, w - 20 - retrigW - 4, 24);
} }
//============================================================================== //==============================================================================
@ -659,10 +668,12 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess
driveKnob ("Drive"), driveKnob ("Drive"),
ringKnob ("Ring"), ringKnob ("Ring"),
delayTimeKnob ("Time"), delayTimeKnob ("Time"),
delayStrengthKnob ("Strength"), delayFeedbackKnob ("Feedback"),
delayDryWetKnob ("Dry/Wet"),
reverbRoomKnob ("Room"), reverbRoomKnob ("Room"),
reverbStrengthKnob ("Strength"), reverbStrengthKnob ("Level"),
reverbDiffusionKnob ("Diff"), reverbDiffusionKnob ("Diff"),
reverbDryWetKnob ("Dry/Wet"),
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" }),
@ -770,10 +781,12 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess
const bool sync = processorRef.getAPVTS().getRawParameterValue ("delaySync")->load() >= 0.5f; const bool sync = processorRef.getAPVTS().getRawParameterValue ("delaySync")->load() >= 0.5f;
return monostep::FxProcessor::delayTimeLabel (v, sync, 120.0); return monostep::FxProcessor::delayTimeLabel (v, sync, 120.0);
}); });
addKnob (delayStrengthKnob, "delayStrength", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; }); addKnob (delayFeedbackKnob, "delayStrength", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
addKnob (delayDryWetKnob, "delayDryWet", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
addKnob (reverbRoomKnob, "reverbRoom", [] (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 (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 (reverbDiffusionKnob, "reverbDiffusion", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
addKnob (reverbDryWetKnob, "reverbDryWet", [] (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.setButtonText ("Sync");
@ -948,10 +961,10 @@ void MonostepAudioProcessorEditor::paintContent (juce::Graphics& g)
drawFrame (glideKnob, accentKnob, 6.0f); drawFrame (glideKnob, accentKnob, 6.0f);
drawPill (glideKnob, accentKnob, "NOTE"); drawPill (glideKnob, accentKnob, "NOTE");
drawFrame (driveKnob, reverbDiffusionKnob, 6.0f); drawFrame (driveKnob, reverbDryWetKnob, 6.0f);
drawPill (driveKnob, ringKnob, "FX"); drawPill (driveKnob, ringKnob, "FX");
drawPill (delayTimeKnob, delaySyncToggle, "DELAY"); drawPill (delayTimeKnob, delaySyncToggle, "DELAY");
drawPill (reverbRoomKnob, reverbDiffusionKnob, "REVERB"); drawPill (reverbRoomKnob, reverbDryWetKnob, "REVERB");
drawFrame (filterTypeBox, resKnob, 6.0f, 6.0f); drawFrame (filterTypeBox, resKnob, 6.0f, 6.0f);
drawPill (filterTypeBox, resKnob, "FILTER", 6.0f); drawPill (filterTypeBox, resKnob, "FILTER", 6.0f);
@ -1027,7 +1040,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 + 12 * step + 104; // osc col + FX (8 widgets + subgroup gaps) const int blockW = oscColW + 15 * step + 152; // osc col + FX (9 widgets + sync toggle + 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
@ -1053,9 +1066,9 @@ void MonostepAudioProcessorEditor::resized()
const int fxX = oscX + 5 * step + 48; const int fxX = oscX + 5 * step + 48;
placeRow (row1Y, fxX, { &driveKnob, &ringKnob }); placeRow (row1Y, fxX, { &driveKnob, &ringKnob });
placeRow (row1Y, fxX + 2 * step + 24, { &delayTimeKnob, &delayStrengthKnob }); placeRow (row1Y, fxX + 2 * step + 24, { &delayTimeKnob, &delayFeedbackKnob, &delayDryWetKnob });
placeRow (row1Y, fxX + 5 * step + 48, { &reverbRoomKnob, &reverbStrengthKnob, &reverbDiffusionKnob }); placeRow (row1Y, fxX + 6 * step + 46, { &reverbRoomKnob, &reverbStrengthKnob, &reverbDiffusionKnob, &reverbDryWetKnob });
delaySyncToggle.setBounds (fxX + 4 * step + 24, row1Y + (knobH - 24) / 2, knobW, 24); delaySyncToggle.setBounds (fxX + 5 * 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.

View file

@ -153,6 +153,10 @@ private:
RandomEnv, RandomEnv,
RandomSeq, RandomSeq,
RandomFx, RandomFx,
RandomAccent,
RandomSlide,
RandomFine,
RandomRate,
RandomGroupCount RandomGroupCount
}; };
@ -194,7 +198,7 @@ private:
ContentComponent content; ContentComponent content;
static constexpr int baseWidth = 1280; static constexpr int baseWidth = 1360;
static constexpr int baseHeight = 780; static constexpr int baseHeight = 780;
float zoomFactor = 1.0f; float zoomFactor = 1.0f;
@ -230,10 +234,12 @@ private:
Knob driveKnob; Knob driveKnob;
Knob ringKnob; Knob ringKnob;
Knob delayTimeKnob; Knob delayTimeKnob;
Knob delayStrengthKnob; Knob delayFeedbackKnob;
Knob delayDryWetKnob;
Knob reverbRoomKnob; Knob reverbRoomKnob;
Knob reverbStrengthKnob; Knob reverbStrengthKnob;
Knob reverbDiffusionKnob; Knob reverbDiffusionKnob;
Knob reverbDryWetKnob;
juce::ToggleButton delaySyncToggle; juce::ToggleButton delaySyncToggle;
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> delaySyncAttachment; std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> delaySyncAttachment;
Knob accentKnob; Knob accentKnob;

View file

@ -14,8 +14,8 @@ static const char* paramId (int index)
"drive", "ringmod", "accent", "fine1", "drive", "ringmod", "accent", "fine1",
"filterAttack", "filterDecay", "filterSustain", "filterRelease", "filterEnvAmt", "filterAttack", "filterDecay", "filterSustain", "filterRelease", "filterEnvAmt",
"seqRetrig", "seqRetrig",
"delayTime", "delayStrength", "delaySync", "delayTime", "delayStrength", "delaySync", "delayDryWet",
"reverbRoom", "reverbStrength", "reverbDiffusion" "reverbRoom", "reverbStrength", "reverbDiffusion", "reverbDryWet"
}; };
return ids[index]; return ids[index];
} }
@ -28,8 +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, pDelayTime, pDelayStrength, pDelaySync, pDelayDryWet,
pReverbRoom, pReverbStrength, pReverbDiffusion, pReverbRoom, pReverbStrength, pReverbDiffusion, pReverbDryWet,
numParams numParams
}; };
@ -72,9 +72,11 @@ MonostepAudioProcessor::MonostepAudioProcessor()
delayTimeParam = apvts.getRawParameterValue (paramId (pDelayTime)); delayTimeParam = apvts.getRawParameterValue (paramId (pDelayTime));
delayStrengthParam = apvts.getRawParameterValue (paramId (pDelayStrength)); delayStrengthParam = apvts.getRawParameterValue (paramId (pDelayStrength));
delaySyncParam = apvts.getRawParameterValue (paramId (pDelaySync)); delaySyncParam = apvts.getRawParameterValue (paramId (pDelaySync));
delayDryWetParam = apvts.getRawParameterValue (paramId (pDelayDryWet));
reverbRoomParam = apvts.getRawParameterValue (paramId (pReverbRoom)); reverbRoomParam = apvts.getRawParameterValue (paramId (pReverbRoom));
reverbStrengthParam = apvts.getRawParameterValue (paramId (pReverbStrength)); reverbStrengthParam = apvts.getRawParameterValue (paramId (pReverbStrength));
reverbDiffusionParam = apvts.getRawParameterValue (paramId (pReverbDiffusion)); reverbDiffusionParam = apvts.getRawParameterValue (paramId (pReverbDiffusion));
reverbDryWetParam = apvts.getRawParameterValue (paramId (pReverbDryWet));
setDefaultPattern(); setDefaultPattern();
} }
@ -186,20 +188,26 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonostepAudioProcessor::crea
"Delay Time", juce::NormalisableRange<float> (0.0f, 1.0f), 0.35f)); "Delay Time", juce::NormalisableRange<float> (0.0f, 1.0f), 0.35f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDelayStrength), layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDelayStrength),
"Delay Strength", juce::NormalisableRange<float> (0.0f, 1.0f), 0.0f)); "Delay Feedback", juce::NormalisableRange<float> (0.0f, 1.0f), 0.5f));
layout.add (std::make_unique<juce::AudioParameterBool> (paramId (pDelaySync), layout.add (std::make_unique<juce::AudioParameterBool> (paramId (pDelaySync),
"Delay Sync", true)); "Delay Sync", true));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDelayDryWet),
"Delay Dry/Wet", juce::NormalisableRange<float> (0.0f, 1.0f), 0.25f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pReverbRoom), layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pReverbRoom),
"Reverb Roomsize", juce::NormalisableRange<float> (0.0f, 1.0f), 0.5f)); "Reverb Roomsize", juce::NormalisableRange<float> (0.0f, 1.0f), 0.5f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pReverbStrength), layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pReverbStrength),
"Reverb Strength", juce::NormalisableRange<float> (0.0f, 1.0f), 0.0f)); "Reverb Strength", juce::NormalisableRange<float> (0.0f, 1.0f), 1.0f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pReverbDiffusion), layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pReverbDiffusion),
"Reverb Diffusion", juce::NormalisableRange<float> (0.0f, 1.0f), 0.6f)); "Reverb Diffusion", juce::NormalisableRange<float> (0.0f, 1.0f), 0.6f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pReverbDryWet),
"Reverb Dry/Wet", juce::NormalisableRange<float> (0.0f, 1.0f), 0.25f));
return layout; return layout;
} }
@ -283,31 +291,39 @@ void MonostepAudioProcessor::applyPreset (int index)
float glide; float gateLen; float swing; int rate; int root; int octave; float glide; float gateLen; float swing; int rate; int root; int octave;
float drive; float ring; float accent; float drive; float ring; float accent;
float fAttack; float fDecay; float fSustain; float fRelease; float fAmt; float fAttack; float fDecay; float fSustain; float fRelease; float fAmt;
float delayTime = 0.35f; // only audible when delayWet > 0
float delayFeedback = 0.45f;
bool delaySync = true;
float delayWet = 0.0f;
float reverbRoom = 0.5f; // only audible when reverbWet > 0
float reverbLevel = 0.6f;
float reverbDiff = 0.6f;
float reverbWet = 0.0f;
}; };
static const Preset presets[] = static const Preset presets[] =
{ {
// --- gritty / glidy ----------------------------------------------------- // --- gritty / glidy -----------------------------------------------------
{ 0, 1, 0, 15.0f, 0.60f, 4200.0f, 0.15f, 0.02f, 0.25f, 0.70f, 0.35f, 0.25f, 0.80f, 0.10f, 2, 0, 3, 0.55f, 0.00f, 0.5f, 0.02f, 0.30f, 0.60f, 0.25f, 1.5f }, // Wet Tape { 0, 1, 0, 15.0f, 0.60f, 4200.0f, 0.15f, 0.02f, 0.25f, 0.70f, 0.35f, 0.25f, 0.80f, 0.10f, 2, 0, 3, 0.55f, 0.00f, 0.5f, 0.02f, 0.30f, 0.60f, 0.25f, 1.5f, 0.21f, 0.55f, true, 0.50f, 0.60f, 0.60f, 0.50f, 0.15f }, // Wet Tape
{ 2, 0, -12, -8.0f, 0.45f, 1100.0f, 0.65f, 0.003f, 0.40f, 0.10f, 0.08f, 0.12f, 0.90f, 0.00f, 3, 0, 3, 0.85f, 0.00f, 0.4f, 0.005f, 0.35f, 0.10f, 0.10f, 3.0f }, // Acid Wash { 2, 0, -12, -8.0f, 0.45f, 1100.0f, 0.65f, 0.003f, 0.40f, 0.10f, 0.08f, 0.12f, 0.90f, 0.00f, 3, 0, 3, 0.85f, 0.00f, 0.4f, 0.005f, 0.35f, 0.10f, 0.10f, 3.0f }, // Acid Wash
{ 3, 3, 0, 5.0f, 0.50f, 380.0f, 0.30f, 0.005f, 0.10f, 0.40f, 0.10f, 0.20f, 0.95f, 0.05f, 3, 0, 3, 0.60f, 0.00f, 0.3f, 0.005f, 0.15f, 0.30f, 0.15f, 0.5f }, // Tin Can { 3, 3, 0, 5.0f, 0.50f, 380.0f, 0.30f, 0.005f, 0.10f, 0.40f, 0.10f, 0.20f, 0.95f, 0.05f, 3, 0, 3, 0.60f, 0.00f, 0.3f, 0.005f, 0.15f, 0.30f, 0.15f, 0.5f }, // Tin Can
{ 0, 3, 7, 25.0f, 0.35f, 2500.0f, 0.20f, 0.04f, 0.20f, 0.30f, 0.60f, 0.45f, 0.85f, 0.25f, 2, 0, 3, 0.40f, 0.00f, 0.5f, 0.03f, 0.25f, 0.50f, 0.40f, 1.0f }, // Bent Toy { 0, 3, 7, 25.0f, 0.35f, 2500.0f, 0.20f, 0.04f, 0.20f, 0.30f, 0.60f, 0.45f, 0.85f, 0.25f, 2, 0, 3, 0.40f, 0.00f, 0.5f, 0.03f, 0.25f, 0.50f, 0.40f, 1.0f }, // Bent Toy
{ 2, 3, 0, 0.0f, 0.55f, 650.0f, 0.45f, 0.002f, 0.15f, 0.20f, 0.05f, 0.10f, 0.90f, 0.10f, 3, 0, 3, 0.95f, 0.60f, 0.3f, 0.005f, 0.20f, 0.40f, 0.15f, 1.5f }, // Broken Radio { 2, 3, 0, 0.0f, 0.55f, 650.0f, 0.45f, 0.002f, 0.15f, 0.20f, 0.05f, 0.10f, 0.90f, 0.10f, 3, 0, 3, 0.95f, 0.60f, 0.3f, 0.005f, 0.20f, 0.40f, 0.15f, 1.5f }, // Broken Radio
{ 1, 2, 0, 20.0f, 0.50f, 6000.0f, 0.10f, 0.003f, 0.15f, 0.80f, 0.20f, 0.10f, 0.90f, 0.15f, 3, 0, 3, 0.20f, 0.00f, 0.6f, 0.01f, 0.30f, 0.70f, 0.30f, 1.0f }, // Neon Arcade { 1, 2, 0, 20.0f, 0.50f, 6000.0f, 0.10f, 0.003f, 0.15f, 0.80f, 0.20f, 0.10f, 0.90f, 0.15f, 3, 0, 3, 0.20f, 0.00f, 0.6f, 0.01f, 0.30f, 0.70f, 0.30f, 1.0f, 0.50f, 0.45f, true, 0.30f, 0.50f, 0.50f, 0.60f, 0.30f }, // Neon Arcade
{ 0, 1, -12, -35.0f, 0.55f, 1800.0f, 0.40f, 0.10f, 0.35f, 0.20f, 0.80f, 0.50f, 0.70f, 0.20f, 2, 0, 3, 0.00f, 0.00f, 0.5f, 0.01f, 0.40f, 0.50f, 0.20f, -1.5f }, // Glitch Garden { 0, 1, -12, -35.0f, 0.55f, 1800.0f, 0.40f, 0.10f, 0.35f, 0.20f, 0.80f, 0.50f, 0.70f, 0.20f, 2, 0, 3, 0.00f, 0.00f, 0.5f, 0.01f, 0.40f, 0.50f, 0.20f, -1.5f, 0.36f, 0.70f, true, 0.50f, 0.50f, 0.60f, 0.60f, 0.00f }, // Glitch Garden
{ 2, 2, 5, 10.0f, 0.40f, 3200.0f, 0.35f, 0.002f, 0.20f, 0.60f, 0.12f, 0.15f, 0.95f, 0.40f, 3, 0, 3, 0.70f, 0.20f, 0.5f, 0.01f, 0.25f, 0.60f, 0.20f, 2.0f }, // Robo Funk { 2, 2, 5, 10.0f, 0.40f, 3200.0f, 0.35f, 0.002f, 0.20f, 0.60f, 0.12f, 0.15f, 0.95f, 0.40f, 3, 0, 3, 0.70f, 0.20f, 0.5f, 0.01f, 0.25f, 0.60f, 0.20f, 2.0f, 0.36f, 0.50f, true, 0.40f, 0.50f, 0.60f, 0.60f, 0.00f }, // Robo Funk
{ 1, 1, -7, -15.0f, 0.50f, 2600.0f, 0.20f, 0.03f, 0.30f, 0.35f, 0.60f, 0.60f, 0.80f, 0.05f, 2, 0, 3, 0.10f, 0.00f, 0.4f, 0.02f, 0.30f, 0.50f, 0.30f, -0.5f }, // Lo-Fi Noir { 1, 1, -7, -15.0f, 0.50f, 2600.0f, 0.20f, 0.03f, 0.30f, 0.35f, 0.60f, 0.60f, 0.80f, 0.05f, 2, 0, 3, 0.10f, 0.00f, 0.4f, 0.02f, 0.30f, 0.50f, 0.30f, -0.5f, 0.21f, 0.40f, true, 0.20f, 0.60f, 0.50f, 0.50f, 0.25f }, // Lo-Fi Noir
{ 0, 2, 0, 8.0f, 0.60f, 7000.0f, 0.08f, 0.01f, 0.20f, 0.90f, 0.25f, 0.30f, 0.75f, 0.00f, 3, 0, 3, 0.35f, 0.35f, 0.6f, 0.01f, 0.30f, 0.20f, 0.30f, 3.5f }, // Spectral Sweep { 0, 2, 0, 8.0f, 0.60f, 7000.0f, 0.08f, 0.01f, 0.20f, 0.90f, 0.25f, 0.30f, 0.75f, 0.00f, 3, 0, 3, 0.35f, 0.35f, 0.6f, 0.01f, 0.30f, 0.20f, 0.30f, 3.5f }, // Spectral Sweep
{ 3, 0, -12, 12.0f, 0.45f, 5000.0f, 0.25f, 0.02f, 0.50f, 0.70f, 0.30f, 0.40f, 0.60f, 0.20f, 2, 0, 3, 0.30f, 0.15f, 0.5f, 0.02f, 0.40f, 0.80f, 0.30f, 0.5f }, // Plasma Organ { 3, 0, -12, 12.0f, 0.45f, 5000.0f, 0.25f, 0.02f, 0.50f, 0.70f, 0.30f, 0.40f, 0.60f, 0.20f, 2, 0, 3, 0.30f, 0.15f, 0.5f, 0.02f, 0.40f, 0.80f, 0.30f, 0.5f, 0.35f, 0.45f, true, 0.00f, 0.70f, 0.80f, 0.70f, 0.40f }, // Plasma Organ
{ 0, 0, 0, 0.0f, 0.50f, 900.0f, 0.50f, 0.05f, 0.30f, 0.50f, 0.50f, 0.35f, 0.85f, 0.30f, 2, 0, 3, 0.25f, 0.55f, 0.3f, 0.05f, 0.30f, 0.50f, 0.40f, -2.0f }, // Underwater FM { 0, 0, 0, 0.0f, 0.50f, 900.0f, 0.50f, 0.05f, 0.30f, 0.50f, 0.50f, 0.35f, 0.85f, 0.30f, 2, 0, 3, 0.25f, 0.55f, 0.3f, 0.05f, 0.30f, 0.50f, 0.40f, -2.0f, 0.64f, 0.50f, true, 0.35f, 0.90f, 0.70f, 0.90f, 0.35f }, // Underwater FM
// --- clean / no glide / no drive --------------------------------------- // --- clean / no glide / no drive ---------------------------------------
{ 0, 0, 0, 0.0f, 0.50f, 7000.0f, 0.10f, 0.002f, 0.25f, 0.20f, 0.30f, 0.00f, 0.35f, 0.00f, 3, 0, 4, 0.00f, 0.00f, 0.6f, 0.005f, 0.20f, 0.30f, 0.40f, 0.3f }, // Glass Bells { 0, 0, 0, 0.0f, 0.50f, 7000.0f, 0.10f, 0.002f, 0.25f, 0.20f, 0.30f, 0.00f, 0.35f, 0.00f, 3, 0, 4, 0.00f, 0.00f, 0.6f, 0.005f, 0.20f, 0.30f, 0.40f, 0.3f }, // Glass Bells
{ 1, 1, 0, 12.0f, 0.60f, 1200.0f, 0.25f, 0.30f, 1.20f, 0.80f, 1.50f, 0.20f, 1.00f, 0.10f, 2, 0, 2, 0.00f, 0.00f, 0.2f, 0.50f, 1.00f, 0.70f, 1.20f, 1.0f }, // Warm Pad { 1, 1, 0, 12.0f, 0.60f, 1200.0f, 0.25f, 0.30f, 1.20f, 0.80f, 1.50f, 0.20f, 1.00f, 0.10f, 2, 0, 2, 0.00f, 0.00f, 0.2f, 0.50f, 1.00f, 0.70f, 1.20f, 1.0f, 0.35f, 0.45f, true, 0.00f, 0.80f, 0.90f, 0.80f, 0.45f }, // Warm Pad
{ 2, 2, 0, 5.0f, 0.50f, 2500.0f, 0.15f, 0.002f, 0.20f, 0.30f, 0.12f, 0.00f, 0.50f, 0.30f, 3, 0, 3, 0.00f, 0.00f, 0.5f, 0.01f, 0.25f, 0.40f, 0.20f, 1.5f }, // Pulse Groove { 2, 2, 0, 5.0f, 0.50f, 2500.0f, 0.15f, 0.002f, 0.20f, 0.30f, 0.12f, 0.00f, 0.50f, 0.30f, 3, 0, 3, 0.00f, 0.00f, 0.5f, 0.01f, 0.25f, 0.40f, 0.20f, 1.5f }, // Pulse Groove
{ 0, 3, 12, 0.0f, 0.40f, 1500.0f, 0.60f, 0.001f, 0.35f, 0.15f, 0.20f, 0.00f, 0.45f, 0.00f, 3, 0, 4, 0.00f, 0.35f, 0.7f, 0.002f, 0.30f, 0.20f, 0.25f, 2.5f }, // Steel Pluck { 0, 3, 12, 0.0f, 0.40f, 1500.0f, 0.60f, 0.001f, 0.35f, 0.15f, 0.20f, 0.00f, 0.45f, 0.00f, 3, 0, 4, 0.00f, 0.35f, 0.7f, 0.002f, 0.30f, 0.20f, 0.25f, 2.5f }, // Steel Pluck
{ 0, 0, 0, -5.0f, 0.40f, 500.0f, 0.20f, 0.01f, 0.40f, 0.90f, 0.30f, 0.00f, 0.90f, 0.00f, 2, 0, 1, 0.00f, 0.00f, 0.3f, 0.01f, 0.30f, 0.60f, 0.30f, -1.0f }, // Sub Rumble { 0, 0, 0, -5.0f, 0.40f, 500.0f, 0.20f, 0.01f, 0.40f, 0.90f, 0.30f, 0.00f, 0.90f, 0.00f, 2, 0, 1, 0.00f, 0.00f, 0.3f, 0.01f, 0.30f, 0.60f, 0.30f, -1.0f }, // Sub Rumble
{ 1, 0, 0, 8.0f, 0.55f, 3000.0f, 0.20f, 0.005f, 0.30f, 0.60f, 0.25f, 0.30f, 0.70f, 0.15f, 2, 0, 3, 0.25f, 0.00f, 0.5f, 0.01f, 0.30f, 0.50f, 0.30f, 1.0f }, // Tape Bounce { 1, 0, 0, 8.0f, 0.55f, 3000.0f, 0.20f, 0.005f, 0.30f, 0.60f, 0.25f, 0.30f, 0.70f, 0.15f, 2, 0, 3, 0.25f, 0.00f, 0.5f, 0.01f, 0.30f, 0.50f, 0.30f, 1.0f, 0.64f, 0.60f, true, 0.45f, 0.50f, 0.50f, 0.50f, 0.20f }, // Tape Bounce
}; };
const auto& pr = presets[juce::jlimit (0, getNumPresets() - 1, index)]; const auto& pr = presets[juce::jlimit (0, getNumPresets() - 1, index)];
@ -337,6 +353,15 @@ void MonostepAudioProcessor::applyPreset (int index)
applyParamValue ("filterSustain", pr.fSustain); applyParamValue ("filterSustain", pr.fSustain);
applyParamValue ("filterRelease", pr.fRelease); applyParamValue ("filterRelease", pr.fRelease);
applyParamValue ("filterEnvAmt", pr.fAmt); applyParamValue ("filterEnvAmt", pr.fAmt);
applyParamValue ("delayTime", pr.delayTime);
applyParamValue ("delayStrength", pr.delayFeedback);
applyParamValue ("delaySync", pr.delaySync ? 1.0f : 0.0f);
applyParamValue ("delayDryWet", pr.delayWet);
applyParamValue ("reverbRoom", pr.reverbRoom);
applyParamValue ("reverbStrength", pr.reverbLevel);
applyParamValue ("reverbDiffusion", pr.reverbDiff);
applyParamValue ("reverbDryWet", pr.reverbWet);
} }
void MonostepAudioProcessor::randomizePattern() void MonostepAudioProcessor::randomizePattern()
@ -420,11 +445,66 @@ void MonostepAudioProcessor::randomizeFx()
applyParamValue ("drive", random.nextFloat()); applyParamValue ("drive", random.nextFloat());
applyParamValue ("ringmod", random.nextFloat()); applyParamValue ("ringmod", random.nextFloat());
applyParamValue ("delayTime", random.nextFloat()); applyParamValue ("delayTime", random.nextFloat());
applyParamValue ("delayStrength", random.nextFloat() * 0.8f); applyParamValue ("delayStrength", random.nextFloat() * 0.9f);
applyParamValue ("delaySync", random.nextFloat() < 0.8f ? 1.0f : 0.0f); applyParamValue ("delaySync", random.nextFloat() < 0.8f ? 1.0f : 0.0f);
applyParamValue ("delayDryWet", random.nextFloat() * 0.8f);
applyParamValue ("reverbRoom", 0.2f + random.nextFloat() * 0.7f); applyParamValue ("reverbRoom", 0.2f + random.nextFloat() * 0.7f);
applyParamValue ("reverbStrength", random.nextFloat() * 0.7f); applyParamValue ("reverbStrength", random.nextFloat());
applyParamValue ("reverbDiffusion", random.nextFloat()); applyParamValue ("reverbDiffusion", random.nextFloat());
applyParamValue ("reverbDryWet", random.nextFloat() * 0.8f);
}
void MonostepAudioProcessor::randomizeAccent()
{
const int length = juce::roundToInt (seqLenParam->load());
for (int i = 0; i < length; ++i)
{
auto& s = sequencer.step (i);
if (! s.gate)
continue;
s.accent = random.nextFloat() < 0.3f;
}
if (onPatternChanged)
onPatternChanged();
}
void MonostepAudioProcessor::randomizeSlide()
{
const int length = juce::roundToInt (seqLenParam->load());
for (int i = 0; i < length; ++i)
{
auto& s = sequencer.step (i);
if (! s.gate)
continue;
s.slide = random.nextFloat() < 0.25f;
}
if (onPatternChanged)
onPatternChanged();
}
void MonostepAudioProcessor::randomizeFine()
{
const int length = juce::roundToInt (seqLenParam->load());
for (int i = 0; i < length; ++i)
{
auto& s = sequencer.step (i);
if (! s.gate)
continue;
s.cents = random.nextFloat() * 100.0f - 50.0f;
}
if (onPatternChanged)
onPatternChanged();
}
void MonostepAudioProcessor::randomizeRate()
{
applyParamValue ("seqRate", (float) random.nextInt (5));
} }
void MonostepAudioProcessor::randomizeParams() void MonostepAudioProcessor::randomizeParams()
@ -686,8 +766,9 @@ void MonostepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
} }
// Accent tracks the current step (not just gate changes), so it holds for // Accent tracks the current step (not just gate changes), so it holds for
// the whole step even when the note repeats without retriggering. // the whole step even when the note repeats without retriggering. Kept
voice.setAccentLevel (seqAccent ? 1.0f + accentParam->load() * 4.0f : 1.0f); // gentle: a modest volume lift plus a bit of brightness/timing (below).
voice.setAccentLevel (seqAccent ? 1.0f + accentParam->load() * 0.4f : 1.0f);
if (wantGate != lastGateApplied) if (wantGate != lastGateApplied)
{ {
@ -714,13 +795,15 @@ void MonostepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
buffer.addFrom (ch, 0, scratch, 0, 0, numSamples); buffer.addFrom (ch, 0, scratch, 0, 0, numSamples);
monostep::FxProcessor::Params fp; monostep::FxProcessor::Params fp;
fp.delayTime = delayTimeParam->load(); fp.delayTime = delayTimeParam->load();
fp.delayMix = delayStrengthParam->load(); fp.delayFeedback = delayStrengthParam->load();
fp.delaySync = delaySyncParam->load() >= 0.5f; fp.delaySync = delaySyncParam->load() >= 0.5f;
fp.reverbRoom = reverbRoomParam->load(); fp.delayMix = delayDryWetParam->load();
fp.reverbMix = reverbStrengthParam->load(); fp.reverbRoom = reverbRoomParam->load();
fp.reverbDiff = reverbDiffusionParam->load(); fp.reverbLevel = reverbStrengthParam->load();
fp.bpm = bpm; fp.reverbMix = reverbDryWetParam->load();
fp.reverbDiff = reverbDiffusionParam->load();
fp.bpm = bpm;
fx.setParams (fp); fx.setParams (fp);
fx.process (buffer, numSamples); fx.process (buffer, numSamples);
} }

View file

@ -59,6 +59,10 @@ public:
void randomizeEnv(); void randomizeEnv();
void randomizeSeqSettings(); void randomizeSeqSettings();
void randomizeFx(); void randomizeFx();
void randomizeAccent();
void randomizeSlide();
void randomizeFine();
void randomizeRate();
juce::String getStepNoteName (int idx) const; juce::String getStepNoteName (int idx) const;
@ -110,9 +114,11 @@ private:
std::atomic<float>* delayTimeParam = nullptr; std::atomic<float>* delayTimeParam = nullptr;
std::atomic<float>* delayStrengthParam = nullptr; std::atomic<float>* delayStrengthParam = nullptr;
std::atomic<float>* delaySyncParam = nullptr; std::atomic<float>* delaySyncParam = nullptr;
std::atomic<float>* delayDryWetParam = nullptr;
std::atomic<float>* reverbRoomParam = nullptr; std::atomic<float>* reverbRoomParam = nullptr;
std::atomic<float>* reverbStrengthParam = nullptr; std::atomic<float>* reverbStrengthParam = nullptr;
std::atomic<float>* reverbDiffusionParam = nullptr; std::atomic<float>* reverbDiffusionParam = nullptr;
std::atomic<float>* reverbDryWetParam = nullptr;
monostep::StepSequencer sequencer; monostep::StepSequencer sequencer;
monostep::SynthVoice voice; monostep::SynthVoice voice;

View file

@ -7,17 +7,19 @@ namespace monostep
{ {
// Freeverb-style comb / allpass sizes, slightly offset per channel so the // Freeverb-style comb / allpass sizes, slightly offset per channel so the
// left and right reverb tails decorrelate. // left and right reverb tails decorrelate. Halved from the classic freeverb
// set so the early reflections land ~13 ms after the note instead of ~25 ms:
// a 100%-wet reverb keeps its tail but speaks much sooner.
static constexpr int combSizes[2][4] = static constexpr int combSizes[2][4] =
{ {
{ 1116, 1188, 1277, 1356 }, { 558, 594, 639, 678 },
{ 1126, 1198, 1287, 1366 } { 563, 599, 644, 683 }
}; };
static constexpr int allpassSizes[2][2] = static constexpr int allpassSizes[2][2] =
{ {
{ 225, 556 }, { 113, 278 },
{ 235, 566 } { 118, 283 }
}; };
float FxProcessor::delayTimeSeconds (float knob, bool sync, double bpm) float FxProcessor::delayTimeSeconds (float knob, bool sync, double bpm)
@ -36,7 +38,7 @@ juce::String FxProcessor::delayTimeLabel (float knob, bool sync, double bpm)
{ {
if (sync) if (sync)
{ {
static const char* names[] = { "1/16", "1/8", "1/8D", "1/4", "1/4D", "1/2", "1/2D", "1W" }; static const char* names[] = { "1/16", "1/8", "3/16", "1/4", "3/8", "1/2", "3/4", "1 bar" };
const int idx = juce::roundToInt (juce::jlimit (0.0f, 1.0f, knob) * 7.0f); const int idx = juce::roundToInt (juce::jlimit (0.0f, 1.0f, knob) * 7.0f);
return names[juce::jlimit (0, 7, idx)]; return names[juce::jlimit (0, 7, idx)];
} }
@ -112,13 +114,15 @@ void FxProcessor::reset()
void FxProcessor::setParams (const Params& p) void FxProcessor::setParams (const Params& p)
{ {
params.delayTime = juce::jlimit (0.0f, 1.0f, p.delayTime); params.delayTime = juce::jlimit (0.0f, 1.0f, p.delayTime);
params.delayMix = juce::jlimit (0.0f, 1.0f, p.delayMix); params.delayFeedback = juce::jlimit (0.0f, 0.9f, p.delayFeedback);
params.delaySync = p.delaySync; params.delaySync = p.delaySync;
params.reverbRoom = juce::jlimit (0.0f, 1.0f, p.reverbRoom); params.delayMix = juce::jlimit (0.0f, 1.0f, p.delayMix);
params.reverbMix = juce::jlimit (0.0f, 1.0f, p.reverbMix); params.reverbRoom = juce::jlimit (0.0f, 1.0f, p.reverbRoom);
params.reverbDiff = juce::jlimit (0.0f, 1.0f, p.reverbDiff); params.reverbLevel = juce::jlimit (0.0f, 1.0f, p.reverbLevel);
params.bpm = juce::jmax (20.0, p.bpm); 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); currentDelaySeconds = delayTimeSeconds (params.delayTime, params.delaySync, params.bpm);
} }
@ -128,8 +132,23 @@ void FxProcessor::process (juce::AudioBuffer<float>& buffer, int numSamples)
if (numSamples <= 0 || buffer.getNumChannels() == 0) if (numSamples <= 0 || buffer.getNumChannels() == 0)
return; return;
// The reverb must feed off the dry (pre-delay) signal, not the delay's
// output, so snapshot the incoming buffer before the delay runs.
const bool reverbActive = params.reverbMix > 0.0001f && params.reverbLevel > 0.0001f;
if (reverbActive)
{
if (dryScratch.getNumSamples() < numSamples)
dryScratch.setSize (buffer.getNumChannels(), numSamples, false, false, true);
const int n = juce::jmin (buffer.getNumChannels(), dryScratch.getNumChannels());
for (int c = 0; c < n; ++c)
dryScratch.copyFrom (c, 0, buffer, c, 0, numSamples);
}
processDelay (buffer, numSamples); processDelay (buffer, numSamples);
processReverb (buffer, numSamples);
if (reverbActive)
processReverb (dryScratch, buffer, numSamples);
} }
void FxProcessor::processDelay (juce::AudioBuffer<float>& buffer, int numSamples) void FxProcessor::processDelay (juce::AudioBuffer<float>& buffer, int numSamples)
@ -166,7 +185,7 @@ void FxProcessor::processDelay (juce::AudioBuffer<float>& buffer, int numSamples
// Feedback with a little high-frequency damping. // Feedback with a little high-frequency damping.
d.feedbackLp += 0.35f * (delayed - d.feedbackLp); d.feedbackLp += 0.35f * (delayed - d.feedbackLp);
d.memory[d.writeIndex] = x + 0.42f * d.feedbackLp; d.memory[d.writeIndex] = x + params.delayFeedback * d.feedbackLp;
out[i] = x + delayed * mix; out[i] = x + delayed * mix;
@ -176,24 +195,38 @@ void FxProcessor::processDelay (juce::AudioBuffer<float>& buffer, int numSamples
} }
} }
void FxProcessor::processReverb (juce::AudioBuffer<float>& buffer, int numSamples) void FxProcessor::processReverb (const juce::AudioBuffer<float>& dryIn,
juce::AudioBuffer<float>& buffer, int numSamples)
{ {
if (params.reverbMix <= 0.0001f) if (params.reverbMix <= 0.0001f || params.reverbLevel <= 0.0001f)
return; return;
const int n = juce::jmin (numChannels, buffer.getNumChannels()); const int n = juce::jmin (numChannels, juce::jmin (dryIn.getNumChannels(), buffer.getNumChannels()));
const float combGain = 0.70f + 0.28f * params.reverbRoom; // 0.70 .. 0.98 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 allpassGain = 0.75f * params.reverbDiff; // 0 .. 0.75
// Room normalisation: the comb network's DC gain is ~4 / (1 - combGain),
// so scaling by (1 - combGain) keeps the wet level bounded as the room
// (decay length) grows instead of blowing up near oscillation. The 2.5
// constant is calibrated (standalone replication of this network): at
// room 0.5 it puts the note-off ring ~ +6 dB above the dry and the tail
// ~ -6 dB one second in, so the reverb is clearly audible even at modest
// dry/wet settings. The wet is intentionally NOT clipped here: any
// soft-clip knee far below the signal level pins the wet at a constant
// flat value (the early tail stops audibly decaying, which reads as a
// "reverb slowly kicking in" plateau) and makes 100% wet sound quiet
// next to a hot dry signal.
const float wetScale = 2.5f * (1.0f - combGain);
const float mix = params.reverbMix; const float mix = params.reverbMix;
for (int ch = 0; ch < n; ++ch) for (int ch = 0; ch < n; ++ch)
{ {
auto& r = reverbs[ch]; auto& r = reverbs[ch];
const float* in = dryIn.getReadPointer (ch);
auto* out = buffer.getWritePointer (ch); auto* out = buffer.getWritePointer (ch);
for (int i = 0; i < numSamples; ++i) for (int i = 0; i < numSamples; ++i)
{ {
const float x = out[i]; const float x = in[i];
float combSum = 0.0f; float combSum = 0.0f;
for (int c = 0; c < 4; ++c) for (int c = 0; c < 4; ++c)
@ -208,7 +241,7 @@ void FxProcessor::processReverb (juce::AudioBuffer<float>& buffer, int numSample
pos = 0; pos = 0;
} }
float wet = combSum * 0.012f; float wet = combSum * wetScale * params.reverbLevel;
for (int a = 0; a < 2; ++a) for (int a = 0; a < 2; ++a)
{ {
@ -223,7 +256,7 @@ void FxProcessor::processReverb (juce::AudioBuffer<float>& buffer, int numSample
pos = 0; pos = 0;
} }
out[i] = x * (1.0f - mix) + wet * mix; out[i] += wet * mix;
} }
} }
} }

View file

@ -17,13 +17,15 @@ public:
struct Params struct Params
{ {
float delayTime = 0.35f; // normalized knob 0..1 float delayTime = 0.35f; // normalized knob 0..1
float delayMix = 0.0f; // 0..1 dry/wet float delayFeedback = 0.45f; // 0..0.9 feedback (repeat density)
bool delaySync = true; // tempo-synced vs. free milliseconds bool delaySync = true; // tempo-synced vs. free milliseconds
float reverbRoom = 0.5f; // 0..1 float delayMix = 0.0f; // 0..1 dry/wet
float reverbMix = 0.0f; // 0..1 dry/wet float reverbRoom = 0.5f; // 0..1
float reverbDiff = 0.6f; // 0..1 allpass diffusion float reverbLevel = 1.0f; // 0..1 wet send gain
double bpm = 120.0; 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 setParams (const Params& p);
@ -52,7 +54,7 @@ private:
}; };
void processDelay (juce::AudioBuffer<float>& buffer, int numSamples); void processDelay (juce::AudioBuffer<float>& buffer, int numSamples);
void processReverb (juce::AudioBuffer<float>& buffer, int numSamples); void processReverb (const juce::AudioBuffer<float>& dryIn, juce::AudioBuffer<float>& buffer, int numSamples);
double sampleRate = 44100.0; double sampleRate = 44100.0;
int numChannels = 2; int numChannels = 2;
@ -64,6 +66,10 @@ private:
Params params; Params params;
float currentDelaySeconds = 0.0f; float currentDelaySeconds = 0.0f;
// Pre-delay snapshot: the reverb is fed the dry signal (before the delay),
// so delay and reverb run in parallel instead of in series.
juce::AudioBuffer<float> dryScratch;
std::vector<DelayChannel> delays; std::vector<DelayChannel> delays;
std::vector<ReverbChannel> reverbs; std::vector<ReverbChannel> reverbs;
}; };

View file

@ -137,6 +137,10 @@ private:
fenvStage = Stage::attack; fenvStage = Stage::attack;
} }
// RC-style (exponential) filter envelope: the cutoff glides smoothly instead
// of stepping linearly, so fast filter sweeps sound analog rather than
// zippered. Each stage still reaches ~95% within the labelled time, so the
// knob values keep their musical meaning.
void updateFilterEnvelope() void updateFilterEnvelope()
{ {
const float sr = sampleRate; const float sr = sampleRate;
@ -144,23 +148,37 @@ private:
switch (fenvStage) switch (fenvStage)
{ {
case Stage::attack: case Stage::attack:
fenv += 1.0f / (params.fAttack * sr); {
if (fenv >= 1.0f) { fenv = 1.0f; fenvStage = Stage::decay; } // Accented notes attack the filter ever so slightly faster.
const float acc = accentSmooth - 1.0f;
const float t = juce::jmax (0.0005f, params.fAttack / (1.0f + acc * 0.5f));
const float a = 1.0f - std::exp (-3.0f / (t * sr));
fenv += (1.0f - fenv) * a;
if (fenv >= 0.999f) { fenv = 1.0f; fenvStage = Stage::decay; }
break; break;
}
case Stage::decay: case Stage::decay:
fenv -= (1.0f - params.fSustain) / (params.fDecay * sr); {
if (fenv <= params.fSustain) { fenv = params.fSustain; fenvStage = Stage::sustain; } const float t = juce::jmax (0.0005f, params.fDecay);
const float a = 1.0f - std::exp (-3.0f / (t * sr));
fenv += (params.fSustain - fenv) * a;
if (std::fabs (fenv - params.fSustain) < 0.001f) { fenv = params.fSustain; fenvStage = Stage::sustain; }
break; break;
}
case Stage::sustain: case Stage::sustain:
fenv = params.fSustain; fenv = params.fSustain;
break; break;
case Stage::release: case Stage::release:
fenv -= 1.0f / (params.fRelease * sr); {
if (fenv <= 0.0f) { fenv = 0.0f; fenvStage = Stage::idle; } const float t = juce::jmax (0.0005f, params.fRelease);
const float a = 1.0f - std::exp (-3.0f / (t * sr));
fenv -= fenv * a;
if (fenv <= 0.001f) { fenv = 0.0f; fenvStage = Stage::idle; }
break; break;
}
case Stage::idle: case Stage::idle:
break; break;
@ -278,15 +296,18 @@ private:
float renderSample() float renderSample()
{ {
updateEnvelope();
updateFilterEnvelope();
updateGlide();
// Accent gains fast attack / slow release, so the boost fades out smoothly // Accent gains fast attack / slow release, so the boost fades out smoothly
// at the tail of an accented note instead of cutting off hard-edged. // at the tail of an accented note instead of cutting off hard-edged.
accentSmooth += (accentLevel - accentSmooth) accentSmooth += (accentLevel - accentSmooth)
* (accentLevel > accentSmooth ? accentAttackCoef : accentReleaseCoef); * (accentLevel > accentSmooth ? accentAttackCoef : accentReleaseCoef);
// 0 for unaccented steps, rising with the accent knob on accented ones.
const float accBoost = accentSmooth - 1.0f;
updateEnvelope();
updateFilterEnvelope();
updateGlide();
const float incA = (currentFreq * params.detuneRatioA) / sampleRate; const float incA = (currentFreq * params.detuneRatioA) / sampleRate;
const float incB = (currentFreq * params.detuneRatio) / sampleRate; const float incB = (currentFreq * params.detuneRatio) / sampleRate;
@ -302,14 +323,17 @@ private:
out = (1.0f - params.ringMod) * out + params.ringMod * (a * b); out = (1.0f - params.ringMod) * out + params.ringMod * (a * b);
const float preFilter = out; const float preFilter = out;
// Accented steps open the filter a tiny bit, so they sparkle without
// relying on a big volume jump.
const float envCutoff = juce::jlimit (20.0f, 20000.0f, const float envCutoff = juce::jlimit (20.0f, 20000.0f,
params.cutoff * std::pow (2.0f, params.fAmount * fenv)); params.cutoff * std::pow (2.0f, params.fAmount * fenv + accBoost * 1.5f));
out = processFilter (out, envCutoff); out = processFilter (out, envCutoff);
// Accented steps also get a touch of the unfiltered signal so they cut // Accented steps also get a touch of the unfiltered signal so they cut
// through the mix (presence), not just a volume bump. // through the mix (presence), not just a volume bump.
if (accentSmooth > 1.0f) if (accBoost > 0.0f)
out += (accentSmooth - 1.0f) * 0.2f * preFilter; out += accBoost * 0.2f * preFilter;
if (params.drive > 0.0f) if (params.drive > 0.0f)
{ {

View file

@ -0,0 +1,22 @@
# Generates BuildStamp.h with the current date/time. Run as a build-time
# custom command so the stamp is refreshed on every build (the TU that includes
# it is recompiled because the header mtime changes each time).
if(NOT DEFINED MONOSTEP_BUILD_STAMP_HEADER)
message(FATAL_ERROR "MONOSTEP_BUILD_STAMP_HEADER must be set")
endif()
string(TIMESTAMP STAMP "%Y-%m-%d__%H:%M:%S")
get_filename_component(MONOSTEP_BUILD_STAMP_DIR "${MONOSTEP_BUILD_STAMP_HEADER}" DIRECTORY)
file(MAKE_DIRECTORY "${MONOSTEP_BUILD_STAMP_DIR}")
file(WRITE "${MONOSTEP_BUILD_STAMP_HEADER}"
"// Generated at build time - do not edit.
#ifndef MONOSTEP_BUILD_STAMP_H_INCLUDED
#define MONOSTEP_BUILD_STAMP_H_INCLUDED
#define MONOSTEP_BUILD_DATE \"${STAMP}\"
#endif
")

View file

@ -305,6 +305,8 @@ static int testProcessorRendering()
*processor.getAPVTS().getRawParameterValue ("release") = 0.01f; *processor.getAPVTS().getRawParameterValue ("release") = 0.01f;
*processor.getAPVTS().getRawParameterValue ("delayStrength") = 0.0f; *processor.getAPVTS().getRawParameterValue ("delayStrength") = 0.0f;
*processor.getAPVTS().getRawParameterValue ("reverbStrength") = 0.0f; *processor.getAPVTS().getRawParameterValue ("reverbStrength") = 0.0f;
*processor.getAPVTS().getRawParameterValue ("delayDryWet") = 0.0f;
*processor.getAPVTS().getRawParameterValue ("reverbDryWet") = 0.0f;
const int releaseBlock = numBlocks / 2; const int releaseBlock = numBlocks / 2;
@ -360,9 +362,11 @@ static int testProcessorRendering()
*processor.getAPVTS().getRawParameterValue ("delayStrength") = 0.8f; *processor.getAPVTS().getRawParameterValue ("delayStrength") = 0.8f;
*processor.getAPVTS().getRawParameterValue ("delayTime") = 0.4f; *processor.getAPVTS().getRawParameterValue ("delayTime") = 0.4f;
*processor.getAPVTS().getRawParameterValue ("delaySync") = 1.0f; *processor.getAPVTS().getRawParameterValue ("delaySync") = 1.0f;
*processor.getAPVTS().getRawParameterValue ("delayDryWet") = 0.5f;
*processor.getAPVTS().getRawParameterValue ("reverbStrength") = 0.8f; *processor.getAPVTS().getRawParameterValue ("reverbStrength") = 0.8f;
*processor.getAPVTS().getRawParameterValue ("reverbRoom") = 0.9f; *processor.getAPVTS().getRawParameterValue ("reverbRoom") = 0.9f;
*processor.getAPVTS().getRawParameterValue ("reverbDiffusion") = 1.0f; *processor.getAPVTS().getRawParameterValue ("reverbDiffusion") = 1.0f;
*processor.getAPVTS().getRawParameterValue ("reverbDryWet") = 0.5f;
const double wetTail = renderFxTail(); const double wetTail = renderFxTail();