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:
@@ -205,22 +205,53 @@ private:
|
||||
velPitchRatio_ * pitchOffsetRatio_ * rateRatio_;
|
||||
}
|
||||
|
||||
// The rate the read head consumes SOURCE at, counting only the factors whose stage-time
|
||||
// coupling is compensated. Under Preserve that is the stretch rate alone — the Pitch offset
|
||||
// transposes inside the shifter and never touches the read. Under Varispeed both Rate and
|
||||
// Pitch are factors of the read increment and both are compensated: they are two views of one
|
||||
// multiply, so the "30 ms is 30 ms" rule binds them identically. Key-tracking and the
|
||||
// velocity->pitch transpose are deliberately LEFT OUT — those predate Rate, are shipped
|
||||
// sounds, and compensating them would move every note off the root.
|
||||
double stageFitRate() const {
|
||||
return preserveRead_ ? stretchRate_ : stretchRate_ * pitchOffsetRatio_;
|
||||
}
|
||||
|
||||
// A staged AHD's wall-clock stage frames converted into the SOURCE-offset domain the
|
||||
// sustain-less envelopes are evaluated in (sourceOffset()). Rate stretches the source span
|
||||
// those envelopes are fitted over, but a 30 ms attack is 30 ms at any rate — multiplying by
|
||||
// the read rate is exactly that conversion. The Varispeed PITCH coupling is deliberately NOT
|
||||
// compensated here: it predates Rate and is the shipped behaviour. Rate 1.0 returns the
|
||||
// argument untouched, which is what keeps the unity render bit-identical.
|
||||
// sustain-less envelopes are evaluated in (sourceOffset()). The read stretches the source
|
||||
// span those envelopes are fitted over, but a 30 ms attack is 30 ms at any rate —
|
||||
// multiplying by the read rate is exactly that conversion. A fit of exactly 1.0 (Rate 100 %,
|
||||
// Pitch 0 st) returns the argument untouched, which is what keeps the unity render
|
||||
// bit-identical.
|
||||
AhdParams rateFittedAhd(const AhdParams& a) const {
|
||||
if (stretchRate_ == 1.0) return a;
|
||||
const double fit = stageFitRate();
|
||||
if (fit == 1.0) return a;
|
||||
AhdParams out = a;
|
||||
out.attackFrames =
|
||||
static_cast<std::int64_t>(static_cast<double>(a.attackFrames) * stretchRate_ + 0.5);
|
||||
static_cast<std::int64_t>(static_cast<double>(a.attackFrames) * fit + 0.5);
|
||||
out.decayFrames =
|
||||
static_cast<std::int64_t>(static_cast<double>(a.decayFrames) * stretchRate_ + 0.5);
|
||||
static_cast<std::int64_t>(static_cast<double>(a.decayFrames) * fit + 0.5);
|
||||
return out;
|
||||
}
|
||||
|
||||
// The pitch AHD's span. That envelope counts OUTPUT frames while its Hold fraction is taken
|
||||
// against the playable SOURCE span, so the span converts by the rate the read head consumes
|
||||
// source at. Divides by that alone though the Varispeed read rate is really baseRatio_ x
|
||||
// envFactor: a deep pitch envelope makes it a first-order approximation, not exact.
|
||||
//
|
||||
// Shared by note-on and every live re-application, so a live Pitch move re-fits the envelope
|
||||
// rather than leaving it on the offset the note started at. Only that live factor is
|
||||
// re-read — pitchSpanBaseRate_ has it divided out — which is what leaves a legato retune's
|
||||
// documented drift (retune) exactly where it was.
|
||||
std::int64_t pitchEnvSpanFrames() const {
|
||||
if (sample_ == nullptr) return 0;
|
||||
const double postStart = static_cast<double>(
|
||||
static_cast<std::int64_t>(sample_->frames.size()) - startFrame_);
|
||||
const double readRate =
|
||||
preserveRead_ ? stretchRate_ : pitchSpanBaseRate_ * pitchOffsetRatio_;
|
||||
const double span = (readRate > 0.0) ? postStart / readRate : postStart;
|
||||
return static_cast<std::int64_t>(span + 0.5);
|
||||
}
|
||||
|
||||
// The read head as a fraction of the whole sample — the domain every spline EG is a pure
|
||||
// function of. Zero-length sample leaves splineScale_ at 0, which parks every contour on
|
||||
// its opening value.
|
||||
@@ -679,6 +710,14 @@ private:
|
||||
double velPitchRatio_ = 1.0; // the velocity->pitch factor alone; retune re-applies it
|
||||
double pitchOffsetRatio_ = 1.0; // the Pitch knob's factor — LIVE, re-applied by applyLive
|
||||
double rateRatio_ = 1.0; // Rate's factor of the read increment; start() owns when it is 1
|
||||
// Whether this note is ACTUALLY taking the Preserve read — a Preserve voice whose shifters
|
||||
// were never sized falls back to the varispeed one, and the two domains differ. Latched at
|
||||
// note-on beside rateRatio_, which start() resolves from the same predicate.
|
||||
bool preserveRead_ = false;
|
||||
// baseRatio_ with the live Pitch factor divided back out, latched at note-on: what
|
||||
// pitchEnvSpanFrames multiplies the CURRENT offset onto. Exact at Pitch 0 (the factor is
|
||||
// exactly 1.0), which is what keeps the unity span bit-identical.
|
||||
double pitchSpanBaseRate_ = 1.0;
|
||||
double ratio_ = 1.0; // fractional source frames advanced per output frame (this frame)
|
||||
double readPos_ = 0.0; // fractional frame index into the sample
|
||||
const SampleData* sample_ = nullptr;
|
||||
|
||||
Reference in New Issue
Block a user