mirror of
https://codeberg.org/armin/monostep.git
synced 2026-09-01 12:20:46 +02:00
Polish GUI, add pattern retrig option, and rework voice DSP
This commit is contained in:
parent
8ed279e84c
commit
4732240cc7
7 changed files with 546 additions and 129 deletions
|
|
@ -9,6 +9,14 @@ namespace monostep
|
|||
class SynthVoice
|
||||
{
|
||||
public:
|
||||
enum class FilterType
|
||||
{
|
||||
lp12 = 0, hp12, bp12, notch12,
|
||||
lp24, hp24, bp24, notch24,
|
||||
lp48, hp48, bp48, notch48, // 48 dB/octave = "2 x 24"
|
||||
numFilterTypes
|
||||
};
|
||||
|
||||
struct Params
|
||||
{
|
||||
Waveform waveA = Waveform::saw;
|
||||
|
|
@ -21,15 +29,22 @@ public:
|
|||
float osc2Phase = 0.0f;
|
||||
float cutoff = 7000.0f;
|
||||
float resonance = 0.15f;
|
||||
FilterType filterType = FilterType::lp12;
|
||||
float attack = 0.005f;
|
||||
float decay = 0.3f;
|
||||
float sustain = 0.7f;
|
||||
float release = 0.4f;
|
||||
float fAttack = 0.005f;
|
||||
float fDecay = 0.3f;
|
||||
float fSustain = 0.7f;
|
||||
float fRelease = 0.4f;
|
||||
float fAmount = 1.0f; // filter-env cutoff modulation in octaves
|
||||
float glide = 0.12f;
|
||||
float master = 0.9f;
|
||||
float drive = 0.0f;
|
||||
float ringMod = 0.0f;
|
||||
float detuneRatio = 1.0f;
|
||||
float detuneRatio = 1.0f; // OSC2 (inverted detune)
|
||||
float detuneRatioA = 1.0f; // OSC1 (direct detune)
|
||||
float mix = 0.5f;
|
||||
};
|
||||
|
||||
|
|
@ -42,7 +57,8 @@ public:
|
|||
void prepare (double sr)
|
||||
{
|
||||
sampleRate = (float) sr;
|
||||
accentSmoothCoef = 1.0f - std::exp (-1.0f / (0.003f * sampleRate));
|
||||
accentAttackCoef = 1.0f - std::exp (-1.0f / (0.003f * sampleRate));
|
||||
accentReleaseCoef = 1.0f - std::exp (-1.0f / (0.050f * sampleRate));
|
||||
reset();
|
||||
}
|
||||
|
||||
|
|
@ -51,9 +67,15 @@ public:
|
|||
phaseA = phaseB = 0.0f;
|
||||
currentFreq = targetFreq = 440.0f;
|
||||
smoothing = 0.0f;
|
||||
filterLow = filterBand = 0.0f;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
filterLow[i] = 0.0f;
|
||||
filterBand[i] = 0.0f;
|
||||
}
|
||||
env = 0.0f;
|
||||
envStage = Stage::idle;
|
||||
fenv = 0.0f;
|
||||
fenvStage = Stage::idle;
|
||||
gate = false;
|
||||
accentLevel = 1.0f;
|
||||
accentSmooth = 1.0f;
|
||||
|
|
@ -92,6 +114,9 @@ public:
|
|||
|
||||
if (envStage != Stage::idle && envStage != Stage::release)
|
||||
envStage = Stage::release;
|
||||
|
||||
if (fenvStage != Stage::idle && fenvStage != Stage::release)
|
||||
fenvStage = Stage::release;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,6 +134,37 @@ private:
|
|||
void retriggerEnvelope()
|
||||
{
|
||||
envStage = Stage::attack;
|
||||
fenvStage = Stage::attack;
|
||||
}
|
||||
|
||||
void updateFilterEnvelope()
|
||||
{
|
||||
const float sr = sampleRate;
|
||||
|
||||
switch (fenvStage)
|
||||
{
|
||||
case Stage::attack:
|
||||
fenv += 1.0f / (params.fAttack * sr);
|
||||
if (fenv >= 1.0f) { 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; }
|
||||
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; }
|
||||
break;
|
||||
|
||||
case Stage::idle:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void updateEnvelope()
|
||||
|
|
@ -155,27 +211,83 @@ private:
|
|||
}
|
||||
}
|
||||
|
||||
float processFilter (float input)
|
||||
float processFilter (float input, float cutoff)
|
||||
{
|
||||
const float f = 2.0f * std::sin (juce::MathConstants<float>::pi * params.cutoff / sampleRate);
|
||||
const float q = 1.0f / (1.0f + params.resonance * 9.0f);
|
||||
const int numStages = numFilterStages();
|
||||
const float f = 2.0f * std::sin (juce::MathConstants<float>::pi * cutoff / sampleRate);
|
||||
// Distribute resonance across the pole stages (each stage gets res/stages) so the
|
||||
// composite resonance matches the 12 dB case instead of stacking into self-oscillation
|
||||
// near Nyquist. The 12 dB case (one stage) is unchanged.
|
||||
const float q = 1.0f / (1.0f + params.resonance * 9.0f / (float) numStages);
|
||||
|
||||
filterLow += f * filterBand;
|
||||
const float high = input - filterLow - q * filterBand;
|
||||
filterBand += f * high;
|
||||
// Always cascade the LP response (low -> low): this keeps the integrator chain
|
||||
// unconditionally stable for every pole count. The requested response (LP/HP/BP/Notch)
|
||||
// is then tapped from the final stage, which preserves the 12/24/48 slope while
|
||||
// avoiding the ringing/unstability of cascading raw high/band signals.
|
||||
float sig = input;
|
||||
float low = 0.0f, high = 0.0f, band = 0.0f;
|
||||
|
||||
return filterLow;
|
||||
for (int s = 0; s < numStages; ++s)
|
||||
{
|
||||
low = filterLow[s] + f * filterBand[s]; // new low (old band)
|
||||
high = sig - low - q * filterBand[s]; // new high (new low, old band)
|
||||
band = filterBand[s] + f * high; // new band
|
||||
|
||||
// Safety clamp: prevents the resonant state from diverging into NaN when a
|
||||
// high pole count is driven at max resonance near Nyquist. Normal signals
|
||||
// (accents peak ~[1 + res*4]) never reach this bound.
|
||||
band = juce::jlimit (-16.0f, 16.0f, band);
|
||||
low = juce::jlimit (-16.0f, 16.0f, low);
|
||||
|
||||
filterLow[s] = low;
|
||||
filterBand[s] = band;
|
||||
|
||||
sig = low; // cascade LP response
|
||||
}
|
||||
|
||||
return filterTap (low, high, band); // tap from last stage
|
||||
}
|
||||
|
||||
int numFilterStages() const
|
||||
{
|
||||
switch (params.filterType)
|
||||
{
|
||||
case FilterType::lp12: case FilterType::hp12:
|
||||
case FilterType::bp12: case FilterType::notch12: return 1;
|
||||
case FilterType::lp24: case FilterType::hp24:
|
||||
case FilterType::bp24: case FilterType::notch24: return 2;
|
||||
case FilterType::lp48: case FilterType::hp48:
|
||||
case FilterType::bp48: case FilterType::notch48: return 4;
|
||||
default: return 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
float filterTap (float low, float high, float band) const
|
||||
{
|
||||
switch (params.filterType)
|
||||
{
|
||||
case FilterType::lp12: case FilterType::lp24: case FilterType::lp48: return low;
|
||||
case FilterType::hp12: case FilterType::hp24: case FilterType::hp48: return high;
|
||||
case FilterType::bp12: case FilterType::bp24: case FilterType::bp48: return band;
|
||||
case FilterType::notch12: case FilterType::notch24: case FilterType::notch48: return low + high;
|
||||
default: return low;
|
||||
}
|
||||
return low;
|
||||
}
|
||||
|
||||
float renderSample()
|
||||
{
|
||||
updateEnvelope();
|
||||
updateFilterEnvelope();
|
||||
updateGlide();
|
||||
|
||||
// Smooth the accent gain so it ramps in/out instead of clicking.
|
||||
accentSmooth += (accentLevel - accentSmooth) * accentSmoothCoef;
|
||||
// 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);
|
||||
|
||||
const float incA = currentFreq / sampleRate;
|
||||
const float incA = (currentFreq * params.detuneRatioA) / sampleRate;
|
||||
const float incB = (currentFreq * params.detuneRatio) / sampleRate;
|
||||
|
||||
const float a = renderWave (params.waveA, phaseA, incA);
|
||||
|
|
@ -190,7 +302,9 @@ private:
|
|||
out = (1.0f - params.ringMod) * out + params.ringMod * (a * b);
|
||||
|
||||
const float preFilter = out;
|
||||
out = processFilter (out);
|
||||
const float envCutoff = juce::jlimit (20.0f, 20000.0f,
|
||||
params.cutoff * std::pow (2.0f, params.fAmount * fenv));
|
||||
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.
|
||||
|
|
@ -215,15 +329,18 @@ private:
|
|||
float targetFreq = 440.0f;
|
||||
float smoothing = 0.0f;
|
||||
|
||||
float filterLow = 0.0f;
|
||||
float filterBand = 0.0f;
|
||||
float filterLow[4] = {}; // SVF stage states (up to 4 poles for 48 dB)
|
||||
float filterBand[4] = {};
|
||||
|
||||
float env = 0.0f;
|
||||
Stage envStage = Stage::idle;
|
||||
float fenv = 0.0f;
|
||||
Stage fenvStage = Stage::idle;
|
||||
bool gate = false;
|
||||
float accentLevel = 1.0f;
|
||||
float accentSmooth = 1.0f;
|
||||
float accentSmoothCoef = 0.01f;
|
||||
float accentAttackCoef = 0.0f; // fast charge when an accent starts
|
||||
float accentReleaseCoef = 0.01f; // slow release -> boost fades out at note end
|
||||
};
|
||||
|
||||
} // namespace monostep
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue