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