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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user