instrument: latch the note done at the read-head run-off, fit the migrated fades, and lift the curve dial and overlay selection into pure modules

This commit is contained in:
2026-07-31 09:17:04 -04:00
parent 13e8c5c4d9
commit d60ab1524a
18 changed files with 575 additions and 129 deletions
@@ -91,8 +91,26 @@ namespace reasampler::instrument::map {
// 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
// seconds, so the lift divides by the PROJECT rate while the build later multiplies by the
// DECODE rate: a file whose own rate differs from the project's comes back scaled by that
// ratio (a 441-frame fade on a 44.1 kHz file in a 48 kHz project resolves to 405 source
// frames, ~8% short). The codec cannot close this — it never sees the file — and deferring the
// lift to build time would mean carrying the retired fade pair through the parameter set,
// reintroducing the mechanism the AHD replaced.
//
// A truncated/unknown/empty payload yields the DEFAULT parameter set.
// The exponents the lifted fades take. The AHD's curve law is phi^p (core/util/curve_law.h),
// which cannot reproduce the retired pair's equal-power sin/cos exactly at ANY exponent — so
// the lift takes the MINIMAX fit rather than the linear neutral, which is free (one constant,
// written once here) and several times closer. The two differ because the two stages fit
// different forms: attack fits phi^p to sin(pi*phi/2), decay fits 1 - t^q to cos(pi*t/2).
// The measured bounds are asserted in tests/test_component_state_io.cpp. Every OTHER curve on
// a migrated blob still lifts to the neutral — only the fades had a prior shape to reproduce.
inline constexpr double kTriggerFadeLiftAttackCurve = 0.6133;
inline constexpr double kTriggerFadeLiftDecayCurve = 1.7437;
inline constexpr std::uint32_t kPerformanceStateVersion = 2;
// The params-payload format version and its detection marker. The marker is a high sentinel
+5 -3
View File
@@ -61,15 +61,17 @@ void putAhd(std::vector<std::uint8_t>& out, const AhdSeconds& a) {
}
// THE lift of the retired Trigger fade pair onto the AHD that replaced it: Attack takes the
// fade-in, Decay the fade-out, Hold the whole remainder — so a zero fade-out lands Decay = 0
// and the abrupt end an old instance could express stays representable. The fades were SOURCE
// frames and the AHD stores wall-clock seconds, so the conversion goes through the same
// project rate the v3 lift already uses. A v10-or-newer blob overwrites this from its own tail.
// and the abrupt end an old instance could express stays representable. The seconds conversion
// and its rate-mismatch bound, and the two fitted exponents, are documented in
// component_state_io.h. A v10-or-newer blob overwrites all five fields from its own tail.
void liftTriggerFades(std::int64_t fadeInFrames, std::int64_t fadeOutFrames, double projectRate,
AhdSeconds& out) {
const double rate = projectRate > 0.0 ? projectRate : 1.0;
out.attackSeconds = static_cast<double>(fadeInFrames > 0 ? fadeInFrames : 0) / rate;
out.decaySeconds = static_cast<double>(fadeOutFrames > 0 ? fadeOutFrames : 0) / rate;
out.holdFraction = 1.0;
out.attackCurve = kTriggerFadeLiftAttackCurve;
out.decayCurve = kTriggerFadeLiftDecayCurve;
}
// Read the play tail (v5 shape onward) into `p`. Shared by the legacy zone reader and the