Polish GUI, add pattern retrig option, and rework voice DSP

This commit is contained in:
Armin 2026-08-06 15:51:56 +02:00
commit 4732240cc7
7 changed files with 546 additions and 129 deletions

View file

@ -9,9 +9,11 @@ static const char* paramId (int index)
static const char* ids[] =
{
"osc1Wave", "osc2Wave", "osc1Coarse", "osc2Coarse", "detune", "mix",
"cutoff", "res", "attack", "decay", "sustain", "release", "glide", "master",
"cutoff", "res", "filterType", "attack", "decay", "sustain", "release", "glide", "master",
"seqLen", "seqRate", "seqRoot", "seqOctave", "seqSwing", "seqGateLen",
"drive", "ringmod", "accent", "fine1"
"drive", "ringmod", "accent", "fine1",
"filterAttack", "filterDecay", "filterSustain", "filterRelease", "filterEnvAmt",
"seqRetrig"
};
return ids[index];
}
@ -19,9 +21,11 @@ static const char* paramId (int index)
enum ParamIndex
{
pOsc1Wave, pOsc2Wave, pOsc1Coarse, pOsc2Coarse, pDetune, pMix,
pCutoff, pRes, pAttack, pDecay, pSustain, pRelease, pGlide, pMaster,
pCutoff, pRes, pFilterType, pAttack, pDecay, pSustain, pRelease, pGlide, pMaster,
pSeqLen, pSeqRate, pSeqRoot, pSeqOctave, pSeqSwing, pSeqGateLen,
pDrive, pRingMod, pAccent, pFine1,
pFilterAttack, pFilterDecay, pFilterSustain, pFilterRelease, pFilterEnvAmt,
pSeqRetrig,
numParams
};
@ -39,10 +43,16 @@ MonostepAudioProcessor::MonostepAudioProcessor()
osc1PhaseParam = apvts.getRawParameterValue (paramId (pFine1));
cutoffParam = apvts.getRawParameterValue (paramId (pCutoff));
resParam = apvts.getRawParameterValue (paramId (pRes));
filterTypeParam = apvts.getRawParameterValue (paramId (pFilterType));
attackParam = apvts.getRawParameterValue (paramId (pAttack));
decayParam = apvts.getRawParameterValue (paramId (pDecay));
sustainParam = apvts.getRawParameterValue (paramId (pSustain));
releaseParam = apvts.getRawParameterValue (paramId (pRelease));
filterAttackParam = apvts.getRawParameterValue (paramId (pFilterAttack));
filterDecayParam = apvts.getRawParameterValue (paramId (pFilterDecay));
filterSustainParam = apvts.getRawParameterValue (paramId (pFilterSustain));
filterReleaseParam = apvts.getRawParameterValue (paramId (pFilterRelease));
filterEnvAmtParam = apvts.getRawParameterValue (paramId (pFilterEnvAmt));
glideParam = apvts.getRawParameterValue (paramId (pGlide));
masterParam = apvts.getRawParameterValue (paramId (pMaster));
seqLenParam = apvts.getRawParameterValue (paramId (pSeqLen));
@ -54,6 +64,7 @@ MonostepAudioProcessor::MonostepAudioProcessor()
driveParam = apvts.getRawParameterValue (paramId (pDrive));
ringModParam = apvts.getRawParameterValue (paramId (pRingMod));
accentParam = apvts.getRawParameterValue (paramId (pAccent));
seqRetrigParam = apvts.getRawParameterValue (paramId (pSeqRetrig));
setDefaultPattern();
}
@ -84,7 +95,13 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonostepAudioProcessor::crea
juce::AudioParameterFloatAttributes().withLabel ("Hz")));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pRes),
"Filter Res", juce::NormalisableRange<float> (0.0f, 1.0f), 0.15f));
"Filter Res", juce::NormalisableRange<float> (0.0f, 1.0f), 0.15f));
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pFilterType),
"Filter Type", 0,
(int) monostep::SynthVoice::FilterType::numFilterTypes - 1,
0));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pAttack),
"Attack", juce::NormalisableRange<float> (0.001f, 2.0f, 0.001f, 0.3f), 0.005f,
@ -101,6 +118,25 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonostepAudioProcessor::crea
"Release", juce::NormalisableRange<float> (0.01f, 4.0f, 0.001f, 0.3f), 0.4f,
juce::AudioParameterFloatAttributes().withLabel ("s")));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pFilterAttack),
"Filter Env Attack", juce::NormalisableRange<float> (0.001f, 2.0f, 0.001f, 0.3f), 0.005f,
juce::AudioParameterFloatAttributes().withLabel ("s")));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pFilterDecay),
"Filter Env Decay", juce::NormalisableRange<float> (0.001f, 3.0f, 0.001f, 0.3f), 0.3f,
juce::AudioParameterFloatAttributes().withLabel ("s")));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pFilterSustain),
"Filter Env Sustain", juce::NormalisableRange<float> (0.0f, 1.0f), 0.7f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pFilterRelease),
"Filter Env Release", juce::NormalisableRange<float> (0.01f, 4.0f, 0.001f, 0.3f), 0.4f,
juce::AudioParameterFloatAttributes().withLabel ("s")));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pFilterEnvAmt),
"Filter Env Amount", juce::NormalisableRange<float> (-4.0f, 4.0f), 1.0f,
juce::AudioParameterFloatAttributes().withLabel ("oct")));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pGlide),
"Glide", juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f, 0.5f), 0.12f,
juce::AudioParameterFloatAttributes().withLabel ("s")));
@ -124,6 +160,9 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonostepAudioProcessor::crea
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pSeqGateLen),
"Gate Length", juce::NormalisableRange<float> (0.05f, 1.0f), 0.8f));
layout.add (std::make_unique<juce::AudioParameterBool> (paramId (pSeqRetrig),
"Restart Pattern on Note", false));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDrive),
"Distortion Drive", juce::NormalisableRange<float> (0.0f, 1.0f), 0.0f));
@ -215,31 +254,32 @@ void MonostepAudioProcessor::applyPreset (int index)
float cutoff; float res; float attack; float decay; float sustain; float release;
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;
};
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 }, // 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 }, // 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 }, // 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 }, // 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 }, // 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 }, // 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 }, // 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 }, // 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 }, // 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 }, // 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 }, // 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 }, // Underwater FM
{ 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
{ 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
{ 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
// --- 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 }, // 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 }, // 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 }, // 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 }, // 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 }, // 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 }, // Tape Bounce
{ 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
{ 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
};
const auto& pr = presets[juce::jlimit (0, getNumPresets() - 1, index)];
@ -264,6 +304,11 @@ void MonostepAudioProcessor::applyPreset (int index)
applyParamValue ("drive", pr.drive);
applyParamValue ("ringmod", pr.ring);
applyParamValue ("accent", pr.accent);
applyParamValue ("filterAttack", pr.fAttack);
applyParamValue ("filterDecay", pr.fDecay);
applyParamValue ("filterSustain", pr.fSustain);
applyParamValue ("filterRelease", pr.fRelease);
applyParamValue ("filterEnvAmt", pr.fAmt);
}
void MonostepAudioProcessor::randomizePattern()
@ -318,6 +363,11 @@ void MonostepAudioProcessor::randomizeFilter()
{
applyParamValue ("cutoff", 200.0f * std::pow (2.0f, random.nextFloat() * 5.0f));
applyParamValue ("res", random.nextFloat() * 0.8f);
applyParamValue ("filterEnvAmt", -3.0f + random.nextFloat() * 6.0f);
applyParamValue ("filterAttack", 0.001f * std::pow (500.0f, random.nextFloat()));
applyParamValue ("filterDecay", 0.05f * std::pow (30.0f, random.nextFloat()));
applyParamValue ("filterSustain", random.nextFloat());
applyParamValue ("filterRelease", 0.05f * std::pow (40.0f, random.nextFloat()));
}
void MonostepAudioProcessor::randomizeEnv()
@ -375,15 +425,22 @@ void MonostepAudioProcessor::updateVoiceParams()
monostep::SynthVoice::Params p;
p.waveA = (monostep::Waveform) (int) osc1WaveParam->load();
p.waveB = (monostep::Waveform) (int) osc2WaveParam->load();
p.detuneRatio = std::pow (2.0f, osc2CoarseParam->load() / 12.0f)
* std::pow (2.0f, detuneParam->load() / 1200.0f);
p.detuneRatioA = std::pow (2.0f, detuneParam->load() / 1200.0f); // OSC1: follows the knob
p.detuneRatio = std::pow (2.0f, osc2CoarseParam->load() / 12.0f) // OSC2: coarse
* std::pow (2.0f, -detuneParam->load() / 1200.0f); // OSC2: detune inverted
p.mix = mixParam->load();
p.cutoff = cutoffParam->load();
p.cutoff = cutoffParam->load();
p.resonance = resParam->load();
p.filterType = (monostep::SynthVoice::FilterType) (int) filterTypeParam->load();
p.attack = attackParam->load();
p.decay = decayParam->load();
p.sustain = sustainParam->load();
p.release = releaseParam->load();
p.fAttack = filterAttackParam->load();
p.fDecay = filterDecayParam->load();
p.fSustain = filterSustainParam->load();
p.fRelease = filterReleaseParam->load();
p.fAmount = filterEnvAmtParam->load();
p.glide = glideParam->load();
p.master = masterParam->load();
p.drive = driveParam->load();
@ -419,6 +476,7 @@ void MonostepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
const float cyclePpq = (float) seqLen * stepLen;
// --- MIDI notes (gate/trigger for the sequencer) -------------------------
bool noteOnSeen = false;
for (const auto metadata : midiMessages)
{
const auto msg = metadata.getMessage();
@ -426,6 +484,7 @@ void MonostepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
if (msg.isNoteOn())
{
midiHeld = true;
noteOnSeen = true;
lastMidiNote = msg.getNoteNumber();
}
else if (msg.isNoteOff())
@ -477,6 +536,16 @@ void MonostepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
lastGateApplied = false;
}
// Optional retrigger: a fresh note-on restarts the pattern from
// step 1. Only honoured in free-run mode; while the host transport
// is running, the pattern follows the host grid instead.
if (noteOnSeen && seqRetrigParam->load() >= 0.5f)
{
freePpq = 0.0;
lastStep = -1;
lastGateApplied = false;
}
freePpq += ppqPerSample * numSamples;
}
}
@ -742,6 +811,7 @@ void MonostepAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
juce::ValueTree state = apvts.copyState();
juce::ValueTree seq = state.getOrCreateChildWithName ("SEQ", nullptr);
storeSequence (seq);
state.setProperty ("zoom", zoomIndex, nullptr);
std::unique_ptr<juce::XmlElement> xml (state.createXml());
copyXmlToBinary (*xml, destData);
@ -760,6 +830,8 @@ void MonostepAudioProcessor::setStateInformation (const void* data, int sizeInBy
if (auto seq = state.getChildWithName ("SEQ"); seq.isValid())
loadSequence (seq);
zoomIndex = juce::jlimit (0, 4, (int) state.getProperty ("zoom", 0));
if (onPatternChanged)
onPatternChanged();
}