Γ-W1-T5: a real Preserve time-stretcher — write rate is duration, tap rate is pitch

Generalizes the correlation-aligned SOLA delay line so the feed and the shift are
independent rates over one ring. Unity is bit-identical to the shipped read, asserted
against a hash baseline captured pre-change.
This commit is contained in:
2026-08-01 19:06:06 -04:00
parent e589addc54
commit 589a8e078b
10 changed files with 873 additions and 71 deletions
+8 -2
View File
@@ -18,7 +18,8 @@ void Voice::presizePreserveShifters(std::int64_t windowFrames) {
primeBuf_.assign(windowFrames > 1 ? static_cast<std::size_t>(windowFrames) : 0, 0.0f);
}
void Voice::start(int note, int velocity, const SampleData& sample, bool declickTakeover) {
void Voice::start(int note, int velocity, const SampleData& sample, bool declickTakeover,
double stretchRate) {
// Before any state reset, record the pre-cut reference (last rendered output) and mark
// the compensation pending iff this start is a takeover/steal of a sounding voice and the
// caller opted in. The ramp is seeded on the first frame rendered after the restart, from
@@ -59,6 +60,9 @@ void Voice::start(int note, int velocity, const SampleData& sample, bool declick
baseRatio_ = keyTrackedRatio(note, sample.rootNote, sample.keyTrack) * velPitchRatio_;
playMode_ = p.playMode;
pitchEngine_ = p.pitchEngine;
// Clamped once here so the read head's increment and the feed cursor's debt accumulate the
// SAME value — they must stay exactly one window apart for the note's whole life.
stretchRate_ = instrument::engine::clampStretchRate(stretchRate);
// 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.
@@ -234,7 +238,9 @@ void Voice::start(int note, int velocity, const SampleData& sample, bool declick
}
// Per-frame feed continues at `p` (the feed bound when the prime exhausted the
// playable span).
feedPos_ = p;
stretch_.start(p);
shiftL_.setFeedRate(stretchRate_);
shiftR_.setFeedRate(stretchRate_);
if (!loopWrap && primeCount < w) {
// Sub-window playable span: the source is already exhausted at prime time.
shiftL_.freezeTail();