S12: store wall-clock ADSR/pitch-env as seconds, resolve to frames at live rate
Kill kTier0Nominal*, adsrNeedsRateResolve, tier0Adsr, gateAdsr. Zones payload v5 carries seconds; v3 legacy reads convert at the frozen authoring rate; v4 (branch-only) dropped. Editor sliders now seconds. Engine takes frames resolved at keymap build.
This commit is contained in:
@@ -298,19 +298,19 @@ int ReaSamplerEditor::upsertPickedOverride(const SetupMarkers& m) {
|
||||
|
||||
namespace {
|
||||
// The S12/S15/S16 control-surface value DOMAINS (the shell owns these — param_slider is
|
||||
// engine-free and maps only 0..1). Time sliders span [0, max] frames at the NOMINAL 44100 Hz
|
||||
// rate; the stored frame count is host-rate-independent, so at other DAW rates the same slider
|
||||
// position maps to a slightly different wall-clock duration. The ceiling is kept nominal-only
|
||||
// because the host rate is not reachable inside the editor without a processor callback, and the
|
||||
// approximation is musically negligible (±1-2 ms at typical rates). Build-time residual — one
|
||||
// place to retune; not persisted.
|
||||
constexpr double kEnvTimeMaxFrames = 2.0 * 44100.0; // AHDSR A/H/D/R + pitch A/D throw ceiling (44100 nominal)
|
||||
// engine-free and maps only 0..1). WALL-CLOCK time sliders (AHDSR A/H/D/R, pitch env A/D) span
|
||||
// [0, kEnvTimeMaxSeconds] SECONDS — rate-free, exactly what the zone stores; the keymap build
|
||||
// resolves seconds->frames at the live rate. SOURCE-timeline fade sliders (Trigger fade-in/out)
|
||||
// span [0, kFadeMaxFrames] SOURCE frames (a source-timeline quantity, PLAN.md §S15 — never a
|
||||
// wall-clock second). Build-time residual — one place to retune; not persisted.
|
||||
constexpr double kEnvTimeMaxSeconds = 2.0; // AHDSR A/H/D/R + pitch A/D throw ceiling (seconds)
|
||||
constexpr double kFadeMaxFrames = 88200.0; // Trigger fade throw ceiling (source frames)
|
||||
constexpr double kPitchDepthMaxSemis = 24.0; // AD pitch depth throw: +/-24 st, centered
|
||||
|
||||
double clamp01(double v) { return v < 0.0 ? 0.0 : (v > 1.0 ? 1.0 : v); }
|
||||
} // namespace
|
||||
|
||||
std::vector<ControlDesc> ReaSamplerEditor::controlDescs(const ZonePlayParams& play) const {
|
||||
std::vector<ControlDesc> ReaSamplerEditor::controlDescs(const ZonePlaySeconds& play) const {
|
||||
std::vector<ControlDesc> out;
|
||||
// Always: the two mode toggles.
|
||||
out.push_back({static_cast<int>(ParamControl::kPlayMode), ControlKind::Toggle});
|
||||
@@ -335,24 +335,27 @@ std::vector<ControlDesc> ReaSamplerEditor::controlDescs(const ZonePlayParams& pl
|
||||
return out;
|
||||
}
|
||||
|
||||
double ReaSamplerEditor::controlValue(int id, const ZonePlayParams& play) const {
|
||||
double ReaSamplerEditor::controlValue(int id, const ZonePlaySeconds& play) const {
|
||||
// Wall-clock seconds -> normalized over the seconds ceiling; source frames -> normalized over
|
||||
// the frames ceiling. Two domains, kept explicit so neither leaks a rate.
|
||||
const auto secToNorm = [](double s) { return clamp01(s / kEnvTimeMaxSeconds); };
|
||||
const auto framesToNorm = [](std::int64_t f) {
|
||||
return clamp01(static_cast<double>(f) / kEnvTimeMaxFrames);
|
||||
return clamp01(static_cast<double>(f) / kFadeMaxFrames);
|
||||
};
|
||||
switch (static_cast<ParamControl>(id)) {
|
||||
case ParamControl::kPlayMode: return play.playMode == PlayMode::Trigger ? 1.0 : 0.0;
|
||||
case ParamControl::kPitchEngine: return play.pitchEngine == PitchEngine::Preserve ? 1.0 : 0.0;
|
||||
case ParamControl::kAttack: return framesToNorm(play.adsr.attackFrames);
|
||||
case ParamControl::kHold: return framesToNorm(play.adsr.holdFrames);
|
||||
case ParamControl::kDecay: return framesToNorm(play.adsr.decayFrames);
|
||||
case ParamControl::kAttack: return secToNorm(play.adsr.attackSeconds);
|
||||
case ParamControl::kHold: return secToNorm(play.adsr.holdSeconds);
|
||||
case ParamControl::kDecay: return secToNorm(play.adsr.decaySeconds);
|
||||
case ParamControl::kSustain: return clamp01(play.adsr.sustainLevel);
|
||||
case ParamControl::kRelease: return framesToNorm(play.adsr.releaseFrames);
|
||||
case ParamControl::kRelease: return secToNorm(play.adsr.releaseSeconds);
|
||||
case ParamControl::kTrigLength: return clamp01(play.trigger.lengthFraction);
|
||||
case ParamControl::kTrigFadeIn: return framesToNorm(play.trigger.fadeInFrames);
|
||||
case ParamControl::kTrigFadeOut: return framesToNorm(play.trigger.fadeOutFrames);
|
||||
case ParamControl::kPitchEnvEnable:return play.pitchEnv.enabled ? 1.0 : 0.0;
|
||||
case ParamControl::kPitchEnvAttack:return framesToNorm(play.pitchEnv.attackFrames);
|
||||
case ParamControl::kPitchEnvDecay: return framesToNorm(play.pitchEnv.decayFrames);
|
||||
case ParamControl::kPitchEnvAttack:return secToNorm(play.pitchEnv.attackSeconds);
|
||||
case ParamControl::kPitchEnvDecay: return secToNorm(play.pitchEnv.decaySeconds);
|
||||
case ParamControl::kPitchEnvDepth:
|
||||
// Signed depth centered at 0.5 (0.5 == 0 semitones).
|
||||
return clamp01(0.5 + play.pitchEnv.peakSemitones / (2.0 * kPitchDepthMaxSemis));
|
||||
@@ -360,10 +363,11 @@ double ReaSamplerEditor::controlValue(int id, const ZonePlayParams& play) const
|
||||
}
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::applyControl(int id, ZonePlayParams& play, double value,
|
||||
void ReaSamplerEditor::applyControl(int id, ZonePlaySeconds& play, double value,
|
||||
int segment) const {
|
||||
const auto normToSec = [](double v) { return clamp01(v) * kEnvTimeMaxSeconds; };
|
||||
const auto normToFrames = [](double v) {
|
||||
return static_cast<std::int64_t>(clamp01(v) * kEnvTimeMaxFrames + 0.5);
|
||||
return static_cast<std::int64_t>(clamp01(v) * kFadeMaxFrames + 0.5);
|
||||
};
|
||||
switch (static_cast<ParamControl>(id)) {
|
||||
case ParamControl::kPlayMode:
|
||||
@@ -372,11 +376,11 @@ void ReaSamplerEditor::applyControl(int id, ZonePlayParams& play, double value,
|
||||
case ParamControl::kPitchEngine:
|
||||
play.pitchEngine = (segment == 1) ? PitchEngine::Preserve : PitchEngine::Varispeed;
|
||||
break;
|
||||
case ParamControl::kAttack: play.adsr.attackFrames = normToFrames(value); break;
|
||||
case ParamControl::kHold: play.adsr.holdFrames = normToFrames(value); break;
|
||||
case ParamControl::kDecay: play.adsr.decayFrames = normToFrames(value); break;
|
||||
case ParamControl::kAttack: play.adsr.attackSeconds = normToSec(value); break;
|
||||
case ParamControl::kHold: play.adsr.holdSeconds = normToSec(value); break;
|
||||
case ParamControl::kDecay: play.adsr.decaySeconds = normToSec(value); break;
|
||||
case ParamControl::kSustain: play.adsr.sustainLevel = clamp01(value); break;
|
||||
case ParamControl::kRelease: play.adsr.releaseFrames = normToFrames(value); break;
|
||||
case ParamControl::kRelease: play.adsr.releaseSeconds = normToSec(value); break;
|
||||
case ParamControl::kTrigLength:
|
||||
// lengthFraction is (0,1]; keep a small floor so a zero-length trigger never plays nothing.
|
||||
play.trigger.lengthFraction = (std::max)(0.01, clamp01(value));
|
||||
@@ -386,8 +390,8 @@ void ReaSamplerEditor::applyControl(int id, ZonePlayParams& play, double value,
|
||||
case ParamControl::kPitchEnvEnable:
|
||||
play.pitchEnv.enabled = (segment == 1);
|
||||
break;
|
||||
case ParamControl::kPitchEnvAttack: play.pitchEnv.attackFrames = normToFrames(value); break;
|
||||
case ParamControl::kPitchEnvDecay: play.pitchEnv.decayFrames = normToFrames(value); break;
|
||||
case ParamControl::kPitchEnvAttack: play.pitchEnv.attackSeconds = normToSec(value); break;
|
||||
case ParamControl::kPitchEnvDecay: play.pitchEnv.decaySeconds = normToSec(value); break;
|
||||
case ParamControl::kPitchEnvDepth:
|
||||
play.pitchEnv.peakSemitones = (clamp01(value) - 0.5) * 2.0 * kPitchDepthMaxSemis;
|
||||
break;
|
||||
@@ -999,7 +1003,7 @@ void ReaSamplerEditor::paintControls(LICE_IBitmap* bmp, const Rect& panel) {
|
||||
// PerformanceZone product defaults when the map is empty but a capture is picked
|
||||
// (S15-F2 lean: the single-capture face shares the same storage site as a one-zone map;
|
||||
// see paintZones for the gate that reaches here).
|
||||
ZonePlayParams play;
|
||||
ZonePlaySeconds play;
|
||||
if (selectedZone_ >= 0 && selectedZone_ < static_cast<int>(map_.zones.size())) {
|
||||
play = map_.zones[static_cast<std::size_t>(selectedZone_)].play;
|
||||
} else if (map_.zones.empty() && !selectedId_.empty()) {
|
||||
@@ -1284,7 +1288,7 @@ void ReaSamplerEditor::onMouseDown(int x, int y) {
|
||||
// Synthesize a probe layout with the product defaults to see if the click is in the
|
||||
// panel before committing to creating the zone.
|
||||
const Rect panel = zonesControlPanel(bands);
|
||||
const ZonePlayParams defaultPlay = PerformanceZone{}.play;
|
||||
const ZonePlaySeconds defaultPlay = PerformanceZone{}.play;
|
||||
const std::vector<ControlDesc> probeDescs = controlDescs(defaultPlay);
|
||||
const std::vector<ControlRow> probeRows = layoutControls(panel, probeDescs);
|
||||
if (controlAtPoint(probeRows, x, y) >= 0) {
|
||||
@@ -1321,14 +1325,6 @@ void ReaSamplerEditor::onMouseDown(int x, int y) {
|
||||
dragParamPanel_ = panel;
|
||||
dragStartMap_ = map_;
|
||||
applyControl(id, z.play, valueAtPoint(r.control, x), 0);
|
||||
// An explicit ADSR slider touch commits a rate-resolved value (the slider
|
||||
// maps 0..1 -> editor-domain frames at kEnvTimeMaxFrames, not nominal 44100-Hz
|
||||
// counts). Mark the zone as no longer needing rate-resolve so buildZonedKeymap
|
||||
// does not re-rescale the value at reload time.
|
||||
if (id >= static_cast<int>(ParamControl::kAttack) &&
|
||||
id <= static_cast<int>(ParamControl::kRelease)) {
|
||||
z.adsrNeedsRateResolve = false;
|
||||
}
|
||||
invalidate(); // live feedback; commit on WM_LBUTTONUP
|
||||
}
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user