instrument: fix AHD node-tracking/tie-break/live-latch defects and close staged-envelope-curve test gaps

This commit is contained in:
2026-07-31 09:52:17 -04:00
parent d60ab1524a
commit 2fa1405b06
27 changed files with 360 additions and 86 deletions
+2 -2
View File
@@ -21,7 +21,7 @@ reasampler_test(bank_sync LINK bank_sync)
# the format already keeps on independent version axes (see component_state_io.h).
reasampler_pure_library(component_state_io
SOURCES component_state_io.cpp params_payload.cpp
LINK PUBLIC velocity_curve master_gain)
LINK PUBLIC velocity_curve master_gain curve_law)
# Links only component_state_io, deliberately no sampler_core/pitch_shift: the structural
# proof the codec is engine-free, which is what keeps engine object code out of the extension.
reasampler_test(component_state_io LINK component_state_io)
@@ -29,7 +29,7 @@ reasampler_test(component_state_io LINK component_state_io)
# The mapping's product is plain SampleData, so the voice engine is not a dependency.
reasampler_pure_library(sample_map
SOURCES sample_map.cpp
LINK PUBLIC bank_book wav_codec velocity_curve peaks)
LINK PUBLIC bank_book wav_codec velocity_curve peaks curve_law)
# Links only sample_map + component_state_io: the same plain-data-boundary proof, spanning
# both halves of the mapping/codec split where the frozen-format assertions live.
reasampler_test(sample_map LINK sample_map component_state_io)
@@ -17,15 +17,13 @@ namespace reasampler::instrument::map {
using engine::masterGainMaxLinear;
using reasampler::wire::ByteReader;
using reasampler::wire::asU64;
using reasampler::wire::bitsToDouble;
using reasampler::wire::doubleToBits;
using reasampler::wire::putLE;
namespace {
// Signed 64-bit values ride the wire as their two's-complement unsigned image.
std::uint64_t asU64(std::int64_t v) { return static_cast<std::uint64_t>(v); }
// Apply a payload read to the state: the adoption rule (a retired payload's first zone
// supersedes the envelope's selection id) lives here, once.
void applyPayload(ComponentState& out, PayloadRead read) {
+9 -5
View File
@@ -14,15 +14,13 @@
namespace reasampler::instrument::map {
using reasampler::wire::ByteReader;
using reasampler::wire::asU64;
using reasampler::wire::bitsToDouble;
using reasampler::wire::doubleToBits;
using reasampler::wire::putLE;
namespace {
// Signed 64-bit values ride the wire as their two's-complement unsigned image.
std::uint64_t asU64(std::int64_t v) { return static_cast<std::uint64_t>(v); }
// Emit the OVERRIDE trio shared by the v2..v7 per-zone record and the v8 single record, so
// the two shapes cannot drift byte-for-byte.
void putOverrides(std::vector<std::uint8_t>& out, const InstrumentParams& p) {
@@ -149,8 +147,14 @@ double readCurveExponent(ByteReader& r) {
}
void readAhd(ByteReader& r, AhdSeconds& a) {
a.attackSeconds = bitsToDouble(r.u64());
a.decaySeconds = bitsToDouble(r.u64());
// attackSeconds/decaySeconds reach resolvePlay's static_cast<std::int64_t> (sample_map.cpp)
// unguarded — UB on NaN, and on a large-enough finite value — so a corrupt/non-finite wire
// value degrades to 0 seconds rather than reaching that cast, the same guard readSecondsPlayTail
// and the v9 filter tail already apply to their own wall-clock fields.
const double attack = bitsToDouble(r.u64());
const double decay = bitsToDouble(r.u64());
a.attackSeconds = std::isfinite(attack) ? attack : 0.0;
a.decaySeconds = std::isfinite(decay) ? decay : 0.0;
const double frac = bitsToDouble(r.u64());
a.holdFraction = std::isfinite(frac) ? frac : 0.0;
a.attackCurve = readCurveExponent(r);
+2 -2
View File
@@ -203,8 +203,8 @@ DecodedPcm decodeChannels(const std::vector<AudioSample>& interleaved,
}
PlayParams resolvePlay(const PlaySeconds& stored, int sampleRate) {
// seconds -> frames at the LIVE rate; source-timeline quantities (trigger %-length +
// fades) carry through untouched, already frames/fractions.
// seconds -> frames at the LIVE rate; the source-timeline quantity (trigger %-length)
// carries through untouched, already a fraction.
assert(sampleRate > 0 && "resolvePlay: sampleRate must be > 0 (programming error)");
const double sr = sampleRate > 0 ? static_cast<double>(sampleRate) : 1.0; // 1.0 avoids div-by-zero; assert fires first
const auto secToFrames = [sr](double sec) {