instrument: one staged-envelope system — per-segment curves, the sustain-less AHD, and a shared overlay for all three envelopes

Trigger's fade pair folds into the AHD (and goes live); the release anchors right;
Preserve rings its synthetic tail out instead of cutting it. Payload v10.
This commit is contained in:
2026-07-31 08:37:57 -04:00
parent 87d7ceb066
commit 13e8c5c4d9
51 changed files with 3406 additions and 1812 deletions
+8 -1
View File
@@ -3,12 +3,19 @@
## Scope
Tiny, dependency-free pure helpers linked by both artifacts: whole-file byte
loading, unit-interval clamping, and the absolute-path rejection test.
loading, unit-interval clamping, the absolute-path rejection test, and the
per-segment envelope curve law.
## Modules
- `file_bytes` (`core/util`) — the ONE whole-file byte loader (Q-W1), linked by both artifacts; blocking I/O, off-audio-thread only.
- `clamp01` (`core/util`, header-only) — the ONE unit-interval clamp (Q-W1), replacing four per-module static copies; NaN passes through unchanged rather than collapsing to a bound.
- `curve_law` (`core/util`, header-only) — the ONE per-segment envelope curve law: the
exponent domain (0.1..10, neutral 1.0), the normalized-position -> normalized-level map, and
the mid-segment inverse an overlay knot drags through. Header-only and dependency-free so
the engine's evaluator, the overlay's forward map, its inverse, and the deck's inner dial all
read one law instead of four copies. **The neutral exponent is the IDENTITY, bit for bit**
that is what makes an instance saved before curves existed play unchanged.
- `relative_path` (`core/util`, header-only) — the ONE absolute-path rejection test behind the relative-paths-only invariant, shared by `bank_model` (`Sample.relativePath`) and `core/tracking/origin_ledger` (`OriginRecord.relativePath`). The two must reject identically or a path one accepts could be smuggled past the other; that is why it is one function and not two.
## Gotchas
+5
View File
@@ -1,2 +1,7 @@
reasampler_pure_library(file_bytes SOURCES file_bytes.cpp)
reasampler_test(file_bytes LINK file_bytes)
# The per-segment envelope curve law is header-only, hence INTERFACE.
add_library(curve_law INTERFACE)
target_include_directories(curve_law INTERFACE ${REASAMPLER_SRC_DIR})
reasampler_test(curve_law LINK curve_law)
+47
View File
@@ -0,0 +1,47 @@
#pragma once
// curve_law — the ONE per-segment envelope curve law: the exponent domain, the map from a
// stage's normalized position to its normalized level, and the mid-segment inverse the
// overlay knot drags through. Header-only and dependency-free so the engine evaluator, the
// overlay's forward map, and its inverse all read the same law rather than three copies.
#include <cmath>
namespace reasampler::util {
// The per-segment curve is exponential: level = phi^exponent over the stage's normalized
// position phi. 1.0 is the LINEAR neutral (phi^1 == phi), which is why a pre-existing
// instance loading at 1.0 plays exactly as it did.
inline constexpr double kCurveNeutral = 1.0;
inline constexpr double kCurveMin = 0.1;
inline constexpr double kCurveMax = 10.0;
// Normalized position -> normalized level. The neutral exponent is compared EXACTLY so the
// at-rest per-sample path pays one predicted branch instead of a transcendental; every
// positive exponent maps 0 -> 0 and 1 -> 1, so a curved stage can never overshoot its own
// endpoint levels.
inline double curveMap(double phi, double exponent) {
if (exponent == kCurveNeutral) return phi;
return std::pow(phi, exponent);
}
inline double clampCurve(double exponent) {
if (!(exponent >= kCurveMin)) return kCurveMin; // also catches NaN
return exponent > kCurveMax ? kCurveMax : exponent;
}
// The normalized level at a segment's MIDPOINT (phi = 0.5) — where the overlay places the
// draggable curve knot — and its inverse. The pair is what keeps knot-drag and inner dial on
// one value: both resolve through this law, not through each other.
inline double curveMidLevel(double exponent) { return curveMap(0.5, clampCurve(exponent)); }
// Mid-level -> exponent: u = 0.5^p, so p = ln(u)/ln(0.5). Out-of-domain u clamps to the
// exponent endpoints rather than producing a non-finite exponent.
inline double curveFromMidLevel(double midLevel) {
const double lo = curveMidLevel(kCurveMax); // smallest reachable mid-level
const double hi = curveMidLevel(kCurveMin); // largest
if (!(midLevel > lo)) return kCurveMax; // also catches NaN
if (midLevel >= hi) return kCurveMin;
return clampCurve(std::log(midLevel) / std::log(0.5));
}
} // namespace reasampler::util