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
+7 -5
View File
@@ -11,11 +11,13 @@ per-segment envelope curve law.
- `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.
exponent domain (0.1..10, neutral 1.0), the normalized-position -> normalized-level map, the
mid-segment inverse an overlay knot drags through, and the inner dial's own norm<->exponent
travel. 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, and what the knob law's centre detent exists to keep
reachable from the dial.
- `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
+29
View File
@@ -19,6 +19,12 @@ inline constexpr double kCurveMax = 10.0;
// 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.
//
// MEASURED (Release, MSVC 19.44 x64, one dev machine): a non-neutral exponent costs ~4.7 ns per
// evaluation. At the 16-voice default with all three envelopes in a sloped stage — three
// evaluations per voice per frame, the worst case — that is +224 ns per output frame: 3.1% of
// one core at 44.1 kHz becomes 4.1%. Affordable at the shipped voice ceiling; re-measure before
// putting a fourth per-voice curve on the frame.
inline double curveMap(double phi, double exponent) {
if (exponent == kCurveNeutral) return phi;
return std::pow(phi, exponent);
@@ -29,6 +35,29 @@ inline double clampCurve(double exponent) {
return exponent > kCurveMax ? kCurveMax : exponent;
}
// The exponent's KNOB travel: logarithmic, so the two halves of the throw are the reciprocal
// shaping directions. Written as kCurveMax^(2t-1) rather than as an interpolation between
// log(kCurveMin) and log(kCurveMax) so t == 0.5 evaluates exp(0) == 1.0 EXACTLY — an inexact
// centre would put std::pow on the per-sample path for a stage the user believes is parked at
// the identity. Requires kCurveMin == 1/kCurveMax, which the domain above satisfies.
//
// The detent is what makes the identity REACHABLE: a knob drag delivers start - dy/128, a grid
// that lands on 0.5 only by luck, so a band wider than one drag step (1/128) snaps to neutral
// and a dial swept through the centre cannot skip over it.
inline constexpr double kCurveKnobDetent = 0.01;
inline double curveFromKnobNorm(double norm) {
const double t = norm < 0.0 ? 0.0 : (norm > 1.0 ? 1.0 : norm);
const double off = t - 0.5;
if (off < kCurveKnobDetent && off > -kCurveKnobDetent) return kCurveNeutral;
return clampCurve(std::exp((2.0 * t - 1.0) * std::log(kCurveMax)));
}
inline double knobNormFromCurve(double exponent) {
const double t = 0.5 + std::log(clampCurve(exponent)) / (2.0 * std::log(kCurveMax));
return t < 0.0 ? 0.0 : (t > 1.0 ? 1.0 : t);
}
// 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.