Γ-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:
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
// time_stretch — the Preserve engine's TIME half: how fast the source is consumed, given a
|
||||
// playback rate. It pairs with pitch_shift's PITCH half (how fast the ring's read tap runs);
|
||||
// the two rates are independent over one delay ring, and only their difference reaches the
|
||||
// splice machinery. Header-inline: every member sits on the per-voice-per-sample feed.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "core/instrument/engine/loop/loop_span.h"
|
||||
|
||||
namespace reasampler::instrument::engine {
|
||||
|
||||
// The playback rates the Preserve DSP is measured over, and therefore the only ones it
|
||||
// accepts. Two independent reasons they are here and not wider:
|
||||
// - the ceiling is what bounds a voice's per-output-frame feed loop (kMaxFeedPerFrame source
|
||||
// frames), which is the RT-safety argument for feeding a variable count at all;
|
||||
// - the splice search can only align a period it can see. The tap's delay drifts at
|
||||
// |rate - shift| per frame, so a wide rate over a deep DOWN-shift splices faster than one
|
||||
// period of the output tone and the correlation stops holding the pitch: measured at rate
|
||||
// 4.0 with -24 st, the observed period came out 539 frames against 785 wanted.
|
||||
inline constexpr double kStretchRateMin = 0.5;
|
||||
inline constexpr double kStretchRateMax = 2.0;
|
||||
inline constexpr int kMaxFeedPerFrame = 2; // ceil(kStretchRateMax)
|
||||
|
||||
// Non-positive and NaN fold to unity rather than to the minimum: an unusable rate should leave
|
||||
// playback alone, not silently quarter-speed it (the same stance as setShiftRatio's refusal to
|
||||
// run the tap backward). 1.0 in gives exactly 1.0 out, which is what keeps the unity read
|
||||
// bit-identical.
|
||||
inline double clampStretchRate(double rate) {
|
||||
if (!(rate > 0.0)) return 1.0;
|
||||
if (rate < kStretchRateMin) return kStretchRateMin;
|
||||
return rate > kStretchRateMax ? kStretchRateMax : rate;
|
||||
}
|
||||
|
||||
// One Preserve voice's source-feed schedule: a fractional source cursor answering, per OUTPUT
|
||||
// frame, which whole source frames fall due. At rate 1.0 that is exactly one frame per output
|
||||
// frame with no residue carried — bit for bit the pre-stretch feed.
|
||||
class StretchCursor {
|
||||
public:
|
||||
// `frame` is where the ring prime stopped; the per-frame feed continues there.
|
||||
void start(std::int64_t frame) {
|
||||
frame_ = frame;
|
||||
debt_ = 0.0;
|
||||
}
|
||||
|
||||
// Adds one output frame's worth of source at `rate` and returns how many whole source
|
||||
// frames are now due, in [0, kMaxFeedPerFrame]. Take each of them with next(). The clamp
|
||||
// lives here rather than at the caller because this return value is the loop bound.
|
||||
std::int64_t due(double rate) {
|
||||
debt_ += clampStretchRate(rate);
|
||||
const std::int64_t whole = static_cast<std::int64_t>(debt_); // debt_ >= 0: trunc = floor
|
||||
debt_ -= static_cast<double>(whole);
|
||||
return whole;
|
||||
}
|
||||
|
||||
// The next due source frame, wrapped into the sustain loop, advancing the cursor past it.
|
||||
// Advances even past the playable span — the caller freezes the shifter's writer there, and
|
||||
// a cursor that stalled instead would re-feed one frame forever.
|
||||
std::int64_t next(const loop::ResolvedLoop& lp) {
|
||||
if (lp.active) {
|
||||
while (frame_ >= lp.end) frame_ -= lp.length;
|
||||
}
|
||||
return frame_++;
|
||||
}
|
||||
|
||||
std::int64_t frame() const { return frame_; }
|
||||
|
||||
private:
|
||||
std::int64_t frame_ = 0;
|
||||
double debt_ = 0.0; // fractional source frames carried into the next output frame
|
||||
};
|
||||
|
||||
} // namespace reasampler::instrument::engine
|
||||
Reference in New Issue
Block a user