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:
@@ -244,6 +244,7 @@ void readBakeHold(ByteReader& r, InstrumentParams& p) {
|
||||
// bipolar at EVERY version — a pre-v12 blob's y values are already valid bipolar ones, so its
|
||||
// v12 domain re-tag needs no version branch (see component_state_io.h).
|
||||
void readFilterTail(ByteReader& r, InstrumentParams& p) {
|
||||
const FilterSeconds fallback; // the construction defaults, read rather than restated
|
||||
FilterSeconds& f = p.play.filter;
|
||||
f.enabled = (r.u8() != 0);
|
||||
f.settings.cutoffNorm = static_cast<float>(bitsToDouble(r.u64()));
|
||||
@@ -260,11 +261,11 @@ void readFilterTail(ByteReader& r, InstrumentParams& p) {
|
||||
f.modAmount = std::isfinite(modAmount) ? modAmount : 0.0;
|
||||
f.velAmount = std::isfinite(velAmount) ? velAmount : 0.0;
|
||||
f.keyTrack = std::isfinite(keyTrack) ? keyTrack : 0.0;
|
||||
f.env.attackSeconds = bitsToDouble(r.u64());
|
||||
f.env.holdSeconds = bitsToDouble(r.u64());
|
||||
f.env.decaySeconds = bitsToDouble(r.u64());
|
||||
f.env.sustainLevel = bitsToDouble(r.u64());
|
||||
f.env.releaseSeconds = bitsToDouble(r.u64());
|
||||
f.env.attackSeconds = finiteOr(bitsToDouble(r.u64()), fallback.env.attackSeconds);
|
||||
f.env.holdSeconds = finiteOr(bitsToDouble(r.u64()), fallback.env.holdSeconds);
|
||||
f.env.decaySeconds = finiteOr(bitsToDouble(r.u64()), fallback.env.decaySeconds);
|
||||
f.env.sustainLevel = finiteOr(bitsToDouble(r.u64()), fallback.env.sustainLevel);
|
||||
f.env.releaseSeconds = finiteOr(bitsToDouble(r.u64()), fallback.env.releaseSeconds);
|
||||
readCurveTail(r, f.velocityCurve, reasampler::instrument::engine::CurveDomain::Bipolar);
|
||||
}
|
||||
|
||||
@@ -371,8 +372,10 @@ PayloadRead readLegacyZonePayload(ByteReader& r, std::uint32_t pv, double projec
|
||||
readSecondsPlayTail(r, p, projectRate);
|
||||
}
|
||||
// A pre-v6 payload leaves keyTrack = 1.0 (100% ET), so an already-saved instance
|
||||
// repitches BIT-IDENTICALLY. A pre-v7 payload leaves VelocityCurve::flat().
|
||||
if (keyTrackTail) p.keyTrack = bitsToDouble(r.u64());
|
||||
// repitches BIT-IDENTICALLY. A pre-v7 payload leaves VelocityCurve::flat(). A NaN
|
||||
// reaches keyTrackedRatio -> baseRatio_ -> readPos_'s per-sample cast (voice.h) — the
|
||||
// same guard the v8+ single-record reader applies to its own keyTrack below.
|
||||
if (keyTrackTail) p.keyTrack = finiteOr(bitsToDouble(r.u64()), InstrumentParams{}.keyTrack);
|
||||
if (curveTail) {
|
||||
readCurveTail(r, p.velocityCurve,
|
||||
reasampler::instrument::engine::CurveDomain::Unipolar);
|
||||
@@ -500,7 +503,10 @@ PayloadRead readParamsPayload(ByteReader& r, double projectRate) {
|
||||
const std::uint8_t hasStart = r.u8();
|
||||
if (hasStart) p.startPoint = r.i64();
|
||||
readSecondsPlayTail(r, p, projectRate);
|
||||
p.keyTrack = bitsToDouble(r.u64());
|
||||
// NaN reaches keyTrackedRatio (voice.h) -> baseRatio_ -> readPos_'s per-sample
|
||||
// static_cast<std::int64_t> — UB on the per-sample path. Guarded here, codec-side, so
|
||||
// that path needs no check of its own.
|
||||
p.keyTrack = finiteOr(bitsToDouble(r.u64()), InstrumentParams{}.keyTrack);
|
||||
readCurveTail(r, p.velocityCurve, reasampler::instrument::engine::CurveDomain::Unipolar);
|
||||
if (pv >= kParamsFilterVersion) readFilterTail(r, p);
|
||||
if (pv >= kParamsCurveVersion) readCurveStageTail(r, p);
|
||||
|
||||
@@ -54,27 +54,6 @@ int divisionIndex(Division d) {
|
||||
+ static_cast<int>(d.modifier());
|
||||
}
|
||||
|
||||
Division shortestDivisionAtLeast(double beats) {
|
||||
// Picker order is NOT length order — a rung's triplet is shorter than the previous rung's
|
||||
// dotted — so both the fit and the fallback are found by scanning. 39 entries.
|
||||
Division longest = divisionAt(0);
|
||||
double longestBeats = divisionBeats(longest);
|
||||
Division best = longest;
|
||||
double bestBeats = 0.0;
|
||||
bool found = false;
|
||||
for (int i = 0; i < kDivisionCount; ++i) {
|
||||
const Division d = divisionAt(i);
|
||||
const double b = divisionBeats(d);
|
||||
if (b > longestBeats) { longest = d; longestBeats = b; }
|
||||
if (b >= beats && (!found || b < bestBeats)) { best = d; bestBeats = b; found = true; }
|
||||
}
|
||||
// NaN names no length to be at least, so it takes the never-short direction rather than
|
||||
// the bottom rung: a corrupt value must not resolve to a near-instant note.
|
||||
if (std::isnan(beats)) return longest;
|
||||
if (!(beats > 0.0)) return divisionAt(0);
|
||||
return found ? best : longest;
|
||||
}
|
||||
|
||||
std::string divisionLabel(Division d) {
|
||||
const int e = d.quarterExponent();
|
||||
// Both branches meet at e == 2 ("1/1"): a division's written form is its length in
|
||||
|
||||
@@ -69,13 +69,6 @@ double divisionBeats(Division d);
|
||||
Division divisionAt(int index);
|
||||
int divisionIndex(Division d);
|
||||
|
||||
// The shortest ladder length that is at least `beats` — for a caller quantizing a duration ONTO
|
||||
// the ladder, where overshooting is the safe direction. It is NOT how a derived duration reaches
|
||||
// the bake: nothing on a finite ladder covers an arbitrarily long source (see note/CLAUDE.md).
|
||||
// Nothing long enough, or a non-finite `beats`, yields the top rung; a non-positive one yields
|
||||
// the bottom.
|
||||
Division shortestDivisionAtLeast(double beats);
|
||||
|
||||
// The notation divisions are named in: "1/16", "1/8.", "1/4t", "4/1".
|
||||
std::string divisionLabel(Division d);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user