instrument: latch the note done at the read-head run-off, fit the migrated fades, and lift the curve dial and overlay selection into pure modules

This commit is contained in:
2026-07-31 09:17:04 -04:00
parent 13e8c5c4d9
commit d60ab1524a
18 changed files with 575 additions and 129 deletions
+10 -2
View File
@@ -90,8 +90,16 @@ void Voice::start(int note, int velocity, const SampleData& sample, bool declick
}
// The pitch AHD's Hold fraction is taken against the whole playable span, so its three
// stages lay 1:1 over the waveform from the start point.
pitchEnv_.configure(postStart, p.pitchEnv);
// stages lay 1:1 over the waveform from the start point. postStart is a SOURCE-frame count
// and this envelope counts OUTPUT frames (envelopes.h), so Varispeed — which consumes
// 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.
const double pitchSpan =
(pitchEngine_ == PitchEngine::Preserve || !(baseRatio_ > 0.0))
? static_cast<double>(postStart)
: static_cast<double>(postStart) / baseRatio_;
pitchEnv_.configure(static_cast<std::int64_t>(pitchSpan + 0.5), p.pitchEnv);
pitchEnv_.noteOn();
// A restart lands every live glide back on the new note's own values, at a step derived
+11 -2
View File
@@ -348,6 +348,12 @@ private:
// play span ends within the ramp). With no declick (the common case) this is
// byte-identical to the plain idle-out.
if (triggerRanOff || readPos_ >= static_cast<double>(frameCount)) {
// The NOTE is over the moment the read head leaves its span, whether or not a ramp
// still rings: no later frame can carry envelope output. Latching here is what keeps
// a ringing-out voice out of soundingNote() — the Preserve cap would otherwise
// refuse a new onset, and mono legato would retune a voice already past its end
// (silencing the new note) for the whole ~4 ms ramp.
amplitudeDone_ = true;
if (declickPending_) seedDeclick();
if (!declickActive_) seedTerminalDeclick();
if (declickActive_) {
@@ -376,8 +382,11 @@ private:
// Peer of the read-head exhaustion path above: a Trigger AHD whose stages end BEFORE
// the play span (a zero decay, which the shape deliberately keeps expressible) cuts the
// same synthetic Preserve tail at whatever level it was at. Seeded from lastOut, which
// still holds the PREVIOUS frame — this one is already silent. Gate is left out on
// purpose: its amplitude reaches zero through a release, so there is no cut to ring out.
// still holds the PREVIOUS frame — this one is already silent. Gate is left out of THIS
// site only: its amplitude reaches zero through a release, so nothing here is cut
// mid-level. The exhaustion path above deliberately does NOT exclude Gate — a held Gate
// note whose source runs out with no loop is cut at its sustain level, and under
// Preserve that cut lands on the same recycled synthetic tail.
if (amplitudeDone_ && amp == 0.0 && !declickActive_ &&
playMode_ == PlayMode::Trigger) {
seedTerminalDeclick();