Bake window: derive it from the rate the voice actually reads at, so a dialled Rate or downward Pitch no longer truncates the file
This commit is contained in:
@@ -70,9 +70,12 @@ void Voice::start(int note, int velocity, const SampleData& sample, bool declick
|
||||
// configured, and a Preserve voice whose shifters were never sized falls back to the
|
||||
// varispeed read. Rate has to reach the increment there too, or that fallback would ignore
|
||||
// the control outright — the predicate is spelled the same way advanceFrame spells it.
|
||||
const bool preserveRead = (pitchEngine_ == PitchEngine::Preserve) && shiftL_.configured();
|
||||
rateRatio_ = preserveRead ? 1.0 : stretchRate_;
|
||||
preserveRead_ = (pitchEngine_ == PitchEngine::Preserve) && shiftL_.configured();
|
||||
rateRatio_ = preserveRead_ ? 1.0 : stretchRate_;
|
||||
recomputeBaseRatio();
|
||||
// pitchOffsetRatio_ is a power of 2 and never zero, so this inverse is well-defined — and at
|
||||
// Pitch 0 it is a division by exactly 1.0.
|
||||
pitchSpanBaseRate_ = baseRatio_ / pitchOffsetRatio_;
|
||||
|
||||
// Clamp into [0, frames): a start at or past the end degrades to 0 (play from the top)
|
||||
// rather than starting a voice already off the end.
|
||||
@@ -133,16 +136,9 @@ 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. postStart is a SOURCE-frame count
|
||||
// and this envelope counts OUTPUT frames (envelopes.h), so the span has to be divided by the
|
||||
// rate the read head consumes source at — baseRatio_ under Varispeed, the stretch rate under
|
||||
// Preserve — or a transposed (or re-rated) note's envelope outruns the note it shapes.
|
||||
// Divides by baseRatio_ alone under Varispeed, though the actual read rate is baseRatio_ x
|
||||
// envFactor — a deep pitch envelope makes that a first-order approximation, not exact.
|
||||
const double readRate = preserveRead ? stretchRate_ : baseRatio_;
|
||||
const double pitchSpan = (readRate > 0.0) ? static_cast<double>(postStart) / readRate
|
||||
: static_cast<double>(postStart);
|
||||
pitchEnv_.configure(static_cast<std::int64_t>(pitchSpan + 0.5), p.pitchEnv);
|
||||
// stages lay 1:1 over the waveform from the start point. The source->output conversion, and
|
||||
// why it is only first-order, are pitchEnvSpanFrames' own (voice.h).
|
||||
pitchEnv_.configure(pitchEnvSpanFrames(), p.pitchEnv);
|
||||
pitchEnv_.noteOn();
|
||||
|
||||
// A restart lands every live glide back on the new note's own values, at a step derived
|
||||
@@ -273,22 +269,24 @@ void Voice::applyLive(const instrument::engine::LiveValues& live, bool snap) {
|
||||
//
|
||||
// live.playRate is deliberately NOT read on either path: Rate is the note-on-latched class,
|
||||
// delivered as start()'s argument by VoiceEngine::startVoice (live_params.h owns why). The
|
||||
// latched stretchRate_ is what rateFittedAhd converts a live AHD against, so a stage-time
|
||||
// move mid-note lands in this note's own rate domain rather than resetting it.
|
||||
// latched stretchRate_ is what stageFitRate carries into every conversion below, so a
|
||||
// stage-time move mid-note lands in this note's own rate domain rather than resetting it.
|
||||
const bool gate = (playMode_ == PlayMode::Gate);
|
||||
// The baseline Pitch offset IS live, under both engines: Varispeed picks the new baseRatio_
|
||||
// up as one more factor of next frame's read increment, Preserve as the shifter's transpose.
|
||||
// Applied BEFORE the envelopes below, because under Varispeed it is a factor of the read rate
|
||||
// both of them are fitted against — a stale offset here would fit them to the previous move.
|
||||
pitchOffsetRatio_ = semitoneRatio(live.pitchOffsetSemitones);
|
||||
recomputeBaseRatio();
|
||||
if (snap) {
|
||||
if (gate) env_.snapLive(live.adsr);
|
||||
else ampAhd_.snapLive(rateFittedAhd(live.ampAhd));
|
||||
pitchEnv_.snapLive(live.pitchEnv);
|
||||
pitchEnv_.snapLive(pitchEnvSpanFrames(), live.pitchEnv);
|
||||
} else {
|
||||
if (gate) env_.applyLive(live.adsr);
|
||||
else ampAhd_.applyLive(sourceOffset(), rateFittedAhd(live.ampAhd));
|
||||
pitchEnv_.applyLive(live.pitchEnv);
|
||||
pitchEnv_.applyLive(pitchEnvSpanFrames(), live.pitchEnv);
|
||||
}
|
||||
// The baseline Pitch offset IS live, under both engines: Varispeed picks the new baseRatio_
|
||||
// up as one more factor of next frame's read increment, Preserve as the shifter's transpose.
|
||||
pitchOffsetRatio_ = semitoneRatio(live.pitchOffsetSemitones);
|
||||
recomputeBaseRatio();
|
||||
// The pitch DEPTH knob stays live under a spline (core/instrument/CLAUDE.md), but
|
||||
// pitchSplineDepth_ is a plain member latched at note-on — unlike filter's modAmount_,
|
||||
// which already glides through rModAmount_'s live ramp regardless of spline state (below),
|
||||
@@ -340,9 +338,10 @@ 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.
|
||||
// Changes baseRatio_ without re-converting pitchEnv_'s already-configured span
|
||||
// (pitchEnvSpanFrames, whose base rate this deliberately does not move), so a slide leaves
|
||||
// that envelope on the first note's domain — consistent with "touch nothing else," but the
|
||||
// drift lives here.
|
||||
// The velocity->pitch factor rides through the slide unchanged, matching velocityGain_ —
|
||||
// one gesture, one strike. Rate and the Pitch offset ride through too: only the note moved.
|
||||
recomputeBaseRatio();
|
||||
|
||||
Reference in New Issue
Block a user