@@ -223,25 +223,35 @@ class Voice {
223223 // Pre-filter saturation -> grit (and dirties the filter for fat/acid tones).
224224 float drv = Saturate (sig, drive) * 0 .6f ;
225225 // Filter coefficients only depend on (fc, res, filter type). SetFreq is costly
226- // (Svf: sinf+powf; Moog: a polynomial), so skip it on samples where none changed
227- // -- a big saving for static-filter patches (cutoff held, no filter envelope).
226+ // (Svf: sinf+powf; Moog: a polynomial). Two savings stack:
227+ // 1. skip it when nothing changed -- free for static-filter patches.
228+ // 2. update at CONTROL RATE (every kCoefInterval samples), not per sample --
229+ // a moving filter envelope changes fc every sample, which used to force a
230+ // per-sample SetFreq on every voice; on a chord that spiked one block past
231+ // the deadline and crackled. ~6 kHz coef updates are inaudible for sweeps.
232+ // A filter-TYPE switch forces an immediate recompute so the new filter isn't stale.
228233 const int fltSel = (filterType < 0 .5f ) ? 0 : 1 ;
229- const bool coefDirty = (fc != lastFc_) || (res != lastRes_) || (fltSel != lastFltSel_);
230- lastFc_ = fc;
231- lastRes_ = res;
234+ const bool typeChanged = (fltSel != lastFltSel_);
232235 lastFltSel_ = fltSel;
233- if (fltSel == 0 ) { // clean 2-pole Svf
234- if (coefDirty) {
235- flt_.SetFreq (fc);
236- flt_.SetRes (res * 0 .85f );
236+ if (typeChanged) coefCountdown_ = 0 ;
237+ if (--coefCountdown_ <= 0 ) {
238+ coefCountdown_ = kCoefInterval ;
239+ if (fc != lastFc_ || res != lastRes_ || typeChanged) {
240+ lastFc_ = fc;
241+ lastRes_ = res;
242+ if (fltSel == 0 ) {
243+ flt_.SetFreq (fc);
244+ flt_.SetRes (res * 0 .85f );
245+ } else {
246+ mflt_.SetFreq (fc);
247+ mflt_.SetRes (res * 0 .95f ); // fat 4-pole MoogLadder
248+ }
237249 }
250+ }
251+ if (fltSel == 0 ) { // clean 2-pole Svf
238252 flt_.Process (drv);
239253 return flt_.Low () * env * vel_;
240254 }
241- if (coefDirty) {
242- mflt_.SetFreq (fc);
243- mflt_.SetRes (res * 0 .95f ); // fat 4-pole MoogLadder
244- }
245255 return mflt_.Process (drv) * env * vel_;
246256 }
247257
@@ -269,8 +279,10 @@ class Voice {
269279 float wtPhase_ = 0 .f, fmPhase_ = 0 .f; // wavetable carrier + FM modulator phases
270280 bool gate_ = false ;
271281 // Cached per-block work (sentinels force a recompute on the first sample):
282+ static constexpr int kCoefInterval = 8 ; // recompute filter coefs every N samples
272283 float lastFc_ = -1 .f, lastRes_ = -1 .f; // filter coefficients (see Process)
273284 int lastFltSel_ = -1 ; // 0 = Svf, 1 = Moog
285+ int coefCountdown_ = 0 ; // samples until the next coef recompute
274286 float uniMul_[kUni ] = {1 .f , 1 .f , 1 .f , 1 .f }; // unison detune frequency multipliers
275287 float uniGain_ = 1 .f; // 1 / unison count
276288 int lastU_ = -1 ; // unison count the multipliers were built for
0 commit comments