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:
@@ -96,10 +96,8 @@ struct PitchEnvParams {
|
||||
// normalized control positions verbatim rather than a parallel set, so no control range is
|
||||
// re-derived here; `filter_params.h` owns every law that maps them to Hz/Q/depth.
|
||||
//
|
||||
// The three modulation depths all land in that same normalized cutoff domain and sum before a
|
||||
// single clamp: `modAmount` scales the per-frame filter envelope, `velAmount` scales the
|
||||
// note-on velocity through `velocityCurve`, and `keyTrack` moves cutoff by octaves per octave
|
||||
// above the root. All three are zero/neutral by default.
|
||||
// The three modulation depths below land in that same normalized cutoff domain and sum
|
||||
// before a single clamp; all three are zero/neutral by default.
|
||||
struct FilterParams {
|
||||
bool enabled = false;
|
||||
instrument::engine::filter::FilterSettings settings;
|
||||
@@ -109,7 +107,10 @@ struct FilterParams {
|
||||
AdsrParams env; // the same staged AHDSR the amp runs; frames
|
||||
// Shapes velocity before velAmount scales it. Linear rather than the amp's flat() default
|
||||
// because a flat curve under a depth control would make every velocity the same offset;
|
||||
// the no-op at rest is velAmount == 0, not the curve.
|
||||
// the no-op at rest is velAmount == 0, not the curve. NOTE: this default only governs a
|
||||
// FRESH FilterParams — the shared codec's corrupt/truncated-point-list repair
|
||||
// (VelocityCurve::fromPoints, used for both this curve and the amp's) still degrades to
|
||||
// flat() regardless, since that repair has no curve-specific fallback.
|
||||
VelocityCurve velocityCurve = VelocityCurve::linear();
|
||||
};
|
||||
|
||||
|
||||
@@ -92,8 +92,8 @@ void Voice::start(int note, int velocity, const SampleData& sample, bool declick
|
||||
pitchEnv_.configure(p.pitchEnv);
|
||||
pitchEnv_.noteOn();
|
||||
|
||||
// Filter: fresh integrators per note (prepare() preserves state on purpose, so a note-on
|
||||
// is the one place that must clear it). Velocity maps through the curve once here, off the
|
||||
// Filter: reset() clears integrator state for the new note (prepare() preserves it —
|
||||
// voice_filter.h / filter/CLAUDE.md). Velocity maps through the curve once here, off the
|
||||
// per-frame path, exactly as the amp's velocityGain_ does.
|
||||
filterOn_ = p.filter.enabled;
|
||||
if (filterOn_) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user