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