instrument: fix AHD DecayEnd overlay/grab defect, pin flaky curve test, close comment/doc findings
This commit is contained in:
@@ -95,6 +95,9 @@ void Voice::start(int note, int velocity, const SampleData& sample, bool declick
|
||||
// baseRatio_ source frames per output frame — needs the span converted, or a transposed
|
||||
// note's envelope outruns (or outlives) the note it shapes. Preserve reads at the source
|
||||
// rate, so its two domains already coincide.
|
||||
// Divides by baseRatio_ alone, though the actual Varispeed read rate is baseRatio_ x
|
||||
// envFactor — a deep pitch envelope makes this a first-order approximation, not exact.
|
||||
// Strictly better than the un-converted source-frame span it replaced.
|
||||
const double pitchSpan =
|
||||
(pitchEngine_ == PitchEngine::Preserve || !(baseRatio_ > 0.0))
|
||||
? static_cast<double>(postStart)
|
||||
@@ -268,6 +271,9 @@ void Voice::retune(int note) {
|
||||
// legato phrase is one gesture, one strike (classic mono-synth behavior).
|
||||
if (!active_ || sample_ == nullptr) return;
|
||||
note_ = note;
|
||||
// Changes baseRatio_ without re-converting pitchEnv_'s already-configured span (the
|
||||
// baseRatio_ division in the note-on setup above), so a slide leaves that envelope on the
|
||||
// first note's domain — consistent with "touch nothing else," but the drift lives here.
|
||||
baseRatio_ = keyTrackedRatio(note, sample_->rootNote, sample_->keyTrack);
|
||||
// Filter key-tracking follows the pitch: it is a function of the note, so a slide moves it
|
||||
// too. The velocity offset deliberately stays the first note's, matching velocityGain_.
|
||||
|
||||
@@ -6,11 +6,6 @@
|
||||
// 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>
|
||||
|
||||
@@ -51,7 +51,7 @@ reasampler_test(knob_deck LINK knob_deck)
|
||||
# MorphLaw) for the v9 filter tail -- plain value types, no filter symbol linked.
|
||||
reasampler_pure_library(deck_groups
|
||||
SOURCES deck_groups.cpp
|
||||
LINK PUBLIC knob_deck velocity_curve peaks)
|
||||
LINK PUBLIC knob_deck velocity_curve peaks curve_law)
|
||||
# sample_bands is linked directly for the test only: the deck-fits-the-floor-window assertion
|
||||
# needs the band allocator deck_groups itself has no reason to depend on.
|
||||
reasampler_test(deck_groups LINK deck_groups sample_bands)
|
||||
|
||||
@@ -108,11 +108,29 @@ double curveFromKnotDrag(const StageEnvelope& grabEnv, EnvNode knot, double grab
|
||||
return curveFromMidLevel((newLevel - seg.start) / span);
|
||||
}
|
||||
|
||||
// An AHD's DecayEnd moves decaySeconds via X, scaled by 1/(1 - holdFraction) — see
|
||||
// resolveNodeDrag's DecayEnd case. At holdFraction == 1.0 that derivative is exactly 0, so a
|
||||
// drag there can never change anything; when it ALSO coincides with HoldEnd (decay ~ 0) it is a
|
||||
// dead handle sitting on top of a live one. Excluded from the grabbable set in that exact case
|
||||
// only — a functional DecayEnd (holdFraction < 1) stays grabbable even when it coincides.
|
||||
bool ahdDecayEndIsDead(const StageEnvelope& env, const std::vector<EnvVertex>& poly) {
|
||||
if (env.kind != EnvKind::Ahd) return false;
|
||||
if (1.0 - clamp01(env.holdFraction) > 1e-9) return false;
|
||||
EnvVertex hold, decay;
|
||||
bool haveHold = false, haveDecay = false;
|
||||
for (const EnvVertex& v : poly) {
|
||||
if (v.node == EnvNode::HoldEnd) { hold = v; haveHold = true; }
|
||||
else if (v.node == EnvNode::DecayEnd) { decay = v; haveDecay = true; }
|
||||
}
|
||||
return haveHold && haveDecay && hold.x == decay.x;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NodeHit nodeAtPoint(const StageEnvelope& env, const OverlayArea& area, double totalSeconds,
|
||||
int x, int y) {
|
||||
const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, area, totalSeconds);
|
||||
const bool dropDeadDecayEnd = ahdDecayEndIsDead(env, poly);
|
||||
// Nearest draggable, kind-matching node within the pick radius wins (Chebyshev distance);
|
||||
// ties go to the earlier draw-order node. Knots are appended last, so a knot coincident
|
||||
// with an endpoint handle loses — a drag there stays a time edit.
|
||||
@@ -120,6 +138,7 @@ NodeHit nodeAtPoint(const StageEnvelope& env, const OverlayArea& area, double to
|
||||
int bestDist = kNodeGrabRadius + 1;
|
||||
for (const EnvVertex& v : poly) {
|
||||
if (!isDraggable(v.node) || !nodeInKind(v.node, env.kind)) continue;
|
||||
if (dropDeadDecayEnd && v.node == EnvNode::DecayEnd) continue;
|
||||
const int dist = std::max(std::abs(x - v.x), std::abs(y - v.y));
|
||||
if (dist < bestDist) { // strict-less-than keeps ties at the earlier draw order
|
||||
bestDist = dist;
|
||||
|
||||
@@ -37,7 +37,9 @@ struct EnvClampBounds {
|
||||
// inputs buildEnvelopePolyline drew from). `hit` is false for a point off every draggable node.
|
||||
// Nearest node within the radius wins (Chebyshev distance); an exact tie goes to the earlier
|
||||
// draw-order node, and since knots are appended last, a coincident endpoint handle wins over a
|
||||
// knot rather than the drag silently becoming a curve edit.
|
||||
// knot rather than the drag silently becoming a curve edit. An AHD's DecayEnd is excluded
|
||||
// entirely when it coincides with HoldEnd AND holdFraction == 1.0 — there it cannot move
|
||||
// (resolveNodeDrag's derivative is 0), so it is dropped rather than won by draw order.
|
||||
struct NodeHit {
|
||||
bool hit = false;
|
||||
EnvNode node = EnvNode::Origin; // meaningful only when hit == true
|
||||
|
||||
@@ -177,19 +177,14 @@ std::vector<EnvVertex> ahdPolyline(const StageEnvelope& env, const Rect& area,
|
||||
pts.reserve(6);
|
||||
pts.push_back(vtx(EnvNode::Origin, area, totalSeconds, t0, 0.0));
|
||||
pts.push_back(vtx(EnvNode::AttackEnd, area, totalSeconds, t0 + s.attack, 1.0));
|
||||
EnvVertex holdVtx = vtx(EnvNode::HoldEnd, area, totalSeconds, t0 + s.attack + s.hold, 1.0);
|
||||
EnvVertex decayVtx = vtx(EnvNode::DecayEnd, area, totalSeconds, t0 + s.total, 0.0);
|
||||
// A hold that consumes the WHOLE post-attack/decay remainder (holdFraction == 1.0, the
|
||||
// Trigger AHD default) puts HoldEnd and DecayEnd on the same wall-clock instant, and
|
||||
// nodeAtPoint's earlier-draw-order tie-break then hides DecayEnd behind HoldEnd forever.
|
||||
// Nudge apart, clamped to the canvas — the same minimum-separation rationale
|
||||
// kGateNodeSepPx exists for on the AHDSR schematic, applied to this coincidence instead.
|
||||
if (decayVtx.x - holdVtx.x < kGateNodeSepPx) {
|
||||
const int right = area.x + std::max(1, area.width) - 1;
|
||||
decayVtx.x = std::min(holdVtx.x + kGateNodeSepPx, right);
|
||||
}
|
||||
pts.push_back(holdVtx);
|
||||
pts.push_back(decayVtx);
|
||||
pts.push_back(vtx(EnvNode::HoldEnd, area, totalSeconds, t0 + s.attack + s.hold, 1.0));
|
||||
// DecayEnd is drawn at t0 + total, which coincides with HoldEnd exactly when decay ~ 0 —
|
||||
// independent of holdFraction (total = attack + hold + decay always). Left at its true
|
||||
// instant rather than nudged: the 1:1 axis this policy exists to keep honest must hold even
|
||||
// at a shared instant, including the Trigger default's abrupt (zero-decay) cutoff.
|
||||
// envelope_edit's nodeAtPoint handles the coincidence instead, by dropping DecayEnd from the
|
||||
// grabbable set when it also cannot move (holdFraction == 1.0).
|
||||
pts.push_back(vtx(EnvNode::DecayEnd, area, totalSeconds, t0 + s.total, 0.0));
|
||||
if (s.attack > 0.0) {
|
||||
pts.push_back(knotVtx(EnvNode::AttackCurve, area, pts[0].x, pts[1].x, 0.0, 1.0,
|
||||
env.attackCurve));
|
||||
|
||||
Reference in New Issue
Block a user