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:
@@ -1,9 +1,9 @@
|
||||
// editor_controls.cpp — the ReaSamplerEditor's parameter plumbing: the band-stack layout
|
||||
// resolve every paint/hit-test path shares, the control-value domain maps (controlValue /
|
||||
// applyControl — seconds/fraction/frames <-> normalized 0..1), the control-id<->value binding
|
||||
// against the pure `deck_groups` module's descriptors, and the envelope pack/unpack (which
|
||||
// stored struct each overlay selection maps onto). Value logic only — no painting, no window
|
||||
// plumbing.
|
||||
// against the pure `deck_groups` module's descriptors, and the node-drag clamp bounds that must
|
||||
// match those domains. The orthogonal half — which stored struct each editor selection names —
|
||||
// is editor_models. Value logic only: no painting, no window plumbing.
|
||||
|
||||
#include "shell/instrument/reasampler_editor.h"
|
||||
|
||||
@@ -99,6 +99,12 @@ double ReaSamplerEditor::controlValue(int id, const PlaySeconds& play) const {
|
||||
const auto secToNorm = [](double s) { return clamp01(s / kEnvTimeMaxSeconds); };
|
||||
switch (static_cast<ParamControl>(id)) {
|
||||
case ParamControl::kPlayMode: return play.playMode == PlayMode::Trigger ? 1.0 : 0.0;
|
||||
case ParamControl::kAmpEnvMode:
|
||||
return play.ampSpline.mode == EnvMode::Spline ? 1.0 : 0.0;
|
||||
case ParamControl::kPitchEnvMode:
|
||||
return play.pitchSpline.mode == EnvMode::Spline ? 1.0 : 0.0;
|
||||
case ParamControl::kFilterEnvMode:
|
||||
return play.filterSpline.mode == EnvMode::Spline ? 1.0 : 0.0;
|
||||
case ParamControl::kPitchEngine: return play.pitchEngine == PitchEngine::Preserve ? 1.0 : 0.0;
|
||||
case ParamControl::kAttack: return secToNorm(play.adsr.attackSeconds);
|
||||
case ParamControl::kHold: return secToNorm(play.adsr.holdSeconds);
|
||||
@@ -164,8 +170,26 @@ void ReaSamplerEditor::applyControl(int id, PlaySeconds& play, double value,
|
||||
const auto normToSec = [](double v) { return clamp01(v) * kEnvTimeMaxSeconds; };
|
||||
switch (static_cast<ParamControl>(id)) {
|
||||
case ParamControl::kPlayMode:
|
||||
// Gate is refused while any EG is drawn — see splineActive (play_params.h). The
|
||||
// segment paints Disabled for the same reason, so the refusal is never a surprise.
|
||||
if (segment == 0 && splineActive(play)) break;
|
||||
play.playMode = (segment == 1) ? PlayMode::Trigger : PlayMode::Gate;
|
||||
break;
|
||||
// Switching TO Spline drops Gate, which the spline model has no place for. Switching
|
||||
// back does NOT restore it: the previous mode is not stored, and silently re-gating an
|
||||
// instrument the user has since heard as a one-shot is the worse surprise.
|
||||
case ParamControl::kAmpEnvMode:
|
||||
case ParamControl::kPitchEnvMode:
|
||||
case ParamControl::kFilterEnvMode: {
|
||||
const EnvMode m = (segment == 1) ? EnvMode::Spline : EnvMode::Staged;
|
||||
switch (static_cast<ParamControl>(id)) {
|
||||
case ParamControl::kAmpEnvMode: play.ampSpline.mode = m; break;
|
||||
case ParamControl::kPitchEnvMode: play.pitchSpline.mode = m; break;
|
||||
default: play.filterSpline.mode = m; break;
|
||||
}
|
||||
if (splineActive(play)) play.playMode = PlayMode::Trigger;
|
||||
break;
|
||||
}
|
||||
case ParamControl::kPitchEngine:
|
||||
play.pitchEngine = (segment == 1) ? PitchEngine::Preserve : PitchEngine::Varispeed;
|
||||
break;
|
||||
@@ -409,9 +433,9 @@ std::string ReaSamplerEditor::deckValueLabel(int id) const {
|
||||
}
|
||||
|
||||
EnvClampBounds ReaSamplerEditor::envClampBounds() const {
|
||||
// Match the deck knobs' own domains so a node drag can never produce a param a knob
|
||||
// couldn't. Every stage time caps at kEnvTimeMaxSeconds; the Hold fractions and the sustain
|
||||
// level are [0,1] by definition and need no bound here.
|
||||
// Lives here, beside controlValue/applyControl, because it must MATCH them: a node drag can
|
||||
// never produce a param a knob couldn't. Every stage time caps at kEnvTimeMaxSeconds; the
|
||||
// Hold fractions and the sustain level are [0,1] by definition and need no bound here.
|
||||
EnvClampBounds b;
|
||||
b.maxAttackSeconds = kEnvTimeMaxSeconds;
|
||||
b.maxHoldSeconds = kEnvTimeMaxSeconds;
|
||||
@@ -420,141 +444,6 @@ EnvClampBounds ReaSamplerEditor::envClampBounds() const {
|
||||
return b;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The two directions of the AHDSR <-> StageEnvelope copy, so a field can only be forgotten in
|
||||
// one place rather than two.
|
||||
void packAhdsr(const AdsrSeconds& a, StageEnvelope& env) {
|
||||
env.kind = instrument::ui::EnvKind::Ahdsr;
|
||||
env.attackSeconds = a.attackSeconds;
|
||||
env.holdSeconds = a.holdSeconds;
|
||||
env.decaySeconds = a.decaySeconds;
|
||||
env.sustainLevel = a.sustainLevel;
|
||||
env.releaseSeconds = a.releaseSeconds;
|
||||
env.attackCurve = a.attackCurve;
|
||||
env.decayCurve = a.decayCurve;
|
||||
env.releaseCurve = a.releaseCurve;
|
||||
}
|
||||
void unpackAhdsr(const StageEnvelope& env, AdsrSeconds& a) {
|
||||
a.attackSeconds = env.attackSeconds;
|
||||
a.holdSeconds = env.holdSeconds;
|
||||
a.decaySeconds = env.decaySeconds;
|
||||
a.sustainLevel = env.sustainLevel;
|
||||
a.releaseSeconds = env.releaseSeconds;
|
||||
a.attackCurve = env.attackCurve;
|
||||
a.decayCurve = env.decayCurve;
|
||||
a.releaseCurve = env.releaseCurve;
|
||||
}
|
||||
void packAhd(const AhdSeconds& a, double originSeconds, double spanSeconds, StageEnvelope& env) {
|
||||
env.kind = instrument::ui::EnvKind::Ahd;
|
||||
env.attackSeconds = a.attackSeconds;
|
||||
env.decaySeconds = a.decaySeconds;
|
||||
env.holdFraction = a.holdFraction;
|
||||
env.attackCurve = a.attackCurve;
|
||||
env.decayCurve = a.decayCurve;
|
||||
env.originSeconds = originSeconds;
|
||||
env.spanSeconds = spanSeconds;
|
||||
}
|
||||
void unpackAhd(const StageEnvelope& env, AhdSeconds& a) {
|
||||
a.attackSeconds = env.attackSeconds;
|
||||
a.decaySeconds = env.decaySeconds;
|
||||
a.holdFraction = env.holdFraction;
|
||||
a.attackCurve = env.attackCurve;
|
||||
a.decayCurve = env.decayCurve;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
StageEnvelope ReaSamplerEditor::packEnvelope(OverlayEnv which, const PlaySeconds& play,
|
||||
std::int64_t frames,
|
||||
std::int64_t startFrame) const {
|
||||
StageEnvelope env;
|
||||
const double rate = liveSampleRate();
|
||||
const double t0 = rate > 0.0 ? static_cast<double>(startFrame) / rate : 0.0;
|
||||
// The Trigger amp and filter AHDs live over the PLAY span; the pitch AHD over the whole
|
||||
// post-start span, since it keeps running after a Trigger one-shot's amplitude has ended.
|
||||
const std::int64_t playLen =
|
||||
triggerPlayLength(play.trigger.lengthFraction, frames, startFrame);
|
||||
const double playSpan = rate > 0.0 ? static_cast<double>(playLen) / rate : 0.0;
|
||||
const double fullSpan =
|
||||
rate > 0.0 ? static_cast<double>((std::max)(std::int64_t{0}, frames - startFrame)) / rate
|
||||
: 0.0;
|
||||
const bool trigger = (play.playMode == PlayMode::Trigger);
|
||||
switch (which) {
|
||||
case OverlayEnv::kPitch:
|
||||
packAhd(play.pitchEnv.shape, t0, fullSpan, env);
|
||||
break;
|
||||
case OverlayEnv::kFilter:
|
||||
if (trigger) packAhd(play.filter.trigEnv, t0, playSpan, env);
|
||||
else packAhdsr(play.filter.env, env);
|
||||
break;
|
||||
case OverlayEnv::kAmp:
|
||||
if (trigger) packAhd(play.trigAhd, t0, playSpan, env);
|
||||
else packAhdsr(play.adsr, env);
|
||||
break;
|
||||
case OverlayEnv::kNone:
|
||||
break; // nothing is overlay-active; a default-constructed StageEnvelope, not amp
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::unpackEnvelope(OverlayEnv which, const StageEnvelope& env,
|
||||
PlaySeconds& play) const {
|
||||
const bool trigger = (play.playMode == PlayMode::Trigger);
|
||||
switch (which) {
|
||||
case OverlayEnv::kPitch:
|
||||
unpackAhd(env, play.pitchEnv.shape);
|
||||
break;
|
||||
case OverlayEnv::kFilter:
|
||||
if (trigger) unpackAhd(env, play.filter.trigEnv);
|
||||
else unpackAhdsr(env, play.filter.env);
|
||||
break;
|
||||
case OverlayEnv::kAmp:
|
||||
if (trigger) unpackAhd(env, play.trigAhd);
|
||||
else unpackAhdsr(env, play.adsr);
|
||||
break;
|
||||
case OverlayEnv::kNone:
|
||||
break; // nothing is overlay-active, so there is nothing a drag could have edited
|
||||
}
|
||||
}
|
||||
|
||||
const VelocityCurve& ReaSamplerEditor::curveFor(CurveTarget target) const {
|
||||
switch (target) {
|
||||
case CurveTarget::kPitch: return params_.play.pitchVelocityCurve;
|
||||
case CurveTarget::kFilter: return params_.play.filter.velocityCurve;
|
||||
case CurveTarget::kAmp:
|
||||
case CurveTarget::kNone:
|
||||
// kNone reads as amp — a valid, harmless choice for a target-agnostic caller (a
|
||||
// disabled deck cell still needs SOME curve to draw). editedCurve()'s mutable
|
||||
// overload below refuses kNone rather than relying on this fallback to protect amp.
|
||||
return params_.velocityCurve;
|
||||
}
|
||||
return params_.velocityCurve;
|
||||
}
|
||||
|
||||
VelocityCurve& ReaSamplerEditor::curveFor(CurveTarget target) {
|
||||
return const_cast<VelocityCurve&>(std::as_const(*this).curveFor(target));
|
||||
}
|
||||
|
||||
const VelocityCurve& ReaSamplerEditor::editedCurve() const { return curveFor(curvePopup_); }
|
||||
|
||||
VelocityCurve& ReaSamplerEditor::editedCurve() {
|
||||
// Refuses kNone rather than aliasing amp (see curvePopup_'s declaration for why); should be
|
||||
// unreachable now that every writer of kNone also cancels the drag, but this is the second,
|
||||
// independent line of defense.
|
||||
if (curvePopup_ == CurveTarget::kNone) {
|
||||
static VelocityCurve sink = VelocityCurve::flat();
|
||||
return sink;
|
||||
}
|
||||
return curveFor(curvePopup_);
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::closeCurvePopup() {
|
||||
curvePopup_ = CurveTarget::kNone;
|
||||
if (drag_ == DragKind::kCurveNode) {
|
||||
drag_ = DragKind::kNone;
|
||||
curvePointIndex_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::applyParamControl(int id, double value, int segment) {
|
||||
if (id == static_cast<int>(ParamControl::kKeyTrack)) {
|
||||
// keyTrack sits beside the play bundle (0..200% over kKeyTrackMax); the knob maps 0..1.
|
||||
|
||||
Reference in New Issue
Block a user