Close derived-bake-window review findings: NaN-guard remaining wire doubles, pin Trigger-span agreement, retire dead quantizer

Guards params_payload.cpp's filter-tail seconds and both keyTrack sites against NaN; pins Voice::start's Trigger-span formula against trigger_seam; retires unused shortestDivisionAtLeast.
This commit is contained in:
2026-08-01 21:32:05 -04:00
parent 65f6070348
commit eb6093e085
7 changed files with 84 additions and 92 deletions
+34 -3
View File
@@ -954,25 +954,41 @@ static void testV13HardFlagCountThatStrandsAlignmentLeavesTheHoldAbsentNotFabric
// Numeric domains are established at the DOOR, not at each consumer. A NaN pitch depth reaches
// the bake's pow() and the voice's ratio multiply; a NaN %-length and a NaN stage time reach
// narrowing casts that are undefined on one; and a root override outside MIDI range makes the
// narrowing casts that are undefined on one; a NaN keyTrack reaches keyTrackedRatio ->
// baseRatio_ -> readPos_'s per-sample static_cast<std::int64_t> (voice.h); a NaN v9 filter
// env second reaches secToFrames the same way the trigAhd/filter.trigEnv pair already covered
// by testNonFiniteAhdSecondsLiftToZero do; and a root override outside MIDI range makes the
// bake's render note and the sample's own root disagree, which is a read rate other than 1 and
// therefore a window sized in the truncating direction.
static void testOutOfDomainWireValuesAreBoundedAtTheCodec() {
const PlaySeconds defaults;
const InstrumentParams paramDefaults;
ComponentState in;
in.selectionId = "pad";
in.params.keyTrack = 0.5; // a neighbouring field, to show the guards are per-field
in.params.keyTrack = std::numeric_limits<double>::quiet_NaN();
in.params.rootOverride = 9999;
in.params.play.pitchEnv.peakSemitones = std::numeric_limits<double>::quiet_NaN();
in.params.play.trigger.lengthFraction = std::numeric_limits<double>::quiet_NaN();
in.params.play.adsr.releaseSeconds = std::numeric_limits<double>::infinity();
in.params.play.filter.enabled = true;
in.params.play.filter.env.attackSeconds = std::numeric_limits<double>::quiet_NaN();
in.params.play.filter.env.holdSeconds = std::numeric_limits<double>::infinity();
in.params.play.filter.env.decaySeconds = -std::numeric_limits<double>::infinity();
in.params.play.filter.env.sustainLevel = std::numeric_limits<double>::quiet_NaN();
in.params.play.filter.env.releaseSeconds = std::numeric_limits<double>::quiet_NaN();
const ComponentState out = deserializeComponentState(serializeComponentState(in), 48000.0);
CHECK(out.params.rootOverride && *out.params.rootOverride == 127);
CHECK(out.params.play.pitchEnv.peakSemitones == defaults.pitchEnv.peakSemitones);
CHECK(out.params.play.trigger.lengthFraction == defaults.trigger.lengthFraction);
CHECK(out.params.play.adsr.releaseSeconds == defaults.adsr.releaseSeconds);
CHECK(out.params.keyTrack == 0.5);
CHECK(out.params.keyTrack == paramDefaults.keyTrack);
CHECK(out.params.play.filter.enabled); // the fallback is per-field, not per-record
CHECK(out.params.play.filter.env.attackSeconds == defaults.filter.env.attackSeconds);
CHECK(out.params.play.filter.env.holdSeconds == defaults.filter.env.holdSeconds);
CHECK(out.params.play.filter.env.decaySeconds == defaults.filter.env.decaySeconds);
CHECK(out.params.play.filter.env.sustainLevel == defaults.filter.env.sustainLevel);
CHECK(out.params.play.filter.env.releaseSeconds == defaults.filter.env.releaseSeconds);
ComponentState low = in;
low.params.rootOverride = -5;
@@ -980,6 +996,20 @@ static void testOutOfDomainWireValuesAreBoundedAtTheCodec() {
CHECK(lowOut.params.rootOverride && *lowOut.params.rootOverride == 0);
}
// The keyTrack guard's OTHER site: a pre-v7 (legacy zone-list) blob's own keyTrack field
// (params_payload.cpp's readLegacyZonePayload, pv >= 6) is a second, independent read of the
// same wire double — same hazard, same fallback, must not be missed just because the v8+
// single-record reader above was fixed.
static void testLegacyZoneKeyTrackNaNLiftsToDefault() {
legacy::Zone z;
z.sampleId = "kick";
z.keyTrack = std::numeric_limits<double>::quiet_NaN();
const ComponentState out =
deserializeComponentState(legacy::envelopeWithZones("kick", {z}, 7), 48000.0);
CHECK(out.params.keyTrack == InstrumentParams{}.keyTrack);
}
// A hard-flag tail truncated mid-COUNT-FIELD (only 2 of its 4 length bytes present, and
// nothing else after) is a different failure shape than a declared-huge count: the u32 read
// itself fails, tripping r.ok inside readHardFlags rather than its own bound check. That must
@@ -1935,6 +1965,7 @@ int main() {
testV13HardFlagOutOfBoundsCountSurvivesWithoutWipingTheRecord();
testV13HardFlagCountThatStrandsAlignmentLeavesTheHoldAbsentNotFabricated();
testOutOfDomainWireValuesAreBoundedAtTheCodec();
testLegacyZoneKeyTrackNaNLiftsToDefault();
testV13HardFlagTailTruncatedMidCountSurvivesWithoutWipingTheRecord();
testBakeHoldRoundTripsAndDisturbsNothingElse();
testV13BlobLiftsToTheDefaultHold();
+2 -51
View File
@@ -3,15 +3,13 @@
//
// Covers: the beat length of all 39 divisions against a literal rung table (NOT the module's
// own exponent formula); the 1/64 and 64/1 extremes; the four named example divisions; the
// label notation; picker order and index round-trip; off-ladder clamping of BOTH persisted
// fields, measured through the readers rather than by comparing two clamped values; and
// shortestDivisionAtLeast's never-short contract across the rung boundaries.
// label notation; picker order and index round-trip; and off-ladder clamping of BOTH persisted
// fields, measured through the readers rather than by comparing two clamped values.
#include "../src/core/instrument/note/musical_division.h"
#include <cmath>
#include <cstdio>
#include <limits>
using namespace reasampler::instrument::note;
@@ -206,54 +204,7 @@ static void testOutOfRangeIndexClampsIntoTheSet() {
CHECK(divisionAt(kDivisionCount) == divisionAt(kDivisionCount - 1));
}
// --- shortestDivisionAtLeast --------------------------------------------------
// The contract is never-short: whatever comes back is at least the requested length, and
// nothing shorter on the ladder also is. Swept over every rung and over the gaps between them.
static void testShortestAtLeastIsNeverShortAndNeverLonger() {
for (int i = 0; i < kDivisionCount; ++i) {
const double target = divisionBeats(divisionAt(i));
for (const double want : {target, target * 0.99, target * 0.5}) {
const Division got = shortestDivisionAtLeast(want);
CHECK(divisionBeats(got) >= want - 1e-12);
// Nothing on the ladder is both long enough and shorter than the answer.
for (int j = 0; j < kDivisionCount; ++j) {
const double other = divisionBeats(divisionAt(j));
CHECK(!(other >= want && other < divisionBeats(got) - 1e-12));
}
}
}
}
// Picker order is NOT length order — a rung's triplet is shorter than the previous rung's
// dotted — so the answer is not simply "the next index up".
static void testShortestAtLeastCrossesRungBoundaries() {
// 5 beats: the 2/1 triplet (8 * 2/3 == 5.33) beats the dotted half (6) above it.
CHECK(shortestDivisionAtLeast(5.0) == makeDivision(3, DivisionModifier::Triplet));
// An exact rung length answers with that rung, not the one above it.
CHECK(shortestDivisionAtLeast(4.0) == makeDivision(2, DivisionModifier::Straight));
}
// Degenerate inputs land on an end rather than anywhere in the middle.
static void testShortestAtLeastDegenerateInputs() {
CHECK(shortestDivisionAtLeast(0.0) == divisionAt(0));
CHECK(shortestDivisionAtLeast(-10.0) == divisionAt(0));
// Past the longest programmable note (the dotted top rung), the top rung is the answer.
const Division longest = makeDivision(kMaxQuarterExponent, DivisionModifier::Dotted);
CHECK(almostEqual(divisionBeats(longest), kMaxDivisionBeats));
CHECK(shortestDivisionAtLeast(kMaxDivisionBeats * 2.0) == longest);
// A non-finite request names no length to be at least, so it takes the SAME never-short
// answer as one nothing covers. Landing on the bottom rung instead would turn a corrupt
// value into a near-instant note.
CHECK(shortestDivisionAtLeast(std::nan("")) == longest);
CHECK(shortestDivisionAtLeast(std::numeric_limits<double>::infinity()) == longest);
}
int main() {
testShortestAtLeastIsNeverShortAndNeverLonger();
testShortestAtLeastCrossesRungBoundaries();
testShortestAtLeastDegenerateInputs();
testLadderSpansSixtyfourthToSixtyFourWhole();
testEveryStraightRungHasItsWrittenBeatLength();
testDottedIsHalfAgainAndTripletIsTwoThirds();
+4 -2
View File
@@ -219,8 +219,10 @@ static void testAnExactLengthCarriesDurationsPastTheLaddersTopRung() {
const Tempo t = at(120.0);
const double pastTheLadder = t.beatsToSeconds(kMaxDivisionBeats) * 3.0;
CHECK(almostEqual(noteLengthSeconds(lengthOfSeconds(pastTheLadder), t), pastTheLadder));
// …and the ladder really does saturate there, which is what makes the seam load-bearing.
CHECK(almostEqual(divisionBeats(shortestDivisionAtLeast(kMaxDivisionBeats * 3.0)),
// …and the ladder really does saturate there, which is what makes the seam load-bearing:
// its longest rung (the dotted top) IS kMaxDivisionBeats, so nothing on it reaches
// pastTheLadder.
CHECK(almostEqual(divisionBeats(makeDivision(kMaxQuarterExponent, DivisionModifier::Dotted)),
kMaxDivisionBeats));
}
+30
View File
@@ -1380,6 +1380,34 @@ static void testTriggerEdgeCases() {
}
}
// --- Trigger out-of-domain lengthFraction: Voice::start's inline copy of trigger_seam's
// formula (map/trigger_seam.h) must clamp the same way the shared formula does — a
// frac > 1.0 never plays past the post-start span, and a NaN frees immediately rather than
// reaching the round()/static_cast<int64_t> undefined on a non-finite value. Pins the second
// implementation directly; test_trigger_seam.cpp pins the first.
static void testTriggerFracAboveOneClampsToSpan() {
// 150% length on a 200-frame sample -> clamped to the full 200-frame post-start span,
// not 300 frames (which would read off the end of the PCM).
SampleData km = (triggerSample(200, 1.5, 0, 0));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
eng.render(out, 220);
for (std::size_t i = 0; i < 200; ++i) CHECK(out[i] > 0.5f);
for (std::size_t i = 200; i < 220; ++i) CHECK(approx(out[i], 0.0, 1e-6));
CHECK(eng.activeVoiceCount() == 0); // frees exactly at frameCount
}
static void testTriggerFracNaNFreesImmediately() {
SampleData km = (triggerSample(200, std::nan(""), 5, 5));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
eng.render(out, 50);
for (float v : out) CHECK(approx(v, 0.0, 1e-6));
CHECK(eng.activeVoiceCount() == 0); // playLen 0 -> frees at start, no sound
}
// --- Trigger ignores note-off (S15): the one-shot plays through regardless. ---
static void testTriggerIgnoresNoteOff() {
SampleData km = (triggerSample(200, 0.5, 0, 0));
@@ -2908,6 +2936,8 @@ int main() {
testTriggerLengthWithStart();
testTriggerAhdFadeShape();
testTriggerEdgeCases();
testTriggerFracAboveOneClampsToSpan();
testTriggerFracNaNFreesImmediately();
testTriggerIgnoresNoteOff();
// S16 — pitch engine + pitch envelope.