instrument: one VELOCITY deck for all three velocity curves, bipolar and off by default for pitch and filter
Payload v12 appends the new velocity->pitch curve and folds the retired filter velAmount into its now-bipolar curve, so pre-v12 projects reopen sounding identical. Preview button takes a drawn play triangle.
This commit is contained in:
@@ -38,14 +38,15 @@ void putOverrides(std::vector<std::uint8_t>& out, const InstrumentParams& p) {
|
||||
if (p.startPoint) putLE(out, asU64(*p.startPoint));
|
||||
}
|
||||
|
||||
// A velocity curve: 4-byte LE control-point count, then per point velocity + amp as doubles.
|
||||
// The amp curve (v7) and the filter's own curve (v9) share this shape.
|
||||
// A velocity curve: 4-byte LE control-point count, then per point velocity + value as doubles.
|
||||
// The amp curve (v7), the filter's own curve (v9) and the pitch curve (v12) share this shape;
|
||||
// the y DOMAIN is not on the wire — it is a property of the slot, so the reader supplies it.
|
||||
void putCurve(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) {
|
||||
putLE(out, doubleToBits(pt.velocity));
|
||||
putLE(out, doubleToBits(pt.amp));
|
||||
putLE(out, doubleToBits(pt.value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +93,12 @@ void readSecondsPlayTail(ByteReader& r, InstrumentParams& p, double projectRate)
|
||||
p.play.adsr.releaseSeconds = bitsToDouble(r.u64());
|
||||
}
|
||||
|
||||
// Read a velocity curve tail into `curve`. fromPoints repairs the X-order/endpoint invariant
|
||||
// defensively; a truncated read leaves `curve` at whatever default it came in with.
|
||||
void readCurveTail(ByteReader& r, VelocityCurve& curve) {
|
||||
// Read a velocity curve tail into `curve`, interpreting its y values in `domain` and scaling
|
||||
// them by `yScale` (the pre-v12 filter lift folds a retired depth in that way — see
|
||||
// component_state_io.h). fromPoints repairs the X-order/endpoint invariant defensively; a
|
||||
// truncated read leaves `curve` at whatever default it came in with.
|
||||
void readCurveTail(ByteReader& r, VelocityCurve& curve,
|
||||
reasampler::instrument::engine::CurveDomain domain, double yScale) {
|
||||
const std::uint32_t ptCount = r.u32();
|
||||
std::vector<VelocityPoint> pts;
|
||||
// Bound the reserve to what the blob can hold (16 bytes/point) so a corrupt huge count
|
||||
@@ -103,17 +107,19 @@ void readCurveTail(ByteReader& r, VelocityCurve& curve) {
|
||||
pts.reserve(std::min(static_cast<std::size_t>(ptCount), remaining / 16));
|
||||
for (std::uint32_t i = 0; i < ptCount && r.ok; ++i) {
|
||||
const double vel = bitsToDouble(r.u64());
|
||||
const double amp = bitsToDouble(r.u64());
|
||||
pts.push_back(VelocityPoint{vel, amp});
|
||||
const double value = bitsToDouble(r.u64());
|
||||
pts.push_back(VelocityPoint{vel, value * yScale});
|
||||
}
|
||||
if (r.ok) {
|
||||
curve = reasampler::instrument::engine::VelocityCurve::fromPoints(std::move(pts));
|
||||
curve = reasampler::instrument::engine::VelocityCurve::fromPoints(std::move(pts), domain);
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
void readFilterTail(ByteReader& r, InstrumentParams& p) {
|
||||
// which is what makes a v8 blob play bit-identically under the new codec. `preVelocityVersion`
|
||||
// selects the pre-v12 lift: the frozen velAmount slot is folded into the curve's knots instead
|
||||
// of being kept as a separate depth.
|
||||
void readFilterTail(ByteReader& r, InstrumentParams& p, bool preVelocityVersion) {
|
||||
FilterSeconds& f = p.play.filter;
|
||||
f.enabled = (r.u8() != 0);
|
||||
f.settings.cutoffNorm = static_cast<float>(bitsToDouble(r.u64()));
|
||||
@@ -122,20 +128,22 @@ void readFilterTail(ByteReader& r, InstrumentParams& p) {
|
||||
f.settings.driveNorm = static_cast<float>(bitsToDouble(r.u64()));
|
||||
f.settings.morphLaw = (r.u8() != 0) ? engine::filter::MorphLaw::HighNotchLow
|
||||
: engine::filter::MorphLaw::HighBandLow;
|
||||
// Same non-finite-falls-back-to-neutral guard as the v8 master gain above: these three
|
||||
// reach Voice::tickFilterCutoff's clamp compares and a static_cast<int>, both UB on NaN.
|
||||
// Same non-finite-falls-back-to-neutral guard as the v8 master gain above: these reach
|
||||
// Voice::tickFilterCutoff's clamp compares and a static_cast<int>, both UB on NaN.
|
||||
double modAmount = bitsToDouble(r.u64());
|
||||
double velAmount = bitsToDouble(r.u64());
|
||||
double keyTrack = bitsToDouble(r.u64());
|
||||
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());
|
||||
readCurveTail(r, f.velocityCurve);
|
||||
const double velFold =
|
||||
preVelocityVersion ? (std::isfinite(velAmount) ? velAmount : 0.0) : 1.0;
|
||||
readCurveTail(r, f.velocityCurve, reasampler::instrument::engine::CurveDomain::Bipolar,
|
||||
velFold);
|
||||
}
|
||||
|
||||
// A curve exponent off the wire. A corrupt/non-finite value degrades to the LINEAR neutral
|
||||
@@ -240,7 +248,10 @@ PayloadRead readLegacyZonePayload(ByteReader& r, std::uint32_t pv, double projec
|
||||
// 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());
|
||||
if (curveTail) readCurveTail(r, p.velocityCurve);
|
||||
if (curveTail) {
|
||||
readCurveTail(r, p.velocityCurve,
|
||||
reasampler::instrument::engine::CurveDomain::Unipolar, 1.0);
|
||||
}
|
||||
// Payload version 4 (a branch-only frames tail, never shipped) and any unknown pv
|
||||
// leave the seconds product defaults on p.play.
|
||||
if (!r.ok) break; // truncated mid-record -> keep what parsed cleanly, drop the rest
|
||||
@@ -299,7 +310,9 @@ void putParamsPayload(std::vector<std::uint8_t>& out, const InstrumentParams& p)
|
||||
putLE(out, doubleToBits(static_cast<double>(f.settings.driveNorm)));
|
||||
out.push_back(f.settings.morphLaw == engine::filter::MorphLaw::HighNotchLow ? 1 : 0);
|
||||
putLE(out, doubleToBits(f.modAmount));
|
||||
putLE(out, doubleToBits(f.velAmount));
|
||||
// The retired filter velAmount's frozen slot: a constant 1.0 so a pre-v12 binary reading
|
||||
// this blob scales the curve by unity rather than silencing it (see component_state_io.h).
|
||||
putLE(out, doubleToBits(1.0));
|
||||
putLE(out, doubleToBits(f.keyTrack));
|
||||
putLE(out, doubleToBits(f.env.attackSeconds));
|
||||
putLE(out, doubleToBits(f.env.holdSeconds));
|
||||
@@ -321,6 +334,8 @@ void putParamsPayload(std::vector<std::uint8_t>& out, const InstrumentParams& p)
|
||||
putAhd(out, f.trigEnv);
|
||||
// v11: the loop crossfade, in SOURCE frames.
|
||||
putLE(out, asU64(p.loopCrossfadeFrames));
|
||||
// v12: the velocity->pitch curve.
|
||||
putCurve(out, pp.pitchVelocityCurve);
|
||||
}
|
||||
|
||||
// Read whichever payload shape follows: the single-record shape (v8 onward, growing by
|
||||
@@ -350,8 +365,10 @@ PayloadRead readParamsPayload(ByteReader& r, double projectRate) {
|
||||
if (hasStart) p.startPoint = r.i64();
|
||||
readSecondsPlayTail(r, p, projectRate);
|
||||
p.keyTrack = bitsToDouble(r.u64());
|
||||
readCurveTail(r, p.velocityCurve);
|
||||
if (pv >= kParamsFilterVersion) readFilterTail(r, p);
|
||||
readCurveTail(r, p.velocityCurve, reasampler::instrument::engine::CurveDomain::Unipolar, 1.0);
|
||||
if (pv >= kParamsFilterVersion) {
|
||||
readFilterTail(r, p, /*preVelocityVersion=*/pv < kParamsVelocityVersion);
|
||||
}
|
||||
if (pv >= kParamsCurveVersion) readCurveStageTail(r, p);
|
||||
if (pv >= kParamsLoopVersion) {
|
||||
// A negative fade is meaningless and would reach resolveLoop's clamp anyway; refusing
|
||||
@@ -359,6 +376,10 @@ PayloadRead readParamsPayload(ByteReader& r, double projectRate) {
|
||||
const std::int64_t xf = r.i64();
|
||||
p.loopCrossfadeFrames = xf > 0 ? xf : 0;
|
||||
}
|
||||
if (pv >= kParamsVelocityVersion) {
|
||||
readCurveTail(r, p.play.pitchVelocityCurve,
|
||||
reasampler::instrument::engine::CurveDomain::Bipolar, 1.0);
|
||||
}
|
||||
// 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