Bound the automation hold to the window the model has not caught up on, and make that authority model stated, enforced and tested

This commit is contained in:
2026-08-02 17:16:02 -04:00
parent de5654fb6f
commit 1fd38bbd57
35 changed files with 1155 additions and 302 deletions
+47 -1
View File
@@ -6,7 +6,11 @@
namespace reasampler::instrument::engine {
LiveValues foldLive(const PlayParams& params, double keyTrack) {
LiveValues v;
// Value-initialized, so the padding is determinate too. Nothing reads it — the block's
// equality is field-wise for exactly that reason — but this is the one construction site
// every publisher goes through, and an object with indeterminate bytes travelling under a
// seqlock is a hazard worth not having. Off the audio thread; the memset costs nothing here.
LiveValues v{};
v.keyTrack = keyTrack;
// Folded here, not at the voice: Voice::start reads the block's value directly, so the
// spline rule has to be applied on the way in or the two would answer differently.
@@ -26,6 +30,48 @@ LiveValues foldLive(const PlayParams& params, double keyTrack) {
return v;
}
namespace {
bool sameAdsr(const AdsrParams& a, const AdsrParams& b) {
return a.attackFrames == b.attackFrames && a.holdFrames == b.holdFrames &&
a.decayFrames == b.decayFrames && a.sustainLevel == b.sustainLevel &&
a.releaseFrames == b.releaseFrames && a.attackCurve == b.attackCurve &&
a.decayCurve == b.decayCurve && a.releaseCurve == b.releaseCurve;
}
bool sameAhd(const AhdParams& a, const AhdParams& b) {
return a.attackFrames == b.attackFrames && a.decayFrames == b.decayFrames &&
a.holdFraction == b.holdFraction && a.attackCurve == b.attackCurve &&
a.decayCurve == b.decayCurve;
}
bool sameFilterSettings(const filter::FilterSettings& a, const filter::FilterSettings& b) {
return a.cutoffNorm == b.cutoffNorm && a.resonanceNorm == b.resonanceNorm &&
a.morphNorm == b.morphNorm && a.driveNorm == b.driveNorm &&
a.morphLaw == b.morphLaw;
}
} // namespace
bool operator==(const LiveValues& a, const LiveValues& b) {
return sameFilterSettings(a.filterSettings, b.filterSettings) &&
a.filterModAmount == b.filterModAmount &&
a.filterVelAmount == b.filterVelAmount &&
a.filterKeyTrack == b.filterKeyTrack &&
sameAdsr(a.filterEnv, b.filterEnv) &&
sameAhd(a.filterAhd, b.filterAhd) &&
sameAdsr(a.adsr, b.adsr) &&
sameAhd(a.ampAhd, b.ampAhd) &&
a.pitchEnv.enabled == b.pitchEnv.enabled &&
a.pitchEnv.peakSemitones == b.pitchEnv.peakSemitones &&
sameAhd(a.pitchEnv.shape, b.pitchEnv.shape) &&
a.playRate == b.playRate &&
a.pitchOffsetSemitones == b.pitchOffsetSemitones &&
a.keyTrack == b.keyTrack &&
a.lengthFraction == b.lengthFraction &&
a.splineActive == b.splineActive;
}
double liveRampStep(double sampleRate) {
if (!(sampleRate > 0.0)) return 0.0; // also catches NaN
return 1.0 / (kLiveRampSeconds * sampleRate);
+10
View File
@@ -71,6 +71,16 @@ struct LiveValues {
static_assert(std::is_trivially_copyable_v<LiveValues>,
"the live block is copied under a seqlock — it must stay a plain value");
// FIELD-wise equality, and it must never be "simplified" into a memcmp. LiveValues carries
// padding, and nothing gives that padding a determinate value across a copy: NRVO is optional
// and the implicit copy/move is specified member-wise, so two blocks folded from the same
// parameter set are NOT reliably byte-equal. A byte compare therefore reports differences that
// do not exist — which is exactly what it did before this existed. Listed member by member, so a
// member added to the block above must be added here as well; this sits directly beneath the
// struct for that reason.
bool operator==(const LiveValues& a, const LiveValues& b);
inline bool operator!=(const LiveValues& a, const LiveValues& b) { return !(a == b); }
// The ONE derivation of the live block from the parameter set. Every publisher goes through
// here so there is a single site to keep in step with PlayParams. `keyTrack` is passed in
// because it belongs to the capture/instrument scalar beside the play bundle, not to