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)
# 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
JUCE_WEB_BROWSER=0
JUCE_VST3_CAN_REPLACE_VST2=0

View file

@ -1,6 +1,7 @@
#pragma once
#include <JuceHeader.h>
#include "BuildStamp.h"
namespace monostep
{
@ -19,6 +20,9 @@ inline juce::String gitInfo()
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",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
@ -36,6 +40,7 @@ inline juce::String buildDateString()
const int year = d.substring (7).getIntValue();
return juce::String::formatted ("%04d-%02d-%02d__%s", year, month, day, __TIME__);
#endif
}
inline juce::String footerText()

View file

@ -504,6 +504,14 @@ StepPanel::StepPanel (MonostepAudioProcessor& processorRef)
processor.randomizeSeqSettings();
if (randomizeToggles[RandomFx].getToggleState())
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);
@ -513,7 +521,8 @@ StepPanel::StepPanel (MonostepAudioProcessor& processorRef)
randomizeCaption.setColour (juce::Label::textColourId, colours::textDim);
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)
{
@ -590,41 +599,41 @@ void StepPanel::resized()
const int w = getWidth();
titleLabel.setBounds (0, 4, w, 16);
noteLabel.setBounds (0, 22, w, 28);
noteLabel.setBounds (0, 22, w, 26);
gateLabel.setBounds (0, 50, w, 14);
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);
nextButton.setBounds (knobX + 74 + 8, btnY, 24, 24);
const int shiftW = 22;
shiftLeftButton.setBounds (10, btnY + 50, shiftW, 26);
clearButton.setBounds (10 + shiftW + 4, btnY + 50, w - 20 - shiftW * 2 - 8, 26);
shiftRightButton.setBounds (10 + w - 20 - shiftW, btnY + 50, shiftW, 26);
shiftLeftButton.setBounds (10, btnY + 40, shiftW, 24);
clearButton.setBounds (10 + shiftW + 4, btnY + 40, w - 20 - shiftW * 2 - 8, 24);
shiftRightButton.setBounds (10 + w - 20 - shiftW, btnY + 40, shiftW, 24);
toolsCaption.setBounds (10, btnY + 86, w - 20, 14);
presetCombo.setBounds (10, btnY + 102, w - 20, 26);
randomizeButton.setBounds (10, btnY + 134, w - 20, 26);
randomizeCaption.setBounds (10, btnY + 166, w - 20, 12);
toolsCaption.setBounds (10, btnY + 70, w - 20, 12);
presetCombo.setBounds (10, btnY + 84, w - 20, 24);
randomizeButton.setBounds (10, btnY + 112, w - 20, 24);
randomizeCaption.setBounds (10, btnY + 140, w - 20, 12);
const int colW = (w - 20 - 4) / 2;
const int rowH = 20;
const int rowH = 18;
for (int i = 0; i < RandomGroupCount; ++i)
{
const int col = i / 3;
const int row = i % 3;
randomizeToggles[i].setBounds (10 + col * (colW + 4), btnY + 180 + row * (rowH + 2), colW, rowH);
const int col = i % 2;
const int row = i / 2;
randomizeToggles[i].setBounds (10 + col * (colW + 4), btnY + 154 + row * (rowH + 2), colW, rowH);
}
const int retrigW = 100;
retrigToggle.setBounds (10, btnY + 252, retrigW, 26);
resetFineButton.setBounds (10 + retrigW + 4, btnY + 252, w - 20 - retrigW - 4, 26);
retrigToggle.setBounds (10, btnY + 254, retrigW, 24);
resetFineButton.setBounds (10 + retrigW + 4, btnY + 254, w - 20 - retrigW - 4, 24);
}
//==============================================================================
@ -659,10 +668,12 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess
driveKnob ("Drive"),
ringKnob ("Ring"),
delayTimeKnob ("Time"),
delayStrengthKnob ("Strength"),
delayFeedbackKnob ("Feedback"),
delayDryWetKnob ("Dry/Wet"),
reverbRoomKnob ("Room"),
reverbStrengthKnob ("Strength"),
reverbStrengthKnob ("Level"),
reverbDiffusionKnob ("Diff"),
reverbDryWetKnob ("Dry/Wet"),
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" }),
@ -770,10 +781,12 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess
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 (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 (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 (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)) + "%"; });
delaySyncToggle.setButtonText ("Sync");
@ -948,10 +961,10 @@ void MonostepAudioProcessorEditor::paintContent (juce::Graphics& g)
drawFrame (glideKnob, accentKnob, 6.0f);
drawPill (glideKnob, accentKnob, "NOTE");
drawFrame (driveKnob, reverbDiffusionKnob, 6.0f);
drawFrame (driveKnob, reverbDryWetKnob, 6.0f);
drawPill (driveKnob, ringKnob, "FX");
drawPill (delayTimeKnob, delaySyncToggle, "DELAY");
drawPill (reverbRoomKnob, reverbDiffusionKnob, "REVERB");
drawPill (reverbRoomKnob, reverbDryWetKnob, "REVERB");
drawFrame (filterTypeBox, resKnob, 6.0f, 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
// 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 row1Y = bottomY + 24;
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;
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);
placeRow (row1Y, fxX + 2 * step + 24, { &delayTimeKnob, &delayFeedbackKnob, &delayDryWetKnob });
placeRow (row1Y, fxX + 6 * step + 46, { &reverbRoomKnob, &reverbStrengthKnob, &reverbDiffusionKnob, &reverbDryWetKnob });
delaySyncToggle.setBounds (fxX + 5 * step + 24, row1Y + (knobH - 24) / 2, knobW, 24);
// Row 2: Filter (dropdown + cutoff, res) [gap] Amp Envelope [gap] Filter Env.
// The three sections are centered as one block with equal frame gaps.

