Fix cross-rate Preserve splices, re-stamp on host-rate change, name the rate refusal

The splice crossfade now counts output frames and stops before the ring's far end (this also fixes a lap at matched rate). The search strides by the rate ratio. The editor takes its time base from its own decode. Same-rate output is pinned to hashes from before the change.
This commit is contained in:
2026-09-25 14:17:11 -04:00
parent ec60cf78c6
commit 64f314a6a4
24 changed files with 592 additions and 183 deletions
+15 -11
View File
@@ -6,8 +6,8 @@
// the mechanism behind the "unity is bit-identical to the shipped Preserve read" gate.
// 2. the schedule tracks the rate: over N output frames the cursor consumes N*rate source
// frames to within one, at rates either side of unity and at irrational ones.
// 3. the per-output-frame feed count never exceeds kMaxFeedPerFrame — the bound that makes
// a variable-length feed loop RT-safe.
// 3. the per-output-frame feed count never exceeds ceil(kStretchRateMax) at a matched rate —
// the bound that makes a variable-length feed loop RT-safe.
// 4. the clamp: out-of-range folds to the bounds, unusable input folds to unity (never to a
// silent stall or a quarter-speed surprise).
// 4b. the source/host rate ratio multiplies the CLAMPED rate, unclipped by the clamp itself.
@@ -29,6 +29,10 @@ static int g_fail = 0;
static ResolvedLoop noLoop() { return ResolvedLoop{}; }
// The most source frames one output frame can make due at a matched rate.
static const std::int64_t kMatchedFeedBound =
static_cast<std::int64_t>(std::ceil(kStretchRateMax));
static ResolvedLoop loopSpan(std::int64_t start, std::int64_t end) {
ResolvedLoop lp;
lp.active = true;
@@ -41,7 +45,7 @@ static ResolvedLoop loopSpan(std::int64_t start, std::int64_t end) {
// --- 1. Unity is exactly one frame per output frame, with no drifting residue. ---
static void testUnityRateFeedsExactlyOneFramePerOutputFrame() {
StretchCursor c;
c.start(100);
c.start(100, 1.0);
const ResolvedLoop lp = noLoop();
for (std::int64_t i = 0; i < 200000; ++i) {
CHECK(c.due(1.0) == 1);
@@ -60,7 +64,7 @@ static void testTotalConsumedTracksTheRate() {
// would drift without bound rather than carrying the residue.
for (double rate : {0.5, 0.75, 1.0, 1.3333333333333333, 2.0, 1.0 / 3.0 + 1.0}) {
StretchCursor c;
c.start(0);
c.start(0, 1.0);
const std::int64_t outFrames = 100000;
for (std::int64_t i = 0; i < outFrames; ++i) {
const std::int64_t due = c.due(rate);
@@ -78,14 +82,14 @@ static void testFeedPerOutputFrameIsBounded() {
// for as many iterations as the rate names.
for (double rate : {kStretchRateMax, kStretchRateMax * 100.0, 3.99, 2.5}) {
StretchCursor c;
c.start(0);
c.start(0, 1.0);
std::int64_t worst = 0;
for (std::int64_t i = 0; i < 20000; ++i) {
const std::int64_t due = c.due(rate);
if (due > worst) worst = due;
for (std::int64_t k = 0; k < due; ++k) (void)c.next(lp);
}
CHECK(worst <= kMaxFeedPerFrame);
CHECK(worst <= kMatchedFeedBound);
CHECK(worst >= 1); // and the bound is not vacuous — frames genuinely fell due
}
}
@@ -103,11 +107,11 @@ static void testRateClamp() {
CHECK(clampStretchRate(std::nan("")) == 1.0);
// ...and the cursor honours it rather than looping on the raw value.
StretchCursor c;
c.start(0);
c.start(0, 1.0);
CHECK(c.due(-5.0) == 1); // folded to unity
StretchCursor d;
d.start(0);
CHECK(d.due(50.0) <= kMaxFeedPerFrame);
d.start(0, 1.0);
CHECK(d.due(50.0) <= kMatchedFeedBound);
}
// --- 4b. The source/host rate ratio scales the feed AFTER the musical clamp. ---
@@ -143,7 +147,7 @@ static void testCursorStaysInsideTheLoopSpan() {
const ResolvedLoop lp = loopSpan(1000, 1040); // a 40-frame loop: rate 4 steps 10% of it
for (double rate : {0.5, 1.0, 2.0, 4.0}) {
StretchCursor c;
c.start(1000);
c.start(1000, 1.0);
std::int64_t lowest = 1 << 30, highest = -1;
for (std::int64_t i = 0; i < 50000; ++i) {
const std::int64_t due = c.due(rate);
@@ -163,7 +167,7 @@ static void testCursorStaysInsideTheLoopSpan() {
// A cursor started BEYOND the loop end (the start-point-past-the-loop case) is pulled in on
// its first take rather than reading off the end.
StretchCursor c;
c.start(5000);
c.start(5000, 1.0);
const std::int64_t q = c.next(lp);
CHECK(q >= lp.start && q < lp.end);
}