Bake window: derived note lengths carry exact durations, not ladder rungs — a long take is no longer cut at 384 beats
Hold keeps its picker. Also: one home for the %-fold, duration-ordered Hold travel, and a corrupt tail degrades to absent rather than fabricating one.
This commit is contained in:
+56
-12
@@ -2,7 +2,8 @@
|
||||
// note-length ladder. No VST3, no REAPER, no framework.
|
||||
//
|
||||
// Covers: both ends of the knob, the round trip from every rung, out-of-range and non-finite
|
||||
// input, and that every rung is reachable (no rung is skipped by the rounding).
|
||||
// input, that every rung is reachable (no rung is skipped by the rounding), that the travel is
|
||||
// monotone in DURATION, and that each rung owns an equal slice of it.
|
||||
|
||||
#include "../src/core/instrument/ui/bake_hold.h"
|
||||
|
||||
@@ -17,15 +18,37 @@ static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
namespace {
|
||||
|
||||
// The shortest and longest lengths ON THE LADDER, found by scanning rather than by indexing an
|
||||
// end of the picker order — which is exactly the assumption under test.
|
||||
Division shortestRung() {
|
||||
Division best = divisionAt(0);
|
||||
for (int i = 1; i < kDivisionCount; ++i)
|
||||
if (divisionBeats(divisionAt(i)) < divisionBeats(best)) best = divisionAt(i);
|
||||
return best;
|
||||
}
|
||||
|
||||
Division longestRung() {
|
||||
Division best = divisionAt(0);
|
||||
for (int i = 1; i < kDivisionCount; ++i)
|
||||
if (divisionBeats(divisionAt(i)) > divisionBeats(best)) best = divisionAt(i);
|
||||
return best;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
// --- The ends of the travel are the ends of the ladder ---------------------------
|
||||
CHECK(bakeHoldFromNorm(0.0) == divisionAt(0));
|
||||
CHECK(bakeHoldFromNorm(1.0) == divisionAt(kDivisionCount - 1));
|
||||
// --- The ends of the travel are the SHORTEST and LONGEST notes -------------------
|
||||
// Not divisionAt(0) / divisionAt(kDivisionCount - 1): those are the ends of the picker's
|
||||
// presentation order, which is not length order.
|
||||
CHECK(bakeHoldFromNorm(0.0) == shortestRung());
|
||||
CHECK(bakeHoldFromNorm(1.0) == longestRung());
|
||||
|
||||
// --- Out of range clamps rather than wrapping ------------------------------------
|
||||
CHECK(bakeHoldFromNorm(-3.0) == divisionAt(0));
|
||||
CHECK(bakeHoldFromNorm(9.5) == divisionAt(kDivisionCount - 1));
|
||||
CHECK(bakeHoldFromNorm(std::nan("")) == divisionAt(0));
|
||||
CHECK(bakeHoldFromNorm(-3.0) == shortestRung());
|
||||
CHECK(bakeHoldFromNorm(9.5) == longestRung());
|
||||
CHECK(bakeHoldFromNorm(std::nan("")) == shortestRung());
|
||||
|
||||
// --- Round trip: a knob painted from a stored rung and released reproduces it -----
|
||||
for (int i = 0; i < kDivisionCount; ++i) {
|
||||
@@ -44,13 +67,34 @@ int main() {
|
||||
for (int i = 0; i < kDivisionCount; ++i) CHECK(seen[static_cast<std::size_t>(i)]);
|
||||
}
|
||||
|
||||
// --- The map is monotone: turning the knob up never shortens the note -------------
|
||||
// --- The map is monotone IN DURATION: turning the knob up never shortens the note --
|
||||
// Asserted over divisionBeats, not divisionIndex: the index is presentation order, in which
|
||||
// a rung's triplet sits after (and is shorter than) the previous rung's dotted — so an
|
||||
// index sweep can be non-decreasing while the note it selects gets shorter.
|
||||
{
|
||||
int previous = -1;
|
||||
double previous = 0.0;
|
||||
for (int step = 0; step <= 4000; ++step) {
|
||||
const int index = divisionIndex(bakeHoldFromNorm(static_cast<double>(step) / 4000.0));
|
||||
CHECK(index >= previous);
|
||||
previous = index;
|
||||
const double beats =
|
||||
divisionBeats(bakeHoldFromNorm(static_cast<double>(step) / 4000.0));
|
||||
CHECK(beats >= previous);
|
||||
previous = beats;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Each rung owns an EQUAL slice, centred on its own position -------------------
|
||||
// A floor-based map passes every property above while giving each rung the slice ABOVE its
|
||||
// position; only the boundaries tell the two apart. Slice k spans [(k-0.5)/L, (k+0.5)/L).
|
||||
{
|
||||
constexpr int last = kDivisionCount - 1;
|
||||
const double slice = 1.0 / static_cast<double>(last);
|
||||
const double eps = slice / 1000.0;
|
||||
for (int k = 1; k < last; ++k) {
|
||||
const double centre = static_cast<double>(k) * slice;
|
||||
CHECK(bakeHoldFromNorm(centre) == bakeHoldFromNorm(centre - slice * 0.5 + eps));
|
||||
CHECK(bakeHoldFromNorm(centre) == bakeHoldFromNorm(centre + slice * 0.5 - eps));
|
||||
// …and one step past the upper boundary is already the NEXT rung.
|
||||
CHECK(divisionBeats(bakeHoldFromNorm(centre + slice * 0.5 + eps)) >
|
||||
divisionBeats(bakeHoldFromNorm(centre)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user