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
+90 -2
View File
@@ -7,9 +7,12 @@
// link is a regression.
#include "../src/core/instrument/map/component_state_io.h"
#include "../src/core/instrument/engine/envelopes.h" // AhdEnvelope (header-only: the codec
// links no engine, and this adds none)
#include "../src/core/instrument/engine/master_gain.h" // masterGainMaxLinear (the v8 wire cap)
#include "../src/core/util/curve_law.h" // kCurveNeutral (the migration neutral)
#include <cmath>
#include <cstdio>
#include <cstring>
#include <limits>
@@ -767,10 +770,94 @@ static void testSingleZoneMigrationIsLossless() {
CHECK(p.play.adsr.attackCurve == util::kCurveNeutral);
CHECK(p.play.adsr.decayCurve == util::kCurveNeutral);
CHECK(p.play.adsr.releaseCurve == util::kCurveNeutral);
CHECK(p.play.trigAhd.attackCurve == util::kCurveNeutral);
CHECK(p.play.trigAhd.decayCurve == util::kCurveNeutral);
CHECK(p.play.pitchEnv.shape.holdFraction == 0.0);
CHECK(p.play.filter.env.attackCurve == util::kCurveNeutral);
// The ONE exception, and the reason it is one: the fades had a prior SHAPE to reproduce,
// so they lift to the fitted exponents rather than to the neutral (see the contour test).
CHECK(p.play.trigAhd.attackCurve == kTriggerFadeLiftAttackCurve);
CHECK(p.play.trigAhd.decayCurve == kTriggerFadeLiftDecayCurve);
}
// The migrated Trigger amp shape against the retired EQUAL-POWER fade pair it replaced. The
// AHD's law is phi^p and cannot reproduce sin/cos at any exponent, so the claim is a bound —
// and the bound the fitted exponents reach is several times tighter than the linear neutral's,
// which is what makes the fit worth a constant.
static void testMigratedFadeContourTracksTheRetiredEqualPowerShape() {
const double rate = 48000.0;
const std::int64_t fadeIn = 200;
const std::int64_t fadeOut = 300;
const std::int64_t span = 1000;
legacy::Zone z;
z.sampleId = "kick";
z.trigger = true;
z.lengthFraction = 1.0;
z.fadeIn = fadeIn;
z.fadeOut = fadeOut;
const ComponentState st =
deserializeComponentState(legacy::envelopeWithZones("kick", {z}, 7), rate);
const AhdSeconds& lifted = st.params.play.trigAhd;
// Resolve the lifted seconds back to frames at the SAME rate the lift used, which is the
// matched-rate case (the mismatched one is asserted in sample_map_tests).
const auto toFrames = [rate](double sec) {
return static_cast<std::int64_t>(sec * rate + 0.5);
};
AhdParams migrated;
migrated.attackFrames = toFrames(lifted.attackSeconds);
migrated.decayFrames = toFrames(lifted.decaySeconds);
migrated.holdFraction = lifted.holdFraction;
migrated.attackCurve = lifted.attackCurve;
migrated.decayCurve = lifted.decayCurve;
// Stage LENGTHS are exact: the fades land on the same frames they always did.
AhdEnvelope ahd;
ahd.configure(span, migrated);
CHECK(ahd.stages().attack == fadeIn);
CHECK(ahd.stages().decay == fadeOut);
CHECK(ahd.stages().total == span);
// The pre-change evaluator, written out so the comparison is against a stated reference
// rather than against whatever the code now does.
const double pi = 3.14159265358979323846;
const auto retired = [&](double off) {
if (off < 0.0 || off >= static_cast<double>(span)) return 0.0;
if (off < static_cast<double>(fadeIn)) {
return std::sin(off / static_cast<double>(fadeIn) * (pi / 2.0));
}
const double foStart = static_cast<double>(span - fadeOut);
if (off >= foStart) {
return std::cos((off - foStart) / static_cast<double>(fadeOut) * (pi / 2.0));
}
return 1.0;
};
const auto worstAgainstRetired = [&](AhdEnvelope& env) {
double worst = 0.0;
for (std::int64_t i = 0; i < span; ++i) {
const double d = env.amplitudeAt(static_cast<double>(i)) -
retired(static_cast<double>(i));
worst = worst > std::fabs(d) ? worst : std::fabs(d);
}
return worst;
};
const double fitted = worstAgainstRetired(ahd);
CHECK(fitted <= 0.0876); // the measured minimax bound of phi^p against sin(pi*phi/2)
// The rejected alternative, evaluated rather than asserted about: the same lift at the
// linear neutral. If the fitted exponents were ever dropped this comparison inverts.
AhdParams neutralLift = migrated;
neutralLift.attackCurve = util::kCurveNeutral;
neutralLift.decayCurve = util::kCurveNeutral;
AhdEnvelope neutral;
neutral.configure(span, neutralLift);
const double neutralWorst = worstAgainstRetired(neutral);
CHECK(neutralWorst > 0.21);
CHECK(fitted < neutralWorst * 0.5);
// Both agree exactly where it matters structurally: the onset, the plateau, and the end.
CHECK(ahd.amplitudeAt(0.0) == retired(0.0));
CHECK(ahd.amplitudeAt(600.0) == retired(600.0));
CHECK(ahd.amplitudeAt(static_cast<double>(span)) == retired(static_cast<double>(span)));
}
// A prior ZERO fade-out lands Decay = 0: the abrupt end an old Trigger instance could express
@@ -1165,6 +1252,7 @@ int main() {
testEnvelopePrefixBytesFrozen();
testWriterEmitsCurrentPayloadVersion();
testSingleZoneMigrationIsLossless();
testMigratedFadeContourTracksTheRetiredEqualPowerShape();
testZeroFadeOutMigratesToZeroDecay();
testSingleZoneMigrationLiftsLoopDisablingOverride();
testLiftedStateReSavesInCurrentFormat();