instrument: spline EGs — hard points on the one shared spline, a drawn contour per envelope beside its staged state, payload v13

This commit is contained in:
2026-07-31 21:33:59 -04:00
parent f115904e4f
commit e44bd42dd9
33 changed files with 1739 additions and 364 deletions
+37
View File
@@ -65,6 +65,20 @@ struct AhdParams {
double decayCurve = util::kCurveNeutral;
};
// Which shape an envelope takes: the STAGED knobs, or a free-drawn SPLINE contour. Both states
// are stored side by side and neither converts into the other, so a mode flip is reversible and
// lossless — the inactive one is saved but inert, edited only by switching back to it.
enum class EnvMode { Staged, Spline };
// The free-drawn alternative to a staged envelope: a contour over NORMALIZED sample time,
// covering the full sample length. Normalized is what makes it length-independent — a
// different-length capture replays the same shape proportionally, with no stored seconds to
// rescale. The default is the smooth y = 1 - x downward slope.
struct SplineEnv {
EnvMode mode = EnvMode::Staged;
VelocityCurve contour = VelocityCurve::rampDown();
};
// GATE = classic held note (AHDSR + sustain loop + note-off release). TRIGGER = one-shot:
// note-off-immune, no sustain loop, plays a % of sample length shaped by the AHD. Both honor
// the start point. Default Gate so an instrument with no params set plays as before.
@@ -150,8 +164,31 @@ struct PlayParams {
// baseRatio_ at note-on — it is fixed for the note's lifetime, so it costs no per-frame work.
VelocityCurve pitchVelocityCurve = VelocityCurve::zero();
FilterParams filter;
// The three drawn contours: the alternative to adsr/trigAhd, to pitchEnv.shape, and to
// filter.env/trigEnv respectively. They sit HERE rather than inside the three envelope
// structs because those are copied whole into the live block, which must stay trivially
// copyable (live_params.h) — and a contour is not a live control anyway: like the velocity
// curves it travels by reload.
SplineEnv ampSpline;
SplineEnv pitchSpline;
SplineEnv filterSpline;
};
// Whether ANY of the three envelopes is drawn rather than staged. Templated over the two
// parameter representations (frames and the editor's seconds mirror) because both spell the
// three fields identically and the rule must not be written twice — compile-time dispatch,
// no runtime cost, off every hot path.
//
// THE consequence, and its one home: a spline contour is a pure time function over the full
// sample length, which IS the Trigger/one-shot playback model — so Gate is not available while
// any spline EG is active. resolvePlay enforces it on the way to the engine; the editor's
// play-mode toggle refuses the Gate segment so the two agree.
template <class Play>
bool splineActive(const Play& p) {
return p.ampSpline.mode == EnvMode::Spline || p.pitchSpline.mode == EnvMode::Spline ||
p.filterSpline.mode == EnvMode::Spline;
}
// [start, end) frames, half-open. A zero-length loop (start == end) is the "no sustain loop"
// marker — a held note past the sample end goes silent rather than looping a zero span.
struct SampleLoop {