instrument: fix AHD node-tracking/tie-break/live-latch defects and close staged-envelope-curve test gaps
This commit is contained in:
@@ -20,7 +20,7 @@ add_subdirectory(filter)
|
||||
# engine — the block is a plain value the voice observes, not a thing the engine owns.
|
||||
reasampler_pure_library(live_params
|
||||
SOURCES live_params.cpp
|
||||
LINK PUBLIC peaks velocity_curve filter)
|
||||
LINK PUBLIC peaks velocity_curve filter curve_law)
|
||||
reasampler_test(live_params LINK live_params)
|
||||
|
||||
# Two TUs on the engine's own responsibility seam (per-note setup vs. note routing and
|
||||
@@ -28,7 +28,7 @@ reasampler_test(live_params LINK live_params)
|
||||
# boundary costs the hot path nothing.
|
||||
reasampler_pure_library(sampler_core
|
||||
SOURCES voice.cpp voice_engine.cpp
|
||||
LINK PUBLIC peaks pitch_shift velocity_curve filter live_params)
|
||||
LINK PUBLIC peaks pitch_shift velocity_curve filter live_params curve_law)
|
||||
# Links only sampler_core: linking more would break the plain-data-boundary proof — a VST3
|
||||
# or REAPER type reaching the core would fail to compile or link here.
|
||||
reasampler_test(sampler_core LINK sampler_core)
|
||||
|
||||
@@ -346,14 +346,14 @@ public:
|
||||
// Trigger amp and filter envelopes. A zero/negative span finishes immediately.
|
||||
void configure(std::int64_t spanFrames, const AhdParams& params) {
|
||||
span_ = spanFrames > 0 ? spanFrames : 0;
|
||||
fit(params);
|
||||
fit(params, /*latchFinished=*/false); // a fresh note starts from a clean read
|
||||
smooth_.clear();
|
||||
}
|
||||
|
||||
// Peer of AdsrEnvelope::snapLive: a voice that has rendered nothing takes the new shape
|
||||
// outright, with no step to absorb.
|
||||
void snapLive(const AhdParams& params) {
|
||||
fit(params);
|
||||
fit(params, /*latchFinished=*/false);
|
||||
smooth_.clear();
|
||||
}
|
||||
|
||||
@@ -361,7 +361,10 @@ public:
|
||||
// why this smooths rather than holding a normalized position.
|
||||
void applyLive(double sourceOffset, const AhdParams& params) {
|
||||
const double before = ahdLevelAt(sourceOffset, fit_, attackCurve_, decayCurve_);
|
||||
fit(params);
|
||||
// LATCHED: a voice already read past its fitted total must never resurge because a
|
||||
// later live move reopened the total. Reachable on any active() voice, including one
|
||||
// ringing out past its own end (voice.h) where tickAmplitude() still runs.
|
||||
fit(params, /*latchFinished=*/true);
|
||||
const double after = ahdLevelAt(sourceOffset, fit_, attackCurve_, decayCurve_);
|
||||
if (after != before) smooth_.absorb(before - after);
|
||||
}
|
||||
@@ -381,11 +384,15 @@ public:
|
||||
const AhdSpan& stages() const { return fit_; }
|
||||
|
||||
private:
|
||||
void fit(const AhdParams& p) {
|
||||
// `latchFinished`: once true, a re-fit can only ever KEEP finished_ true, never clear it —
|
||||
// see applyLive above for why. configure()/snapLive() pass false: those are a fresh read
|
||||
// (new note or a not-yet-rendered voice), which must compute finished_ from scratch.
|
||||
void fit(const AhdParams& p, bool latchFinished) {
|
||||
fit_ = fitAhd(span_, p);
|
||||
attackCurve_ = p.attackCurve;
|
||||
decayCurve_ = p.decayCurve;
|
||||
finished_ = (fit_.total <= 0);
|
||||
const bool empty = (fit_.total <= 0);
|
||||
finished_ = latchFinished ? (finished_ || empty) : empty;
|
||||
}
|
||||
|
||||
std::int64_t span_ = 0;
|
||||
|
||||
@@ -55,8 +55,8 @@ struct AdsrParams {
|
||||
|
||||
// Attack -> Hold -> Decay over a bounded span: the shape every SUSTAIN-LESS envelope takes
|
||||
// (the Trigger amp, the Trigger filter envelope, the pitch envelope). Hold is a FRACTION of
|
||||
// the span left after attack and decay, never a time of its own — that is what makes
|
||||
// A + H + D <= span structural rather than clamped (see fitAhd in envelopes.h).
|
||||
// the span left after attack and decay, never a time of its own — fitAhd (envelopes.h) owns
|
||||
// why a fraction, not a time.
|
||||
struct AhdParams {
|
||||
std::int64_t attackFrames = 0;
|
||||
std::int64_t decayFrames = 0;
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
// configured an out-of-line render would put a call — and the envelope ticks behind it —
|
||||
// across a TU boundary on the hottest path in the program. The per-NOTE half (start /
|
||||
// retune / release / hardStop / presize) is cold enough to live in voice.cpp.
|
||||
//
|
||||
// DOCUMENTED ~600-line-ceiling EXCEPTION (root CLAUDE.md structural heuristic 1): this file
|
||||
// is over the ceiling because of the constraint above, not silent overshoot. A responsibility
|
||||
// seam here would move part of advanceFrame's inline body out of this header, reintroducing
|
||||
// the cross-TU call the header-inlining exists to avoid — worse than the overshoot.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
Reference in New Issue
Block a user