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:
@@ -10,6 +10,7 @@
|
||||
// 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.
|
||||
// 5. the sustain loop wraps the cursor and never lets it leave [start, end) — "loop the
|
||||
// source" holds at every rate, including one that steps over the loop end.
|
||||
|
||||
@@ -109,6 +110,34 @@ static void testRateClamp() {
|
||||
CHECK(d.due(50.0) <= kMaxFeedPerFrame);
|
||||
}
|
||||
|
||||
// --- 4b. The source/host rate ratio scales the feed AFTER the musical clamp. ---
|
||||
static void testSourceRateRatioScalesTheClampedRate() {
|
||||
const ResolvedLoop lp = noLoop();
|
||||
// 96k source in a 44.1k host: past the musical ceiling on its own, and not clipped by it.
|
||||
const double ratio = 96000.0 / 44100.0;
|
||||
for (double rate : {0.5, 1.0, 2.0, 50.0}) {
|
||||
StretchCursor c;
|
||||
c.start(0, ratio);
|
||||
const std::int64_t outFrames = 100000;
|
||||
std::int64_t worst = 0;
|
||||
for (std::int64_t i = 0; i < outFrames; ++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);
|
||||
}
|
||||
const double expected = static_cast<double>(outFrames) * clampStretchRate(rate) * ratio;
|
||||
CHECK(std::fabs(static_cast<double>(c.frame()) - expected) <= 1.0);
|
||||
CHECK(static_cast<double>(worst) <= std::ceil(kStretchRateMax * ratio));
|
||||
}
|
||||
// A ratio of exactly 1.0 is the unity schedule, frame for frame.
|
||||
StretchCursor u;
|
||||
u.start(7, 1.0);
|
||||
for (std::int64_t i = 0; i < 1000; ++i) {
|
||||
CHECK(u.due(1.0) == 1);
|
||||
CHECK(u.next(lp) == 7 + i);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 5. The loop wraps the SOURCE cursor, at every rate. ---
|
||||
static void testCursorStaysInsideTheLoopSpan() {
|
||||
const ResolvedLoop lp = loopSpan(1000, 1040); // a 40-frame loop: rate 4 steps 10% of it
|
||||
@@ -144,6 +173,7 @@ int main() {
|
||||
testTotalConsumedTracksTheRate();
|
||||
testFeedPerOutputFrameIsBounded();
|
||||
testRateClamp();
|
||||
testSourceRateRatioScalesTheClampedRate();
|
||||
testCursorStaysInsideTheLoopSpan();
|
||||
|
||||
if (g_fail == 0) {
|
||||
|
||||
Reference in New Issue
Block a user