Close pass four: undetented host curve read, LiveValues guard, ordering assert, static-lane fix, docs

Points toPlain's exponent arm at the undetented curve map so host reads
match the editor; adds a sizeof guard plus field-poison test for
LiveValues::operator==; skips the model write when an automation value
hasn't moved; corrects five stale doc citations.
This commit is contained in:
2026-08-02 18:22:21 -04:00
parent 1fd38bbd57
commit 9b5393098b
14 changed files with 163 additions and 59 deletions
+63
View File
@@ -91,6 +91,68 @@ static void testADrawnEnvelopePinsTheFoldedTriggerLength() {
CHECK(v.lengthFraction == 1.0);
}
// Poisons the block ONE LEAF FIELD AT A TIME and checks operator== catches every one — the half
// that actually catches a forgotten field, since the static_assert above only fires when a
// member changes sizeof(LiveValues), which padding can absorb. Covers every leaf of every
// nested struct, not just the 14 top-level members, so a member dropped from sameAdsr/sameAhd/
// sameFilterSettings is caught here too, not just a member dropped from operator== itself.
static void testEveryFieldOfLiveValuesIsCompared() {
using instrument::engine::filter::MorphLaw;
const LiveValues base{};
auto poisoned = [&](auto mutate) {
LiveValues v = base;
mutate(v);
return v;
};
CHECK(base == base);
CHECK(poisoned([](LiveValues& v) { v.filterSettings.cutoffNorm += 0.1f; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterSettings.resonanceNorm += 0.1f; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterSettings.morphNorm += 0.1f; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterSettings.driveNorm += 0.1f; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterSettings.morphLaw = MorphLaw::HighNotchLow; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterModAmount += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterVelAmount += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterKeyTrack += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterEnv.attackFrames += 1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterEnv.holdFrames += 1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterEnv.decayFrames += 1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterEnv.sustainLevel += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterEnv.releaseFrames += 1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterEnv.attackCurve += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterEnv.decayCurve += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterEnv.releaseCurve += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterAhd.attackFrames += 1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterAhd.decayFrames += 1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterAhd.holdFraction += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterAhd.attackCurve += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.filterAhd.decayCurve += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.adsr.attackFrames += 1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.adsr.holdFrames += 1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.adsr.decayFrames += 1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.adsr.sustainLevel += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.adsr.releaseFrames += 1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.adsr.attackCurve += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.adsr.decayCurve += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.adsr.releaseCurve += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.ampAhd.attackFrames += 1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.ampAhd.decayFrames += 1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.ampAhd.holdFraction += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.ampAhd.attackCurve += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.ampAhd.decayCurve += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.pitchEnv.enabled = !v.pitchEnv.enabled; }) != base);
CHECK(poisoned([](LiveValues& v) { v.pitchEnv.peakSemitones += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.pitchEnv.shape.attackFrames += 1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.pitchEnv.shape.decayFrames += 1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.pitchEnv.shape.holdFraction += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.pitchEnv.shape.attackCurve += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.pitchEnv.shape.decayCurve += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.playRate += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.pitchOffsetSemitones += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.keyTrack += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.lengthFraction += 0.1; }) != base);
CHECK(poisoned([](LiveValues& v) { v.splineActive = !v.splineActive; }) != base);
}
static void testUnpublishedBlockReadsAsNothing() {
LiveParams block;
LiveValues out;
@@ -188,6 +250,7 @@ static void testRampStepIsRateDerived() {
int main() {
testFoldCarriesEveryContinuousControl();
testADrawnEnvelopePinsTheFoldedTriggerLength();
testEveryFieldOfLiveValuesIsCompared();
testUnpublishedBlockReadsAsNothing();
testConcurrentReaderNeverSeesAHalfAppliedEdit();
testRampTerminatesExactlyOnTheTarget();
+6 -7
View File
@@ -126,11 +126,10 @@ static void testTheEditorAndTheHostPrintTheSameDigitsAtTheSameStoredValue() {
}
}
// The one place the two surfaces GENUINELY diverge, asserted so it stays a known property rather
// than a surprise: an exponent inside curve_law's centre detent but not exactly neutral is
// reachable only through an overlay knot drag, and the host — which holds the norm and nothing
// else — reads it back as the neutral the knob law snaps to.
static void testAnOffDetentExponentReadsNeutralToTheHostAndTrueToTheEditor() {
// A continuous range stays continuous at the host boundary (Daniel, 2026-08-02): the knob
// detent is a drag affordance only, so an exponent inside it but not exactly neutral — reachable
// via an overlay knot drag — must read back true to the host, the same digits the editor shows.
static void testAnOffDetentExponentReadsTrueToBothTheHostAndTheEditor() {
PlaySeconds play;
// The detent is +/-0.01 in NORM, which is a ~+/-0.047 band in the exponent — so 1.04 is
// inside it and still prints as a distinct number.
@@ -144,7 +143,7 @@ static void testAnOffDetentExponentReadsNeutralToTheHostAndTrueToTheEditor() {
char hostBuf[24];
formatPlainFor(DeckParam::kAttackCurve, toPlain(DeckParam::kAttackCurve, hostNorm), hostBuf,
sizeof(hostBuf));
CHECK(std::string(hostBuf) == "1.00");
CHECK(std::string(hostBuf) == "1.04");
}
static void testKeyTrackPrintsTheSameDigitsFromEitherSurface() {
@@ -227,7 +226,7 @@ static void testEveryExposedParameterHasAFormatterThatWritesSomething() {
int main() {
testEachCategoryPrintsItsSpecifiedShape();
testTheEditorAndTheHostPrintTheSameDigitsAtTheSameStoredValue();
testAnOffDetentExponentReadsNeutralToTheHostAndTrueToTheEditor();
testAnOffDetentExponentReadsTrueToBothTheHostAndTheEditor();
testMasterGainPrintsTheSameDigitsFromEitherSurface();
testKeyTrackPrintsTheSameDigitsFromEitherSurface();
testTypingBackADisplayedValueLandsOnIt();