Play cross-rate captures at their own pitch and length
The voice now converts the file's rate to the host's at note-on instead of assuming they match. Stage times resolve at the host rate, the filter solves at it, and the bake follows. Same-rate renders are unchanged bit for bit.
This commit is contained in:
@@ -207,24 +207,27 @@ private:
|
||||
// THE fold of every pitch factor that is constant for the note into one number, so
|
||||
// advanceFrame's read increment stays the single multiply `baseRatio_ * envFactor` it has
|
||||
// always been: key-tracked repitch, the velocity->pitch transpose, the baseline Pitch offset,
|
||||
// and the Rate ratio — which start() zeroes out of this product when the note is running the
|
||||
// the Rate ratio — which start() zeroes out of this product when the note is running the
|
||||
// Preserve read, since Rate feeds stretch_ (duration) there and must never reach the
|
||||
// shifter's transpose. Cold: note-on, legato retune, and a live block, never per frame.
|
||||
// shifter's transpose — and the source/host rate conversion, which belongs in BOTH reads:
|
||||
// under Preserve the shifter's ring holds source frames, so its tap needs the same factor
|
||||
// to land the root on the root. Cold: note-on, legato retune, and a live block.
|
||||
void recomputeBaseRatio() {
|
||||
if (sample_ == nullptr) return;
|
||||
baseRatio_ = keyTrackedRatio(note_, sample_->rootNote, keyTrack_) *
|
||||
velPitchRatio_ * pitchOffsetRatio_ * rateRatio_;
|
||||
velPitchRatio_ * pitchOffsetRatio_ * rateRatio_ * sourceRatio_;
|
||||
}
|
||||
|
||||
// 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
|
||||
// coupling is compensated. Under Preserve that is the feed 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.
|
||||
// multiply, so the "30 ms is 30 ms" rule binds them identically. The source/host conversion
|
||||
// (inside feedRate_) is compensated under both, because it is a change of units, not a
|
||||
// sound. 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_;
|
||||
return preserveRead_ ? feedRate_ : feedRate_ * pitchOffsetRatio_;
|
||||
}
|
||||
|
||||
// A staged AHD's wall-clock stage frames converted into the SOURCE-offset domain the
|
||||
@@ -258,7 +261,7 @@ private:
|
||||
const double postStart = static_cast<double>(
|
||||
static_cast<std::int64_t>(sample_->frames.size()) - startFrame_);
|
||||
const double readRate =
|
||||
preserveRead_ ? stretchRate_ : pitchSpanBaseRate_ * pitchOffsetRatio_;
|
||||
preserveRead_ ? feedRate_ : pitchSpanBaseRate_ * pitchOffsetRatio_;
|
||||
const double span = (readRate > 0.0) ? postStart / readRate : postStart;
|
||||
return static_cast<std::int64_t>(span + 0.5);
|
||||
}
|
||||
@@ -529,8 +532,8 @@ private:
|
||||
double outL, outRlocal = 0.0;
|
||||
if (pitchEngine_ == PitchEngine::Preserve && shiftL_.configured()) {
|
||||
// The two rates the shifter takes (pitch_shift.h owns why they are independent):
|
||||
// the source is FED at stretchRate_, and the tap is SHIFTED by
|
||||
// 2^((note-root + pitchEnvSemis)/12) — the pitch envelope adds to the shift amount,
|
||||
// the source is FED at feedRate_, and the tap is SHIFTED by baseRatio_ x
|
||||
// 2^(pitchEnvSemis/12) — the pitch envelope adds to the shift amount,
|
||||
// never to the read rate. The feed runs one window ahead of readPos_ (the rings were
|
||||
// primed with that window at start()), under the same sustain-loop wrap rule,
|
||||
// reading integer source frames into the ring — no RATE-DEPENDENT interpolation
|
||||
@@ -607,13 +610,13 @@ private:
|
||||
outRlocal = shiftedL;
|
||||
}
|
||||
}
|
||||
// Preserve advances the read head at the STRETCH rate — the one duration control.
|
||||
// Everything downstream of it (the loop wrap, the Trigger span, the spline phase)
|
||||
// therefore stays a source-frame fact and scales by construction.
|
||||
// Preserve advances the read head at the FEED rate — the one duration control, in
|
||||
// source frames. Everything downstream of it (the loop wrap, the Trigger span, the
|
||||
// spline phase) therefore stays a source-frame fact and scales by construction.
|
||||
//
|
||||
// The two sustain-less envelopes are evaluated at sourceOffset(), which advances at
|
||||
// this rate — rateFittedAhd is what keeps their stage times wall-clock anyway.
|
||||
ratio_ = stretchRate_;
|
||||
ratio_ = feedRate_;
|
||||
} else {
|
||||
// VARISPEED: pitch and duration coupled. The read rate carries the repitch; the
|
||||
// pitch envelope multiplies the ratio for the read-rate bias (unchanged idiom when
|
||||
@@ -816,6 +819,11 @@ private:
|
||||
PitchShifter shiftR_;
|
||||
instrument::engine::StretchCursor stretch_;
|
||||
double stretchRate_ = 1.0; // Preserve playback rate, clamped and latched at note-on
|
||||
// SampleData::sourceRateRatio, latched at note-on: source frames per output frame at unity.
|
||||
double sourceRatio_ = 1.0;
|
||||
// stretchRate_ in source frames per output frame (x sourceRatio_): the Preserve read head's
|
||||
// and feed's exact rate, and the Rate factor every stage-time fit converts by.
|
||||
double feedRate_ = 1.0;
|
||||
std::vector<AudioSample> primeBuf_;
|
||||
|
||||
// lastOut{L,R}_ track the voice's most recent rendered output. A takeover/steal start()
|
||||
|
||||
Reference in New Issue
Block a user