Smooth MIDI CC parameter changes per-sample to avoid stepped/zippery fast knob rotation

This commit is contained in:
Armin 2026-09-08 16:29:47 +02:00
commit 51a6944326
4 changed files with 77 additions and 18 deletions

View file

@ -62,6 +62,25 @@ SynthEngine::Voice* SynthEngine::acquireVoice()
return &voices[stealIdx];
}
void SynthEngine::glideParams (VParams& sp, const VParams& tgt, float k)
{
auto glide = [k] (float& cur, float tgtv) { cur += (tgtv - cur) * k; };
glide (sp.detune, tgt.detune);
glide (sp.oscMix, tgt.oscMix);
glide (sp.fltCut, tgt.fltCut);
glide (sp.fltRes, tgt.fltRes);
glide (sp.fltEnv, tgt.fltEnv);
glide (sp.fltKey, tgt.fltKey);
glide (sp.ampA, tgt.ampA); glide (sp.ampD, tgt.ampD);
glide (sp.ampS, tgt.ampS); glide (sp.ampR, tgt.ampR);
glide (sp.fA, tgt.fA); glide (sp.fD, tgt.fD);
glide (sp.fS, tgt.fS); glide (sp.fR, tgt.fR);
glide (sp.lfoRate, tgt.lfoRate); glide (sp.lfoDepth, tgt.lfoDepth);
glide (sp.fxAmt, tgt.fxAmt);
glide (sp.vol, tgt.vol); glide (sp.pan, tgt.pan);
glide (sp.glide, tgt.glide);
}
void SynthEngine::noteOn (int midiNote, float velocity)
{
const float noteFreq = (float) (440.0 * std::exp2 ((midiNote - 69.0) / 12.0));
@ -108,18 +127,31 @@ void SynthEngine::noteOff (int midiNote)
void SynthEngine::render (const VParams& p, float* left, float* right, int num)
{
// Parameter glide: smooth CC/automation changes that arrive between blocks.
// Without this, several MIDI CC values that fall inside one audio block are
// collapsed into a single block step, so fast knob rotation appears to skip
// intermediate values ("zipper"/stepped sound). We ramp the continuous
// parameters per-sample toward their targets instead of snapping.
if (! svValid)
{
sv = p;
svValid = true;
}
VParams sp = sv;
const float k = 0.02f;
for (auto& v : voices)
{
v.amp.setRates (p.ampA, p.ampD, p.ampS, p.ampR, sr);
v.flt.setRates (p.fA, p.fD, p.fS, p.fR, sr);
v.amp.setRates (sp.ampA, sp.ampD, sp.ampS, sp.ampR, sr);
v.flt.setRates (sp.fA, sp.fD, sp.fS, sp.fR, sr);
}
lastGlide = p.glide;
lastPan = p.pan;
lastPan = sp.pan;
polyLimit = juce::jlimit (1, maxVoice, (int) p.poly);
const float glideRate = p.glide <= 0.001f ? 1.0f
: 1.0f - std::exp (-1.0f / (p.glide * (float) sr));
const float detuneRatio = std::exp2f (p.detune / 1200.0f);
const float glideRate = sp.glide <= 0.001f ? 1.0f
: 1.0f - std::exp (-1.0f / (sp.glide * (float) sr));
const float detuneRatio = std::exp2f (sp.detune / 1200.0f);
const float tuneRatio = std::exp2f (p.tune / 12.0f);
const float maxFc = 18000.0f, minFc = 30.0f;
@ -127,18 +159,20 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
{
float mixL = 0.0f, mixR = 0.0f;
glideParams (sp, p, k);
for (auto& v : voices)
{
if (! v.active)
continue;
if (p.glide > 0.001f)
if (sp.glide > 0.001f)
v.curFreq += (v.targetFreq - v.curFreq) * glideRate;
else
v.curFreq = v.targetFreq;
// LFO
v.lfoPh += (double) p.lfoRate / sr;
v.lfoPh += (double) sp.lfoRate / sr;
while (v.lfoPh >= 1.0)
{
v.lfoPh -= 1.0;
@ -159,7 +193,7 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
// frequency, vibrato, bend, tune
float freq = v.curFreq * tuneRatio;
if ((int) p.lfoTarget == 1)
freq *= std::exp2f (lfo * p.lfoDepth * 1.6f / 12.0f);
freq *= std::exp2f (lfo * sp.lfoDepth * 1.6f / 12.0f);
if (bendSmooth != bendCents)
bendSmooth += (bendCents - bendSmooth) * 0.08f;
freq *= std::exp2f (bendSmooth / 1200.0f);
@ -172,17 +206,17 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
const float o1 = osc ((int) p.osc1w, v.ph1);
const float o2 = osc ((int) p.osc2w, v.ph2);
const float mixb = p.oscMix * 0.01f;
const float mixb = sp.oscMix * 0.01f;
float sig = o1 * (1.0f - mixb) + o2 * mixb;
const float fe = v.flt.next();
// cutoff with keytrack + filter envelope + LFO + mod wheel
float fc = p.fltCut;
fc *= std::exp2f (p.fltKey * 0.01f * std::log2f (freq / 261.6256f));
fc *= std::exp2f (p.fltEnv * 0.01f * fe * std::log2f (maxFc / fc));
float fc = sp.fltCut;
fc *= std::exp2f (sp.fltKey * 0.01f * std::log2f (freq / 261.6256f));
fc *= std::exp2f (sp.fltEnv * 0.01f * fe * std::log2f (maxFc / fc));
if ((int) p.lfoTarget == 0)
fc *= std::exp2f (lfo * p.lfoDepth * 10.0f / 12.0f);
fc *= std::exp2f (lfo * sp.lfoDepth * 10.0f / 12.0f);
fc *= std::exp2f (mod);
fc = std::min (maxFc, std::max (minFc, fc));
v.fcSm += (fc - v.fcSm) * 0.25f;
@ -191,7 +225,7 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
// SVF (cap f1 to keep the Chamberlin structure stable; past ~1.5 the
// resonance feedback can self-oscillate and sputter at maxed cutoff+res)
const float f1 = std::min (1.50f, 2.0f * std::sin (juce::MathConstants<float>::pi * fc / (float) sr));
const float q1 = 1.0f / (1.0f + p.fltRes * 3.0f);
const float q1 = 1.0f / (1.0f + sp.fltRes * 3.0f);
v.f1c = f1;
const float hp = sig - v.lp - q1 * v.bp;
v.bp += f1 * hp;
@ -210,7 +244,7 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
const float ae = v.amp.next();
float ampMod = 1.0f;
if ((int) p.lfoTarget == 2)
ampMod = 1.0f - p.lfoDepth * 0.5f * (1.0f + lfo);
ampMod = 1.0f - sp.lfoDepth * 0.5f * (1.0f + lfo);
v.outSmooth += (filtered - v.outSmooth) * 0.12f;
float o = v.outSmooth * (ae * ampMod) * v.vel;
@ -233,5 +267,7 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
left[s] += mixL;
right[s] += mixR;
sv = sp;
}
}