instrument: spline EGs — hard points on the one shared spline, a drawn contour per envelope beside its staged state, payload v13
This commit is contained in:
@@ -50,6 +50,27 @@ void putCurve(std::vector<std::uint8_t>& out, const VelocityCurve& curve) {
|
||||
}
|
||||
}
|
||||
|
||||
// A spline EG: 1 byte mode, then the contour as count + (x, y, hard) per point. Distinct from
|
||||
// putCurve because the three velocity-curve blocks are frozen at 16 bytes/point and cannot grow
|
||||
// the hard flag; this block was born with it.
|
||||
void putSplineEnv(std::vector<std::uint8_t>& out, const SplineEnv& s) {
|
||||
out.push_back(s.mode == EnvMode::Spline ? 1 : 0);
|
||||
const std::vector<VelocityPoint>& pts = s.contour.points();
|
||||
putLE(out, static_cast<std::uint32_t>(pts.size()));
|
||||
for (const VelocityPoint& pt : pts) {
|
||||
putLE(out, doubleToBits(pt.velocity));
|
||||
putLE(out, doubleToBits(pt.value));
|
||||
out.push_back(pt.hard ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
// The hard flags of an already-written velocity curve: count + one byte per point.
|
||||
void putHardFlags(std::vector<std::uint8_t>& out, const VelocityCurve& curve) {
|
||||
const std::vector<VelocityPoint>& pts = curve.points();
|
||||
putLE(out, static_cast<std::uint32_t>(pts.size()));
|
||||
for (const VelocityPoint& pt : pts) out.push_back(pt.hard ? 1 : 0);
|
||||
}
|
||||
|
||||
// A stored AHD's five doubles, in one order shared by every AHD on the wire.
|
||||
void putAhd(std::vector<std::uint8_t>& out, const AhdSeconds& a) {
|
||||
putLE(out, doubleToBits(a.attackSeconds));
|
||||
@@ -114,6 +135,41 @@ void readCurveTail(ByteReader& r, VelocityCurve& curve,
|
||||
}
|
||||
}
|
||||
|
||||
// Read a spline EG. A truncated read leaves `s` at its Staged/default-contour construction
|
||||
// value, which is what makes a pre-v13 blob play exactly as it did.
|
||||
void readSplineEnv(ByteReader& r, SplineEnv& s) {
|
||||
const bool spline = (r.u8() != 0);
|
||||
const std::uint32_t ptCount = r.u32();
|
||||
std::vector<VelocityPoint> pts;
|
||||
// Bound the reserve to what the blob can hold (17 bytes/point) so a corrupt huge count
|
||||
// can't trigger a giant allocation before the bounded reads fail.
|
||||
const std::size_t remaining = r.bytes.size() > r.pos ? r.bytes.size() - r.pos : 0;
|
||||
pts.reserve(std::min(static_cast<std::size_t>(ptCount), remaining / 17));
|
||||
for (std::uint32_t i = 0; i < ptCount && r.ok; ++i) {
|
||||
const double x = bitsToDouble(r.u64());
|
||||
const double y = bitsToDouble(r.u64());
|
||||
const bool hard = (r.u8() != 0);
|
||||
pts.push_back(VelocityPoint{x, y, hard});
|
||||
}
|
||||
if (!r.ok) return;
|
||||
s.mode = spline ? EnvMode::Spline : EnvMode::Staged;
|
||||
s.contour = VelocityCurve::fromPoints(std::move(pts),
|
||||
reasampler::instrument::engine::CurveDomain::Unipolar);
|
||||
}
|
||||
|
||||
// Apply a hard-flag tail to an already-read velocity curve. A count that disagrees with the
|
||||
// curve fromPoints actually produced is dropped rather than applied to shifted knots.
|
||||
void readHardFlags(ByteReader& r, VelocityCurve& curve) {
|
||||
const std::uint32_t count = r.u32();
|
||||
const std::size_t remaining = r.bytes.size() > r.pos ? r.bytes.size() - r.pos : 0;
|
||||
if (count > remaining) { r.ok = false; return; }
|
||||
std::vector<std::uint8_t> flags;
|
||||
flags.reserve(count);
|
||||
for (std::uint32_t i = 0; i < count && r.ok; ++i) flags.push_back(r.u8());
|
||||
if (!r.ok || flags.size() != curve.size()) return;
|
||||
for (std::size_t i = 0; i < flags.size(); ++i) curve.setHard(i, flags[i] != 0);
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -331,6 +387,14 @@ void putParamsPayload(std::vector<std::uint8_t>& out, const InstrumentParams& p)
|
||||
putLE(out, asU64(p.loopCrossfadeFrames));
|
||||
// v12: the velocity->pitch curve.
|
||||
putCurve(out, pp.pitchVelocityCurve);
|
||||
// v13: the dual Staged/Spline state — the three contours, then the hard flags the three
|
||||
// frozen velocity-curve blocks above had no room for.
|
||||
putSplineEnv(out, pp.ampSpline);
|
||||
putSplineEnv(out, pp.pitchSpline);
|
||||
putSplineEnv(out, pp.filterSpline);
|
||||
putHardFlags(out, p.velocityCurve);
|
||||
putHardFlags(out, f.velocityCurve);
|
||||
putHardFlags(out, pp.pitchVelocityCurve);
|
||||
}
|
||||
|
||||
// Read whichever payload shape follows: the single-record shape (v8 onward, growing by
|
||||
@@ -373,6 +437,14 @@ PayloadRead readParamsPayload(ByteReader& r, double projectRate) {
|
||||
readCurveTail(r, p.play.pitchVelocityCurve,
|
||||
reasampler::instrument::engine::CurveDomain::Bipolar);
|
||||
}
|
||||
if (pv >= kParamsSplineVersion) {
|
||||
readSplineEnv(r, p.play.ampSpline);
|
||||
readSplineEnv(r, p.play.pitchSpline);
|
||||
readSplineEnv(r, p.play.filterSpline);
|
||||
readHardFlags(r, p.velocityCurve);
|
||||
readHardFlags(r, p.play.filter.velocityCurve);
|
||||
readHardFlags(r, p.play.pitchVelocityCurve);
|
||||
}
|
||||
// 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{};
|
||||
|
||||
Reference in New Issue
Block a user