loop: crossfade the Gate sustain seam, and unshadow the loop handles that made loop points look gone
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// component_state_io — the ComponentState ENVELOPE codec. See component_state_io.h for both
|
||||
// format ladders (envelope v1..v11, params payload v1..v10); the payload half lives in
|
||||
// format ladders (envelope v1..v11, params payload v1..v11); the payload half lives in
|
||||
// params_payload, which grows on its own version axis. Every wire format is FROZEN —
|
||||
// byte-identical across revisions.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// own links are velocity_curve + master_gain (wire value validation), never the engine.
|
||||
//
|
||||
// EVERY wire format below is FROZEN; the full version ladders (envelope v1..v11, params
|
||||
// payload v1..v10) must be preserved exactly. This header is the ONE home for both ladders
|
||||
// payload v1..v11) must be preserved exactly. This header is the ONE home for both ladders
|
||||
// and every version constant; the payload half is IMPLEMENTED in params_payload.
|
||||
|
||||
#include <cstdint>
|
||||
@@ -78,17 +78,21 @@ namespace reasampler::instrument::map {
|
||||
// the filter's OWN velocity curve (count + points, same shape as v7's). A v8 blob is a strict
|
||||
// prefix, so it lifts to the off/neutral filter default and plays bit-identically.
|
||||
//
|
||||
// v10 (CURRENT WRITE FORMAT) is v9 PLUS the staged-curve tail, appended after the filter's
|
||||
// velocity curve, all 8-byte LE doubles in this order: amp AHDSR attack/decay/release curve
|
||||
// v10 is v9 PLUS the staged-curve tail, appended after the filter's velocity curve, all
|
||||
// 8-byte LE doubles in this order: amp AHDSR attack/decay/release curve
|
||||
// exponents; the Trigger amp AHD (attack SECONDS, decay SECONDS, hold FRACTION, attack curve,
|
||||
// decay curve); the pitch envelope's hold FRACTION + attack/decay curve exponents; the filter
|
||||
// AHDSR's attack/decay/release curve exponents; the filter's Trigger AHD (same five fields as
|
||||
// the amp's). A v9-or-older blob is a strict prefix and lifts to the neutral exponent 1.0.
|
||||
//
|
||||
// v11 (CURRENT WRITE FORMAT) is v10 PLUS one 8-byte LE int64: the loop crossfade in SOURCE
|
||||
// frames (a source-timeline quantity like the loop points, so no rate resolves it). A v10-or-
|
||||
// older blob is a strict prefix and lifts to 0 — the hard seam it always played.
|
||||
//
|
||||
// The two int64 slots the v5 play tail spends on the RETIRED Trigger fade pair are frozen in
|
||||
// shape and still read: a pre-v10 blob's fade-in/fade-out become the Trigger AHD that replaced
|
||||
// them (attack <- fade-in, decay <- fade-out, hold <- the whole remainder), converted to
|
||||
// seconds at the project rate the reader is handed. v10 writes ZERO into both — the values
|
||||
// seconds at the project rate the reader is handed. v10+ writes ZERO into both — the values
|
||||
// live in the AHD now, so a DOWNGRADE to a pre-v10 binary loses the Trigger amp shape.
|
||||
//
|
||||
// LOSSY UNDER A RATE MISMATCH. The fades were SOURCE frames and the AHD stores wall-clock
|
||||
@@ -116,7 +120,7 @@ inline constexpr std::uint32_t kPerformanceStateVersion = 2;
|
||||
// The params-payload format version and its detection marker. The marker is a high sentinel
|
||||
// no legitimate v1 zone count (bounded by 128 MIDI zones, always tiny) could ever equal, so
|
||||
// a reader detects record shape independent of the envelope version.
|
||||
inline constexpr std::uint32_t kParamsPayloadVersion = 10; // v9 + the staged-curve tail
|
||||
inline constexpr std::uint32_t kParamsPayloadVersion = 11; // v10 + the loop-crossfade tail
|
||||
inline constexpr std::uint32_t kParamsFormatMarker = 0xFFFFFF00u;
|
||||
|
||||
// The first SINGLE-RECORD payload version. Everything below it is a retired zone list and
|
||||
@@ -132,6 +136,9 @@ inline constexpr std::uint32_t kParamsFilterVersion = 9;
|
||||
// v9 + the staged-curve tail (curve exponents, the Trigger AHDs, the pitch Hold fraction).
|
||||
inline constexpr std::uint32_t kParamsCurveVersion = 10;
|
||||
|
||||
// v10 + the loop-crossfade frame count.
|
||||
inline constexpr std::uint32_t kParamsLoopVersion = 11;
|
||||
|
||||
// (No nominal-rate constant.) The legacy v3 payload's wall-clock frame counts convert to
|
||||
// seconds at the v3 read boundary using the PROJECT sample rate threaded in as a parameter
|
||||
// (frames / projectRate = seconds) — the same rate the build already receives, so the
|
||||
|
||||
@@ -319,6 +319,8 @@ void putParamsPayload(std::vector<std::uint8_t>& out, const InstrumentParams& p)
|
||||
putLE(out, doubleToBits(f.env.decayCurve));
|
||||
putLE(out, doubleToBits(f.env.releaseCurve));
|
||||
putAhd(out, f.trigEnv);
|
||||
// v11: the loop crossfade, in SOURCE frames.
|
||||
putLE(out, asU64(p.loopCrossfadeFrames));
|
||||
}
|
||||
|
||||
// Read whichever payload shape follows: the single-record shape (v8 onward, growing by
|
||||
@@ -351,6 +353,12 @@ PayloadRead readParamsPayload(ByteReader& r, double projectRate) {
|
||||
readCurveTail(r, p.velocityCurve);
|
||||
if (pv >= kParamsFilterVersion) readFilterTail(r, p);
|
||||
if (pv >= kParamsCurveVersion) readCurveStageTail(r, p);
|
||||
if (pv >= kParamsLoopVersion) {
|
||||
// A negative fade is meaningless and would reach resolveLoop's clamp anyway; refusing
|
||||
// it here keeps the parameter set itself sane for the editor that reads it back.
|
||||
const std::int64_t xf = r.i64();
|
||||
p.loopCrossfadeFrames = xf > 0 ? xf : 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{};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// responsibilities. An INTERNAL seam of `component_state_io` — the public entry points stay
|
||||
// serialize/deserializeComponentState; nothing outside the codec calls these.
|
||||
//
|
||||
// The format ladder (payload v1..v10) is documented in component_state_io.h, which stays its
|
||||
// The format ladder (payload v1..v11) is documented in component_state_io.h, which stays its
|
||||
// one home. EVERY wire format is FROZEN.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -270,6 +270,7 @@ ResolvedCapture resolveCapture(const SelectedSample& ref, const InstrumentParams
|
||||
// The override wins over the intrinsic; absent -> intrinsic (loop) / frame 0 (start).
|
||||
// The bank is never mutated.
|
||||
rs.loop = params.loopOverride ? *params.loopOverride : ref.loop;
|
||||
rs.loopCrossfadeFrames = params.loopCrossfadeFrames;
|
||||
rs.startFrame = params.startPoint ? *params.startPoint : 0;
|
||||
rs.play = params.play; // SECONDS; buildSampleData resolves to frames
|
||||
return rs;
|
||||
@@ -306,6 +307,7 @@ SampleData buildSampleData(const ResolvedCapture& resolved, DecodedPcm decoded)
|
||||
data.sampleRate = decoded.sampleRate;
|
||||
data.rootNote = resolved.rootNote;
|
||||
data.loop = resolved.loop;
|
||||
data.loopCrossfadeFrames = resolved.loopCrossfadeFrames;
|
||||
data.startFrame = resolved.startFrame;
|
||||
data.keyTrack = resolved.keyTrack;
|
||||
data.velocityCurve = resolved.velocityCurve;
|
||||
|
||||
@@ -227,6 +227,12 @@ struct InstrumentParams {
|
||||
std::optional<SampleLoop> loopOverride; // instrument-owned sustain loop; absent -> intrinsic
|
||||
std::optional<std::int64_t> startPoint; // instrument-owned initial read frame; absent -> 0
|
||||
|
||||
// Pre-seam crossfade at the loop reset, in SOURCE frames — a source-timeline quantity
|
||||
// like the loop points, so it needs no rate to resolve and cannot be rescaled by a
|
||||
// project/file rate mismatch. 0 is the hard seam a blob predating the field lifts to.
|
||||
// Never a bank fact: the fade is a performance choice, the loop points are the file's.
|
||||
std::int64_t loopCrossfadeFrames = 0;
|
||||
|
||||
// Key-tracking scalar: how far playback pitch tracks the keyboard around the root. 1.0
|
||||
// (100%, standard 12-tone-ET) is the default — a blob predating this field lifts to
|
||||
// exactly 1.0, so already-saved instances are bit-identical. 0.0 = no tracking (every
|
||||
@@ -259,6 +265,7 @@ struct ResolvedCapture {
|
||||
double keyTrack = 1.0;
|
||||
VelocityCurve velocityCurve = VelocityCurve::flat();
|
||||
SampleLoop loop; // effective: loopOverride, else bank intrinsic
|
||||
std::int64_t loopCrossfadeFrames = 0; // instrument-owned; no bank intrinsic to beat
|
||||
std::int64_t startFrame = 0; // effective initial read frame: startPoint, else 0
|
||||
PlaySeconds play; // stored SECONDS; resolved to frames at build
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user