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:
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user