Bake window derives itself: %-knob fold, declick pad, Gate held to exhaustion, preview velocity; Hold is the one knob a loop needs

This commit is contained in:
2026-08-01 20:26:04 -04:00
parent d3894dae6d
commit 19aeb92775
36 changed files with 1040 additions and 274 deletions
@@ -193,6 +193,24 @@ void readHardFlags(ByteReader& r, VelocityCurve& curve) {
for (std::size_t i = 0; i < flags.size(); ++i) curve.setHard(i, flags[i] != 0);
}
// Read the v14 bake Hold. Same revive discipline as readHardFlags directly above, and for the
// same reason: this tail reaches no audio path, so a blob truncated inside it must cost the
// Hold alone and not reset the whole record that parsed cleanly ahead of it. It sits LAST, so
// a truncation stranding the hard flags strands this too — reviving in only one of the two
// would still wipe the record.
void readBakeHold(ByteReader& r, InstrumentParams& p) {
const bool enteredOk = r.ok;
const std::int32_t exponent = r.i32();
const std::uint8_t modifier = r.u8();
if (!r.ok) {
if (enteredOk) r.ok = true;
return;
}
// makeDivision clamps BOTH fields, so a corrupt pair becomes the nearest legal rung
// rather than an unrepresentable one — never a memcpy into the type.
p.bakeHold = note::makeDivision(exponent, static_cast<note::DivisionModifier>(modifier));
}
// Read the v9 filter tail into `p`. A blob that stops short leaves the off/neutral default,
// which is what makes a v8 blob play bit-identically under the new codec. The curve reads as
// bipolar at EVERY version — a pre-v12 blob's y values are already valid bipolar ones, so its
@@ -418,6 +436,11 @@ void putParamsPayload(std::vector<std::uint8_t>& out, const InstrumentParams& p)
putHardFlags(out, p.velocityCurve);
putHardFlags(out, f.velocityCurve);
putHardFlags(out, pp.pitchVelocityCurve);
// v14: the bake's Hold division, as its {quarterExponent, modifier} pair — never its
// picker index, which the ladder gaining a rung would silently re-map.
putLE(out, static_cast<std::uint32_t>(
static_cast<std::int32_t>(p.bakeHold.quarterExponent())));
out.push_back(static_cast<std::uint8_t>(p.bakeHold.modifier()));
}
// Read whichever payload shape follows: the single-record shape (v8 onward, growing by
@@ -468,6 +491,7 @@ PayloadRead readParamsPayload(ByteReader& r, double projectRate) {
readHardFlags(r, p.play.filter.velocityCurve);
readHardFlags(r, p.play.pitchVelocityCurve);
}
if (pv >= kParamsBakeHoldVersion) readBakeHold(r, p);
// A truncated record leaves whatever parsed plus construction defaults for the rest —
// the same degrade-don't-throw contract the zone ladder always had.
if (!r.ok) return PayloadRead{};