View file

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

View file

@ -14,8 +14,8 @@ static const char* paramId (int index)
"drive", "ringmod", "accent", "fine1",
"filterAttack", "filterDecay", "filterSustain", "filterRelease", "filterEnvAmt",
"seqRetrig",
"delayTime", "delayStrength", "delaySync",
"reverbRoom", "reverbStrength", "reverbDiffusion"
"delayTime", "delayStrength", "delaySync", "delayDryWet",
"reverbRoom", "reverbStrength", "reverbDiffusion", "reverbDryWet"
};
return ids[index];
}
@ -28,8 +28,8 @@ enum ParamIndex
pDrive, pRingMod, pAccent, pFine1,
pFilterAttack, pFilterDecay, pFilterSustain, pFilterRelease, pFilterEnvAmt,
pSeqRetrig,
pDelayTime, pDelayStrength, pDelaySync,
pReverbRoom, pReverbStrength, pReverbDiffusion,
pDelayTime, pDelayStrength, pDelaySync, pDelayDryWet,
pReverbRoom, pReverbStrength, pReverbDiffusion, pReverbDryWet,
numParams
};
@ -72,9 +72,11 @@ MonostepAudioProcessor::MonostepAudioProcessor()
delayTimeParam = apvts.getRawParameterValue (paramId (pDelayTime));
delayStrengthParam = apvts.getRawParameterValue (paramId (pDelayStrength));
delaySyncParam = apvts.getRawParameterValue (paramId (pDelaySync));
delayDryWetParam = apvts.getRawParameterValue (paramId (pDelayDryWet));
reverbRoomParam = apvts.getRawParameterValue (paramId (pReverbRoom));
reverbStrengthParam = apvts.getRawParameterValue (paramId (pReverbStrength));
reverbDiffusionParam = apvts.getRawParameterValue (paramId (pReverbDiffusion));
reverbDryWetParam = apvts.getRawParameterValue (paramId (pReverbDryWet));
setDefaultPattern();
}
@ -186,20 +188,26 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonostepAudioProcessor::crea
"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));
"Delay Feedback", juce::NormalisableRange<float> (0.0f, 1.0f), 0.5f));
layout.add (std::make_unique<juce::AudioParameterBool> (paramId (pDelaySync),
"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),
"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));
"Reverb Strength", juce::NormalisableRange<float> (0.0f, 1.0f), 1.0f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pReverbDiffusion),
"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;
}
@ -283,31 +291,39 @@ void MonostepAudioProcessor::applyPreset (int index)
float glide; float gateLen; float swing; int rate; int root; int octave;
float drive; float ring; float accent;
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[] =
{
// --- 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
{ 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
{ 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
{ 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
{ 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
{ 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, 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, 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, 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, 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
{ 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
{ 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
{ 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, 0.64f, 0.50f, true, 0.35f, 0.90f, 0.70f, 0.90f, 0.35f }, // Underwater FM
// --- 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
{ 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
{ 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
{ 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)];
@ -337,6 +353,15 @@ void MonostepAudioProcessor::applyPreset (int index)
applyParamValue ("filterSustain", pr.fSustain);
applyParamValue ("filterRelease", pr.fRelease);
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()
@ -420,11 +445,66 @@ void MonostepAudioProcessor::randomizeFx()
applyParamValue ("drive", random.nextFloat());
applyParamValue ("ringmod", 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 ("delayDryWet", random.nextFloat() * 0.8f);
applyParamValue ("reverbRoom", 0.2f + random.nextFloat() * 0.7f);
applyParamValue ("reverbStrength", random.nextFloat() * 0.7f);
applyParamValue ("reverbStrength", 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()
@ -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
// the whole step even when the note repeats without retriggering.
voice.setAccentLevel (seqAccent ? 1.0f + accentParam->load() * 4.0f : 1.0f);
// the whole step even when the note repeats without retriggering. Kept
// 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)
{
@ -715,10 +796,12 @@ void MonostepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
monostep::FxProcessor::Params fp;
fp.delayTime = delayTimeParam->load();
fp.delayMix = delayStrengthParam->load();
fp.delayFeedback = delayStrengthParam->load();
fp.delaySync = delaySyncParam->load() >= 0.5f;
fp.delayMix = delayDryWetParam->load();
fp.reverbRoom = reverbRoomParam->load();
fp.reverbMix = reverbStrengthParam->load();
fp.reverbLevel = reverbStrengthParam->load();
fp.reverbMix = reverbDryWetParam->load();
fp.reverbDiff = reverbDiffusionParam->load();
fp.bpm = bpm;
fx.setParams (fp);

View file

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

View file

@ -7,17 +7,19 @@ namespace monostep
{
// 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] =
{
{ 1116, 1188, 1277, 1356 },
{ 1126, 1198, 1287, 1366 }
{ 558, 594, 639, 678 },
{ 563, 599, 644, 683 }
};
static constexpr int allpassSizes[2][2] =
{
{ 225, 556 },
{ 235, 566 }
{ 113, 278 },
{ 118, 283 }
};
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)
{
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);
return names[juce::jlimit (0, 7, idx)];
}
@ -113,9 +115,11 @@ void FxProcessor::reset()
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.delayFeedback = juce::jlimit (0.0f, 0.9f, p.delayFeedback);
params.delaySync = p.delaySync;
params.delayMix = juce::jlimit (0.0f, 1.0f, p.delayMix);
params.reverbRoom = juce::jlimit (0.0f, 1.0f, p.reverbRoom);
params.reverbLevel = juce::jlimit (0.0f, 1.0f, p.reverbLevel);
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);
@ -128,8 +132,23 @@ void FxProcessor::process (juce::AudioBuffer<float>& buffer, int numSamples)
if (numSamples <= 0 || buffer.getNumChannels() == 0)
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);
processReverb (buffer, numSamples);
if (reverbActive)
processReverb (dryScratch, buffer, 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.
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;
@ -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;
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 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;
for (int ch = 0; ch < n; ++ch)
{
auto& r = reverbs[ch];
const float* in = dryIn.getReadPointer (ch);
auto* out = buffer.getWritePointer (ch);
for (int i = 0; i < numSamples; ++i)
{
const float x = out[i];
const float x = in[i];
float combSum = 0.0f;
for (int c = 0; c < 4; ++c)
@ -208,7 +241,7 @@ void FxProcessor::processReverb (juce::AudioBuffer<float>& buffer, int numSample
pos = 0;
}
float wet = combSum * 0.012f;
float wet = combSum * wetScale * params.reverbLevel;
for (int a = 0; a < 2; ++a)
{
@ -223,7 +256,7 @@ void FxProcessor::processReverb (juce::AudioBuffer<float>& buffer, int numSample
pos = 0;
}
out[i] = x * (1.0f - mix) + wet * mix;
out[i] += wet * mix;
}
}
}

View file

@ -18,9 +18,11 @@ public:
struct Params
{
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
float delayMix = 0.0f; // 0..1 dry/wet
float reverbRoom = 0.5f; // 0..1
float reverbLevel = 1.0f; // 0..1 wet send gain
float reverbMix = 0.0f; // 0..1 dry/wet
float reverbDiff = 0.6f; // 0..1 allpass diffusion
double bpm = 120.0;
@ -52,7 +54,7 @@ private:
};
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;
int numChannels = 2;
@ -64,6 +66,10 @@ private:
Params params;
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<ReverbChannel> reverbs;
};

View file

@ -137,6 +137,10 @@ private:
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()
{
const float sr = sampleRate;
@ -144,23 +148,37 @@ private:
switch (fenvStage)
{
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;
}
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;
}
case Stage::sustain:
fenv = params.fSustain;
break;
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;
}
case Stage::idle:
break;
@ -278,15 +296,18 @@ private:
float renderSample()
{
updateEnvelope();
updateFilterEnvelope();
updateGlide();
// 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.
accentSmooth += (accentLevel - accentSmooth)
* (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 incB = (currentFreq * params.detuneRatio) / sampleRate;
@ -302,14 +323,17 @@ private:
out = (1.0f - params.ringMod) * out + params.ringMod * (a * b);
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,
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);
// Accented steps also get a touch of the unfiltered signal so they cut
// through the mix (presence), not just a volume bump.
if (accentSmooth > 1.0f)
out += (accentSmooth - 1.0f) * 0.2f * preFilter;
if (accBoost > 0.0f)
out += accBoost * 0.2f * preFilter;
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 ("delayStrength") = 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;
@ -360,9 +362,11 @@ static int testProcessorRendering()
*processor.getAPVTS().getRawParameterValue ("delayStrength") = 0.8f;
*processor.getAPVTS().getRawParameterValue ("delayTime") = 0.4f;
*processor.getAPVTS().getRawParameterValue ("delaySync") = 1.0f;
*processor.getAPVTS().getRawParameterValue ("delayDryWet") = 0.5f;
*processor.getAPVTS().getRawParameterValue ("reverbStrength") = 0.8f;
*processor.getAPVTS().getRawParameterValue ("reverbRoom") = 0.9f;
*processor.getAPVTS().getRawParameterValue ("reverbDiffusion") = 1.0f;
*processor.getAPVTS().getRawParameterValue ("reverbDryWet") = 0.5f;
const double wetTail = renderFxTail();