Bake window: derived note lengths carry exact durations, not ladder rungs — a long take is no longer cut at 384 beats
Hold keeps its picker. Also: one home for the %-fold, duration-ordered Hold travel, and a corrupt tail degrades to absent rather than fabricating one.
This commit is contained in:
@@ -2,9 +2,9 @@ reasampler_pure_library(bridge_marshal SOURCES bridge_marshal.cpp)
|
||||
reasampler_test(bridge_marshal LINK bridge_marshal)
|
||||
|
||||
reasampler_pure_library(trigger_seam SOURCES trigger_seam.cpp)
|
||||
# The %-length fold lives beside the span formula, so the header reads play_params' value
|
||||
# layer; the test links that set and nothing else — still no engine, no editor geometry.
|
||||
reasampler_test(trigger_seam LINK trigger_seam velocity_curve peaks filter curve_law)
|
||||
# Plain frame arithmetic over doubles: no engine, no value layer, no editor geometry. The
|
||||
# %-length fold it used to host lives with its siblings in play_params.h.
|
||||
reasampler_test(trigger_seam LINK trigger_seam)
|
||||
|
||||
reasampler_pure_library(bank_sync
|
||||
SOURCES bank_sync.cpp
|
||||
|
||||
@@ -94,24 +94,41 @@ void liftTriggerFades(std::int64_t fadeInFrames, std::int64_t fadeOutFrames, dou
|
||||
out.decayCurve = kTriggerFadeLiftDecayCurve;
|
||||
}
|
||||
|
||||
// A wire double whose consumers assume a domain they cannot check: the seconds fields reach
|
||||
// resolvePlay's static_cast<std::int64_t> and peakSemitones reaches the bake's pow() and the
|
||||
// voice's ratio multiply — both undefined or poisoning on NaN. Degrades to the field's own
|
||||
// construction default, so a damaged blob loses that field rather than the record.
|
||||
double finiteOr(double v, double fallback) { return std::isfinite(v) ? v : fallback; }
|
||||
|
||||
// A root-note override off the wire. Clamped HERE and not only where it is consumed: planBake
|
||||
// clamps the note it renders at into MIDI range while the SampleData keeps the raw override as
|
||||
// its root, and the two disagreeing makes the read rate something other than 1 — which
|
||||
// mis-sizes the bake's window in the truncating direction.
|
||||
int clampMidiNote(int note) { return (std::max)(0, (std::min)(127, note)); }
|
||||
|
||||
// Read the play tail (v5 shape onward) into `p`. Shared by the legacy zone reader and the
|
||||
// v8 single-record reader so the two can never disagree about field order.
|
||||
void readSecondsPlayTail(ByteReader& r, InstrumentParams& p, double projectRate) {
|
||||
const PlaySeconds fallback; // the construction defaults, read rather than restated
|
||||
p.play.playMode = (r.u8() != 0) ? PlayMode::Trigger : PlayMode::Gate;
|
||||
p.play.adsr.holdSeconds = bitsToDouble(r.u64());
|
||||
p.play.trigger.lengthFraction = bitsToDouble(r.u64());
|
||||
p.play.adsr.holdSeconds = finiteOr(bitsToDouble(r.u64()), fallback.adsr.holdSeconds);
|
||||
p.play.trigger.lengthFraction =
|
||||
finiteOr(bitsToDouble(r.u64()), fallback.trigger.lengthFraction);
|
||||
const std::int64_t fadeIn = r.i64();
|
||||
const std::int64_t fadeOut = r.i64();
|
||||
liftTriggerFades(fadeIn, fadeOut, projectRate, p.play.trigAhd);
|
||||
p.play.pitchEngine = (r.u8() != 0) ? PitchEngine::Preserve : PitchEngine::Varispeed;
|
||||
p.play.pitchEnv.enabled = (r.u8() != 0);
|
||||
p.play.pitchEnv.shape.attackSeconds = bitsToDouble(r.u64());
|
||||
p.play.pitchEnv.shape.decaySeconds = bitsToDouble(r.u64());
|
||||
p.play.pitchEnv.peakSemitones = bitsToDouble(r.u64());
|
||||
p.play.adsr.attackSeconds = bitsToDouble(r.u64());
|
||||
p.play.adsr.decaySeconds = bitsToDouble(r.u64());
|
||||
p.play.adsr.sustainLevel = bitsToDouble(r.u64());
|
||||
p.play.adsr.releaseSeconds = bitsToDouble(r.u64());
|
||||
p.play.pitchEnv.shape.attackSeconds =
|
||||
finiteOr(bitsToDouble(r.u64()), fallback.pitchEnv.shape.attackSeconds);
|
||||
p.play.pitchEnv.shape.decaySeconds =
|
||||
finiteOr(bitsToDouble(r.u64()), fallback.pitchEnv.shape.decaySeconds);
|
||||
p.play.pitchEnv.peakSemitones =
|
||||
finiteOr(bitsToDouble(r.u64()), fallback.pitchEnv.peakSemitones);
|
||||
p.play.adsr.attackSeconds = finiteOr(bitsToDouble(r.u64()), fallback.adsr.attackSeconds);
|
||||
p.play.adsr.decaySeconds = finiteOr(bitsToDouble(r.u64()), fallback.adsr.decaySeconds);
|
||||
p.play.adsr.sustainLevel = finiteOr(bitsToDouble(r.u64()), fallback.adsr.sustainLevel);
|
||||
p.play.adsr.releaseSeconds = finiteOr(bitsToDouble(r.u64()), fallback.adsr.releaseSeconds);
|
||||
}
|
||||
|
||||
// Read a velocity curve tail into `curve`, interpreting its y values in `domain` — the domain
|
||||
@@ -170,6 +187,14 @@ void readSplineEnv(ByteReader& r, SplineEnv& s) {
|
||||
reasampler::instrument::engine::CurveDomain::Unipolar);
|
||||
}
|
||||
|
||||
// A block whose declared length overruns what the blob still holds has no realignment point:
|
||||
// every byte after it belongs to a block that was truncated, so a later tail that reads them
|
||||
// as its own gets an ARBITRARY value — and a tail that clamps (the bake Hold does) turns that
|
||||
// into a legal-looking fabrication rather than an obvious one. Draining is what makes the
|
||||
// stream's end honest: each later tail then reads past it and degrades to absent through its
|
||||
// own revive, while the record that parsed cleanly ahead of the damage survives.
|
||||
void drainUnaligned(ByteReader& r) { r.pos = r.bytes.size(); }
|
||||
|
||||
// Apply a hard-flag tail to an already-read velocity curve. A count that disagrees with the
|
||||
// curve fromPoints actually produced — including an out-of-bounds or truncated one — is
|
||||
// dropped rather than applied to shifted knots, and the whole params record parsed ahead of
|
||||
@@ -185,7 +210,10 @@ void readHardFlags(ByteReader& r, VelocityCurve& curve) {
|
||||
return;
|
||||
}
|
||||
const std::size_t remaining = r.bytes.size() > r.pos ? r.bytes.size() - r.pos : 0;
|
||||
if (count > remaining) return; // bound-and-skip: cannot safely reserve/read this many
|
||||
if (count > remaining) {
|
||||
drainUnaligned(r); // the flags this count promised are not all there
|
||||
return;
|
||||
}
|
||||
std::vector<std::uint8_t> flags;
|
||||
flags.reserve(count);
|
||||
for (std::uint32_t i = 0; i < count; ++i) flags.push_back(r.u8());
|
||||
@@ -306,7 +334,7 @@ PayloadRead readLegacyZonePayload(ByteReader& r, std::uint32_t pv, double projec
|
||||
r.i32(); // lowNote — the retired key range; read to keep the record walk aligned
|
||||
r.i32(); // highNote
|
||||
const std::uint8_t hasOverride = r.u8();
|
||||
if (hasOverride) p.rootOverride = r.i32();
|
||||
if (hasOverride) p.rootOverride = clampMidiNote(r.i32());
|
||||
if (extended) {
|
||||
const std::uint8_t hasLoop = r.u8();
|
||||
if (hasLoop) {
|
||||
@@ -325,9 +353,11 @@ PayloadRead readLegacyZonePayload(ByteReader& r, std::uint32_t pv, double projec
|
||||
// are source-timeline, read as-is. A/D/S/R are ABSENT in v3 -> keep the defaults.
|
||||
assert(projectRate > 0.0 && "readLegacyZonePayload: projectRate must be > 0 for v3 lift");
|
||||
const double liftRate = projectRate > 0.0 ? projectRate : 1.0; // avoids div-by-zero; assert fires first
|
||||
const PlaySeconds fallback; // same guard as readSecondsPlayTail's peer fields
|
||||
p.play.playMode = (r.u8() != 0) ? PlayMode::Trigger : PlayMode::Gate;
|
||||
p.play.adsr.holdSeconds = static_cast<double>(r.i64()) / liftRate;
|
||||
p.play.trigger.lengthFraction = bitsToDouble(r.u64());
|
||||
p.play.trigger.lengthFraction =
|
||||
finiteOr(bitsToDouble(r.u64()), fallback.trigger.lengthFraction);
|
||||
const std::int64_t fadeIn = r.i64();
|
||||
const std::int64_t fadeOut = r.i64();
|
||||
liftTriggerFades(fadeIn, fadeOut, liftRate, p.play.trigAhd);
|
||||
@@ -335,7 +365,8 @@ PayloadRead readLegacyZonePayload(ByteReader& r, std::uint32_t pv, double projec
|
||||
p.play.pitchEnv.enabled = (r.u8() != 0);
|
||||
p.play.pitchEnv.shape.attackSeconds = static_cast<double>(r.i64()) / liftRate;
|
||||
p.play.pitchEnv.shape.decaySeconds = static_cast<double>(r.i64()) / liftRate;
|
||||
p.play.pitchEnv.peakSemitones = bitsToDouble(r.u64());
|
||||
p.play.pitchEnv.peakSemitones =
|
||||
finiteOr(bitsToDouble(r.u64()), fallback.pitchEnv.peakSemitones);
|
||||
} else if (secondsPlay) {
|
||||
readSecondsPlayTail(r, p, projectRate);
|
||||
}
|
||||
@@ -457,7 +488,7 @@ PayloadRead readParamsPayload(ByteReader& r, double projectRate) {
|
||||
PayloadRead out;
|
||||
InstrumentParams& p = out.params;
|
||||
const std::uint8_t hasRoot = r.u8();
|
||||
if (hasRoot) p.rootOverride = r.i32();
|
||||
if (hasRoot) p.rootOverride = clampMidiNote(r.i32());
|
||||
const std::uint8_t hasLoop = r.u8();
|
||||
if (hasLoop) {
|
||||
SampleLoop lp;
|
||||
|
||||
@@ -10,8 +10,10 @@ std::int64_t triggerPlayLength(double lengthFraction,
|
||||
std::int64_t frameCount,
|
||||
std::int64_t startFrame) {
|
||||
const std::int64_t postStart = (std::max)(std::int64_t{0}, frameCount - startFrame);
|
||||
if (postStart <= 0 || lengthFraction <= 0.0) return 0;
|
||||
return static_cast<std::int64_t>(lengthFraction * static_cast<double>(postStart) + 0.5);
|
||||
if (postStart <= 0 || !(lengthFraction > 0.0)) return 0; // also catches NaN
|
||||
const double frac = (std::min)(1.0, lengthFraction);
|
||||
const auto len = static_cast<std::int64_t>(frac * static_cast<double>(postStart) + 0.5);
|
||||
return (std::min)(postStart, (std::max)(std::int64_t{0}, len));
|
||||
}
|
||||
|
||||
} // namespace reasampler::instrument::map
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
// trigger_seam — the shared Trigger play-span formula: how the stored %-length becomes the
|
||||
// source-frame span the voice plays, the overlay draws over, and the bake's window holds.
|
||||
// One home so those three cannot disagree about where a Trigger note ends.
|
||||
// trigger_seam — the shared Trigger play-span formula: how a %-length becomes the source-frame
|
||||
// span the voice plays, the overlay draws over, and the bake's window holds. One home so those
|
||||
// cannot disagree about where a Trigger note ends.
|
||||
//
|
||||
// playLengthFrames = round(effectiveLengthFraction * (frameCount - startFrame))
|
||||
// playLengthFrames = round(clamp01(lengthFraction) * (frameCount - startFrame))
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "core/instrument/engine/play_params.h" // splineActive (the %-knob's inert rule)
|
||||
|
||||
namespace reasampler::instrument::map {
|
||||
|
||||
// The %-length the voice ACTUALLY plays. A drawn contour is a pure time function over the
|
||||
// full sample length, so any active spline EG folds the fraction to 1.0 and the stored knob
|
||||
// goes inert (splineActive, play_params.h) — but the stored value survives, so a pre-spline
|
||||
// setting is still there to be read. Every consumer of the span must fold it here or it
|
||||
// silently plays/draws/bakes a fraction of the take.
|
||||
// `startFrame` is the effective start point (0 when absent), `lengthFraction` the EFFECTIVE one
|
||||
// — fold it through effectiveLengthFraction (play_params.h) first, or a stored-but-inert %-knob
|
||||
// shortens the span. Returns 0 for an empty post-start span and for any fraction that is not
|
||||
// above zero, NaN included; the fraction is clamped to 1.0 and the result to the span, so a
|
||||
// corrupt stored value cannot reach past the source.
|
||||
//
|
||||
// Templated over the two parameter representations for the same reason splineActive is.
|
||||
template <class Play>
|
||||
double effectiveLengthFraction(const Play& p) {
|
||||
return splineActive(p) ? 1.0 : p.trigger.lengthFraction;
|
||||
}
|
||||
|
||||
// postStart = max(0, frameCount - startFrame); playLength = round(lengthFraction * postStart).
|
||||
// `startFrame` is the effective start point (0 when absent), `lengthFraction` the EFFECTIVE
|
||||
// one above. Returns 0 when postStart == 0 or lengthFraction <= 0.
|
||||
// Voice::start evaluates this same clamped formula inline rather than calling it: the engine
|
||||
// does not depend on `map/`, in either direction.
|
||||
std::int64_t triggerPlayLength(double lengthFraction,
|
||||
std::int64_t frameCount,
|
||||
std::int64_t startFrame);
|
||||
|
||||
Reference in New Issue
Block a user