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();