Γ-W1-T1 review fixes: mode-independent taper rounding, sharper drag-step test, reset-sweep verifies stored fields
Swap nearbyint for std::round (MXCSR-independent); derive the finest-drag test from the editor floor, not the knob; verify resets against fields, not norms; record the spline-point modifier exclusion.
This commit is contained in:
@@ -175,8 +175,11 @@ static void testResetLandsOnTheStoredDefaultOfEachControl() {
|
||||
// EVERY knob resets to its own stored default, not just the six dual-ring pairs above. Swept
|
||||
// over the whole control-id space so a control added later cannot quietly miss the reset table:
|
||||
// perturb, reset, and require the control to read exactly what a fresh PlaySeconds reads.
|
||||
// Compared against the STORED FIELD directly (deckDoubleField/deckFloatField), not the
|
||||
// normalized read-back: deckParamNorm is not guaranteed injective, so a norm match is weaker
|
||||
// than the criterion — verification against a default-constructed PlaySeconds.
|
||||
static void testEveryKnobIdResetsToItsDefault() {
|
||||
const PlaySeconds defaults;
|
||||
PlaySeconds defaults;
|
||||
for (int i = 0; i < static_cast<int>(DeckParam::kCount); ++i) {
|
||||
const DeckParam id = static_cast<DeckParam>(i);
|
||||
if (deckParamUnit(id) == UnitCategory::None) continue; // no reset gesture
|
||||
@@ -184,9 +187,17 @@ static void testEveryKnobIdResetsToItsDefault() {
|
||||
PlaySeconds p;
|
||||
setDeckParam(id, p, 0.37, 0);
|
||||
setDeckParam(id, p, 0.83, 0); // two writes: one of the two is off every default
|
||||
CHECK(deckParamNorm(id, p) != deckParamNorm(id, defaults));
|
||||
resetDeckParam(id, p);
|
||||
CHECK(deckParamNorm(id, p) == deckParamNorm(id, defaults));
|
||||
if (double* pd = deckDoubleField(id, p)) {
|
||||
CHECK(*pd != *deckDoubleField(id, defaults));
|
||||
resetDeckParam(id, p);
|
||||
CHECK(*pd == *deckDoubleField(id, defaults));
|
||||
} else if (float* pf = deckFloatField(id, p)) {
|
||||
CHECK(*pf != *deckFloatField(id, defaults));
|
||||
resetDeckParam(id, p);
|
||||
CHECK(*pf == *deckFloatField(id, defaults));
|
||||
} else {
|
||||
CHECK(false); // every non-None, non-excluded id must own a reset field
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,8 +290,10 @@ static void testAValueStoredUnderTheOldCeilingIsReadNotRewritten() {
|
||||
CHECK(p.adsr.releaseSeconds == 2.0);
|
||||
CHECK(normDecay > 0.0 && normDecay < 1.0); // still on the knob, just at a new angle
|
||||
CHECK(deckParamNorm(DeckParam::kRelease, p) > normDecay);
|
||||
// And it survives the norm the knob would hand back, so a no-op touch of the control does
|
||||
// not quantize a legacy value away.
|
||||
// And a no-op touch survives the norm the knob would hand back — for THIS value, which is
|
||||
// exactly on the taper's output quantum grid (1.75 s parses to a grid-aligned double). A
|
||||
// legacy value off the grid (e.g. 1.2345678912345) WOULD be re-quantized on first touch;
|
||||
// that is correct, intended behaviour, not a gap this test is claiming to cover.
|
||||
setDeckParam(DeckParam::kDecay, p, normDecay, 0);
|
||||
CHECK(p.adsr.decaySeconds == 1.75);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
|
||||
#include "../src/core/instrument/ui/param_taper.h"
|
||||
|
||||
#include "../src/core/instrument/ui/envelope_overlay.h" // gateStageSlotPx: finest drag surface
|
||||
#include "../src/core/instrument/ui/sample_bands.h" // kEditorMinWidth/kPad: the editor floor
|
||||
|
||||
#include <cfenv>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
@@ -78,10 +82,15 @@ static void testStageTimeIsMonotone() {
|
||||
}
|
||||
}
|
||||
|
||||
// The FINEST drag a user can make — Ctrl's 1/20 rate over the 128 px knob travel — must still
|
||||
// move the value, or the output quantum would be observable as a dead zone.
|
||||
// The FINEST drag a user can make on ANY surface this taper serves — not the knob's own 128 px
|
||||
// travel, which is coarser than the AHDSR schematic's node drag at the editor floor. Derived from
|
||||
// the floor constant and the overlay's own slot-width formula, so a later floor change sharpens
|
||||
// (or coarsens) the step this test exercises automatically instead of leaving a copied number
|
||||
// silently stale.
|
||||
static void testEveryFinestDragStepMovesTheValue() {
|
||||
const int steps = static_cast<int>(1.0 / kFineDragScale) * 128;
|
||||
const Rect floorArea = Rect::ltrb(0, 0, kEditorMinWidth - 2 * kPad, 100);
|
||||
const double slot = gateStageSlotPx(floorArea);
|
||||
const int steps = static_cast<int>(slot / kFineDragScale);
|
||||
for (int i = 0; i < steps; ++i) {
|
||||
const double lo = timeSecondsFromNorm(static_cast<double>(i) / steps);
|
||||
const double hi = timeSecondsFromNorm(static_cast<double>(i + 1) / steps);
|
||||
@@ -108,6 +117,32 @@ static void testEveryWholeMicrosecondRoundTripsExactly() {
|
||||
}
|
||||
}
|
||||
|
||||
// MODE-INDEPENDENCE, the whole point of resolveTo's std::round over std::nearbyint. First shows
|
||||
// the defect directly, generically: under round-toward-zero, the RETIRED std::nearbyint reads
|
||||
// that mode and truncates a value whose fraction is well past half, while std::round (specified
|
||||
// to round half-away-from-zero REGARDLESS of the current mode) does not. Then proves the
|
||||
// production round trip itself — not a stand-in — survives the same hostile mode across the grid.
|
||||
static void testRoundingSurvivesAHostileFpRoundingMode() {
|
||||
const int saved = std::fegetround();
|
||||
CHECK(std::fesetround(FE_TOWARDZERO) == 0);
|
||||
|
||||
CHECK(std::nearbyint(12.9) == 12.0); // the RETIRED behaviour: mode-dependent, wrong here
|
||||
CHECK(std::round(12.9) == 13.0); // the fix: mode-independent, rounds to nearest
|
||||
|
||||
for (int us = 0; us <= 200000; us += 7) {
|
||||
const double seconds = static_cast<double>(us) / 1e6;
|
||||
CHECK(timeSecondsFromNorm(timeNormFromSeconds(seconds)) == seconds);
|
||||
if (timeSecondsFromNorm(timeNormFromSeconds(seconds)) != seconds) break;
|
||||
}
|
||||
for (int milli = -24000; milli <= 24000; milli += 37) {
|
||||
const double d = static_cast<double>(milli) / 1000.0;
|
||||
CHECK(depthSemitonesFromNorm(depthNormFromSemitones(d, kDepth), kDepth) == d);
|
||||
if (depthSemitonesFromNorm(depthNormFromSemitones(d, kDepth), kDepth) != d) break;
|
||||
}
|
||||
|
||||
std::fesetround(saved); // restore — every other test in this binary assumes the default
|
||||
}
|
||||
|
||||
// The converse round trip is NOT required, but its residual is worth pinning: it is bounded by
|
||||
// the output quantum read back through the map, which stays four orders below one drag pixel.
|
||||
// Pinned so a future quantum change cannot make the needle visibly lag the hand unnoticed.
|
||||
@@ -239,6 +274,7 @@ int main() {
|
||||
testStageTimeIsMonotone();
|
||||
testEveryFinestDragStepMovesTheValue();
|
||||
testEveryWholeMicrosecondRoundTripsExactly();
|
||||
testRoundingSurvivesAHostileFpRoundingMode();
|
||||
testNormRoundTripResidualStaysBelowOneDragPixel();
|
||||
testTheStageTimeDefaultsRoundTripExactly();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user