fix: guard filter tail NaNs, skip static-filter envelope work, pin stereo filter path, correct stale comments

Codec fallback for non-finite filter fields, a modAmount==0 early-out in tickFilterCutoff,
new stereo render tests for the dual-mono mirror, and comment/test accuracy fixes flagged
in review (stale deck-width claims, restated invariants, drifted CMake link comments).
This commit is contained in:
2026-07-30 17:35:45 -04:00
parent 67215509cb
commit 39389c1183
14 changed files with 137 additions and 41 deletions
+12 -4
View File
@@ -54,8 +54,9 @@ inline double filterNormPerOctave() {
// A modulated cutoff re-solves the SVF coefficients, which costs a tan() plus the morph's
// cos/sin — so the solve is gated on the modulated position crossing one step of this
// quantization of the sweep. 2048 steps over three decades is ~0.06 semitone, far under the
// ear's resolution for a filter corner, and it collapses the solve to nothing across a static
// envelope stage: an unmodulated voice pays one integer compare per frame.
// ear's resolution for a filter corner. tickFilterCutoff's own modAmount==0 early-out (see
// there) skips the envelope/clamp/quantize math too, so a static, already-solved voice pays
// two cheap compares per frame there, never the tan/cos/sin.
inline constexpr int kFilterModSteps = 2048;
// Takeover declick: a restart of a sounding voice (mono retrigger takeover/fallback or a
@@ -178,9 +179,16 @@ private:
}
// Advances the filter envelope and re-solves the filter's coefficients when the modulated
// cutoff has moved a whole quantization step (see kFilterModSteps). prepare() deliberately
// preserves integrator state, so a moving cutoff glides rather than clicking.
// cutoff has moved a whole quantization step (see kFilterModSteps); state preservation
// across that solve is voice_filter's own contract (voice_filter.h / filter/CLAUDE.md).
//
// filterModAmount_ is fixed for the note's lifetime (set once in start()), so once the
// first solve has run (filterModStep_ != -1) a zero depth can only ever re-derive the same
// cutoff — skip the envelope tick, clamp, and quantization entirely rather than pay them
// to land on the answer already solved. filterModStep_ == -1 (forced by start()/retune()
// via updateFilterCutoffBase) still falls through here so the base cutoff's own solve runs.
void tickFilterCutoff() {
if (filterModAmount_ == 0.0 && filterModStep_ != -1) return;
double cut = static_cast<double>(filterBaseCutoff_) +
filterModAmount_ * filterEnv_.tick();
if (cut < 0.0) cut = 0.0;