39389c1183
Codec fallback for non-finite filter fields, a modAmount==0 early-out in tickFilterCutoff, new stereo render tests for the dual-mono mirror, and comment/test accuracy fixes flagged in review (stale deck-width claims, restated invariants, drifted CMake link comments).
401 lines
22 KiB
C++
401 lines
22 KiB
C++
// 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 (the
|
|
// trigger-seam converter). Value logic only — no painting, no window plumbing.
|
|
|
|
#include "shell/instrument/reasampler_editor.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <cstdio> // snprintf (deck value labels)
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "core/instrument/engine/filter/filter_params.h" // the filter's own control laws
|
|
#include "core/instrument/engine/master_gain.h" // master-gain dB<->linear<->knob taper
|
|
#include "core/instrument/map/trigger_seam.h" // triggerPlayLength / fade fraction converters
|
|
#include "core/instrument/ui/deck_groups.h" // sampleDeckGroups (the deck's composition)
|
|
#include "core/instrument/ui/knob_deck.h" // deckHeight / kDeckKnobSize (the band's own height)
|
|
#include "core/util/clamp01.h"
|
|
#include "shell/instrument/editor_internal.h"
|
|
#include "shell/instrument/reasampler_processor.h"
|
|
|
|
namespace reasampler::vst {
|
|
|
|
using namespace reasampler::instrument::map; // PlaySeconds vocabulary + trigger_seam converters
|
|
using instrument::ui::EnvMode; // envelope_overlay's mode enum
|
|
using instrument::ui::computeSampleBands;
|
|
using instrument::ui::chromeRects;
|
|
using instrument::ui::deckHeight;
|
|
using instrument::ui::kDeckKnobSize;
|
|
using instrument::ui::kPad;
|
|
using instrument::ui::deckBipolarFromNorm;
|
|
using instrument::ui::deckNormFromBipolar;
|
|
using instrument::ui::sampleDeckGroups;
|
|
using instrument::engine::formatMasterGainLabel;
|
|
using instrument::engine::masterGainLinearFromNorm;
|
|
using instrument::engine::masterGainNormFromLinear;
|
|
using instrument::engine::filter::MorphLaw;
|
|
using instrument::engine::filter::filterCutoffHzFromNorm;
|
|
using instrument::engine::filter::filterDriveDepthFromNorm;
|
|
using instrument::engine::filter::filterQFromNorm;
|
|
using util::clamp01;
|
|
|
|
namespace {
|
|
// Control-surface value domains (the shell owns these — param_slider is 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 parameter set stores; the build resolves seconds->frames
|
|
// at the live rate. Source-timeline fade sliders (Trigger fade-in/out) store source frames
|
|
// (never a wall-clock second), but the knob's full-scale throw is a wall-clock intent —
|
|
// kFadeMaxSeconds resolved against the live rate at use (fadeMaxFrames()) rather than a baked-in
|
|
// rate constant, per the no-hardcoded-rate ruling.
|
|
constexpr double kEnvTimeMaxSeconds = 2.0; // AHDSR A/H/D/R + pitch A/D throw ceiling (seconds)
|
|
constexpr double kFadeMaxSeconds = 2.0; // Trigger fade throw ceiling (wall-clock)
|
|
constexpr double kPitchDepthMaxSemis = 24.0; // AD pitch depth throw: +/-24 st, centered
|
|
constexpr double kKeyTrackMax = 2.0; // key-track slider ceiling (0..200%)
|
|
|
|
} // namespace
|
|
|
|
ReaSamplerEditor::FaceLayout ReaSamplerEditor::faceLayout(int w, int h) const {
|
|
// The ONE resolve every paint and hit-test path goes through, so the band stack, the
|
|
// chrome interior, and the deck descriptors can never be derived three different ways.
|
|
// The deck's own wrapped height is the only interior measurement the allocator needs.
|
|
FaceLayout fl;
|
|
fl.deckDescs = sampleDeckGroups(params_.play.playMode);
|
|
fl.bands = computeSampleBands(w, h, deckHeight(fl.deckDescs, w - 2 * kPad));
|
|
fl.chrome = chromeRects(fl.bands.chrome, kDeckKnobSize);
|
|
return fl;
|
|
}
|
|
|
|
double ReaSamplerEditor::controlValue(int id, const PlaySeconds& play) const {
|
|
// Wall-clock seconds -> normalized over the seconds ceiling; source frames -> normalized over
|
|
// the rate-resolved frames ceiling. Two domains, kept explicit so neither leaks a rate. A
|
|
// stored fade exceeding fadeMaxFrames() at the current host rate reads as norm 1.0 (clamp01
|
|
// pins it) and gets rewritten down on the next knob touch.
|
|
const double fadeMax = fadeMaxFrames();
|
|
const auto secToNorm = [](double s) { return clamp01(s / kEnvTimeMaxSeconds); };
|
|
const auto framesToNorm = [fadeMax](std::int64_t f) {
|
|
// Ceiling unavailable (rate not yet known): inert until fadeMaxFrames() resolves.
|
|
return fadeMax > 0.0 ? clamp01(static_cast<double>(f) / fadeMax) : 0.0;
|
|
};
|
|
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 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 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 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));
|
|
// Filter. The four tone controls ARE the module's normalized positions — stored and
|
|
// shown as-is, so the knob travel is exactly filter_params' own law.
|
|
case ParamControl::kFilterEnable: return play.filter.enabled ? 1.0 : 0.0;
|
|
case ParamControl::kFilterLaw:
|
|
return play.filter.settings.morphLaw == MorphLaw::HighNotchLow ? 1.0 : 0.0;
|
|
case ParamControl::kFilterMorph: return clamp01(play.filter.settings.morphNorm);
|
|
case ParamControl::kFilterCutoff: return clamp01(play.filter.settings.cutoffNorm);
|
|
case ParamControl::kFilterQ: return clamp01(play.filter.settings.resonanceNorm);
|
|
case ParamControl::kFilterDrive: return clamp01(play.filter.settings.driveNorm);
|
|
case ParamControl::kFilterModAmt: return deckNormFromBipolar(play.filter.modAmount);
|
|
case ParamControl::kFilterVel: return deckNormFromBipolar(play.filter.velAmount);
|
|
case ParamControl::kFilterKeyTrack:return clamp01(play.filter.keyTrack / kKeyTrackMax);
|
|
case ParamControl::kFilterEnvAttack: return secToNorm(play.filter.env.attackSeconds);
|
|
case ParamControl::kFilterEnvHold: return secToNorm(play.filter.env.holdSeconds);
|
|
case ParamControl::kFilterEnvDecay: return secToNorm(play.filter.env.decaySeconds);
|
|
case ParamControl::kFilterEnvSustain: return clamp01(play.filter.env.sustainLevel);
|
|
case ParamControl::kFilterEnvRelease: return secToNorm(play.filter.env.releaseSeconds);
|
|
default: return 0.0;
|
|
}
|
|
}
|
|
|
|
void ReaSamplerEditor::applyControl(int id, PlaySeconds& play, double value,
|
|
int segment) const {
|
|
const double fadeMax = fadeMaxFrames(); // rate-resolved knob full-scale
|
|
const auto normToSec = [](double v) { return clamp01(v) * kEnvTimeMaxSeconds; };
|
|
const auto normToFrames = [fadeMax](double v) -> std::int64_t {
|
|
// Ceiling unavailable (rate not yet known): inert until fadeMaxFrames() resolves.
|
|
if (fadeMax <= 0.0) return 0;
|
|
return static_cast<std::int64_t>(clamp01(v) * fadeMax + 0.5);
|
|
};
|
|
switch (static_cast<ParamControl>(id)) {
|
|
case ParamControl::kPlayMode:
|
|
play.playMode = (segment == 1) ? PlayMode::Trigger : PlayMode::Gate;
|
|
break;
|
|
case ParamControl::kPitchEngine:
|
|
play.pitchEngine = (segment == 1) ? PitchEngine::Preserve : PitchEngine::Varispeed;
|
|
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.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));
|
|
break;
|
|
case ParamControl::kTrigFadeIn: play.trigger.fadeInFrames = normToFrames(value); break;
|
|
case ParamControl::kTrigFadeOut: play.trigger.fadeOutFrames = normToFrames(value); break;
|
|
case ParamControl::kPitchEnvEnable:
|
|
play.pitchEnv.enabled = (segment == 1);
|
|
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;
|
|
case ParamControl::kFilterEnable: play.filter.enabled = (segment == 1); break;
|
|
case ParamControl::kFilterLaw:
|
|
play.filter.settings.morphLaw =
|
|
(segment == 1) ? MorphLaw::HighNotchLow : MorphLaw::HighBandLow;
|
|
break;
|
|
case ParamControl::kFilterMorph:
|
|
play.filter.settings.morphNorm = static_cast<float>(clamp01(value)); break;
|
|
case ParamControl::kFilterCutoff:
|
|
play.filter.settings.cutoffNorm = static_cast<float>(clamp01(value)); break;
|
|
case ParamControl::kFilterQ:
|
|
play.filter.settings.resonanceNorm = static_cast<float>(clamp01(value)); break;
|
|
case ParamControl::kFilterDrive:
|
|
play.filter.settings.driveNorm = static_cast<float>(clamp01(value)); break;
|
|
case ParamControl::kFilterModAmt: play.filter.modAmount = deckBipolarFromNorm(value); break;
|
|
case ParamControl::kFilterVel: play.filter.velAmount = deckBipolarFromNorm(value); break;
|
|
case ParamControl::kFilterKeyTrack:
|
|
play.filter.keyTrack = clamp01(value) * kKeyTrackMax; break;
|
|
case ParamControl::kFilterEnvAttack:
|
|
play.filter.env.attackSeconds = normToSec(value); break;
|
|
case ParamControl::kFilterEnvHold:
|
|
play.filter.env.holdSeconds = normToSec(value); break;
|
|
case ParamControl::kFilterEnvDecay:
|
|
play.filter.env.decaySeconds = normToSec(value); break;
|
|
case ParamControl::kFilterEnvSustain:
|
|
play.filter.env.sustainLevel = clamp01(value); break;
|
|
case ParamControl::kFilterEnvRelease:
|
|
play.filter.env.releaseSeconds = normToSec(value); break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
double ReaSamplerEditor::liveSampleRate() const {
|
|
return processor_ ? processor_->sampleRate() : 0.0;
|
|
}
|
|
|
|
double ReaSamplerEditor::fadeMaxFrames() const {
|
|
// The Trigger-fade knob's full-scale throw is kFadeMaxSeconds (2 s wall-clock) resolved
|
|
// against the live rate — the same time base the envelope overlay already uses to place
|
|
// these source-frame fades on screen. Pre-setupProcessing the rate is still 0: rather than
|
|
// substitute a literal rate, callers treat a <= 0 return as "ceiling unavailable yet" and
|
|
// degrade the knob to inert rather than guess a rate. Storage stays source frames — this
|
|
// resolves the UI ceiling only.
|
|
const double rate = liveSampleRate();
|
|
if (rate <= 0.0) return 0.0;
|
|
return kFadeMaxSeconds * rate;
|
|
}
|
|
|
|
double ReaSamplerEditor::previewVelocity01() const {
|
|
if (!processor_) return static_cast<double>(kPreviewVelocityDefault) / 127.0;
|
|
return static_cast<double>(processor_->previewVelocity()) / 127.0;
|
|
}
|
|
|
|
double ReaSamplerEditor::deckControlNorm(int id) const {
|
|
if (id == -2) return previewVelocity01(); // the chrome preview-velocity knob
|
|
switch (static_cast<ParamControl>(id)) {
|
|
case ParamControl::kKeyTrack:
|
|
return clamp01(params_.keyTrack / kKeyTrackMax);
|
|
case ParamControl::kVoiceCount:
|
|
return clamp01(static_cast<double>(voiceCount_ - kMinVoiceCount) /
|
|
static_cast<double>(kMaxVoiceCount - kMinVoiceCount));
|
|
case ParamControl::kMasterGain:
|
|
return masterGainNormFromLinear(processor_ ? processor_->masterGainLinear() : 1.0);
|
|
default:
|
|
return controlValue(id, params_.play);
|
|
}
|
|
}
|
|
|
|
void ReaSamplerEditor::applyDeckKnob(int id, double norm) {
|
|
if (!processor_) return;
|
|
norm = clamp01(norm);
|
|
if (id == -2) {
|
|
// Preview velocity: live processor write (persisted per-instance; the setter clamps
|
|
// to MIDI 1..127 so the knob's bottom still strikes audibly).
|
|
processor_->setPreviewVelocity(static_cast<std::uint8_t>(norm * 127.0 + 0.5));
|
|
return;
|
|
}
|
|
switch (static_cast<ParamControl>(id)) {
|
|
case ParamControl::kVoiceCount: {
|
|
// Stepped: quantize the continuous drag to the integer count and track it live
|
|
// for the label/needle. The actual engine rebuild (setVoiceCount) fires ONCE on
|
|
// WM_LBUTTONUP — not per step — so a full drag (~31 steps) costs one rebuild,
|
|
// not thirty.
|
|
const int count =
|
|
kMinVoiceCount +
|
|
static_cast<int>(norm * (kMaxVoiceCount - kMinVoiceCount) + 0.5);
|
|
voiceCount_ = count;
|
|
return;
|
|
}
|
|
case ParamControl::kMasterGain:
|
|
// Post-mixer gain: one atomic store; the audio thread picks it up next block.
|
|
processor_->setMasterGainLinear(masterGainLinearFromNorm(norm));
|
|
return;
|
|
default:
|
|
applyParamControl(id, norm, 0);
|
|
return;
|
|
}
|
|
}
|
|
|
|
std::string ReaSamplerEditor::deckValueLabel(int id) const {
|
|
char buf[24];
|
|
buf[0] = '\0';
|
|
const PlaySeconds& play = params_.play;
|
|
switch (id == -2 ? ParamControl::kCount : static_cast<ParamControl>(id)) {
|
|
case ParamControl::kAttack:
|
|
snprintf(buf, sizeof(buf), "%.3fs", play.adsr.attackSeconds); break;
|
|
case ParamControl::kHold:
|
|
snprintf(buf, sizeof(buf), "%.3fs", play.adsr.holdSeconds); break;
|
|
case ParamControl::kDecay:
|
|
snprintf(buf, sizeof(buf), "%.3fs", play.adsr.decaySeconds); break;
|
|
case ParamControl::kSustain:
|
|
snprintf(buf, sizeof(buf), "%.0f%%", play.adsr.sustainLevel * 100.0); break;
|
|
case ParamControl::kRelease:
|
|
snprintf(buf, sizeof(buf), "%.3fs", play.adsr.releaseSeconds); break;
|
|
case ParamControl::kTrigLength:
|
|
snprintf(buf, sizeof(buf), "%.0f%%", play.trigger.lengthFraction * 100.0); break;
|
|
case ParamControl::kTrigFadeIn:
|
|
snprintf(buf, sizeof(buf), "%lldf",
|
|
static_cast<long long>(play.trigger.fadeInFrames)); break;
|
|
case ParamControl::kTrigFadeOut:
|
|
snprintf(buf, sizeof(buf), "%lldf",
|
|
static_cast<long long>(play.trigger.fadeOutFrames)); break;
|
|
case ParamControl::kPitchEnvAttack:
|
|
snprintf(buf, sizeof(buf), "%.3fs", play.pitchEnv.attackSeconds); break;
|
|
case ParamControl::kPitchEnvDecay:
|
|
snprintf(buf, sizeof(buf), "%.3fs", play.pitchEnv.decaySeconds); break;
|
|
case ParamControl::kPitchEnvDepth:
|
|
snprintf(buf, sizeof(buf), "%+.1fst", play.pitchEnv.peakSemitones); break;
|
|
case ParamControl::kKeyTrack:
|
|
snprintf(buf, sizeof(buf), "%.0f%%", params_.keyTrack * 100.0); break;
|
|
case ParamControl::kVoiceCount:
|
|
snprintf(buf, sizeof(buf), "%d", voiceCount_); break;
|
|
case ParamControl::kMasterGain:
|
|
formatMasterGainLabel(deckControlNorm(id), buf, sizeof(buf)); break;
|
|
// Filter readouts run the stored normalized positions back through the module's OWN
|
|
// laws, so what the label says is what the kernel is solved for.
|
|
case ParamControl::kFilterMorph: {
|
|
const double m = play.filter.settings.morphNorm;
|
|
snprintf(buf, sizeof(buf), "%.0f%%", m * 100.0);
|
|
break;
|
|
}
|
|
case ParamControl::kFilterCutoff: {
|
|
const float hz = filterCutoffHzFromNorm(play.filter.settings.cutoffNorm);
|
|
if (hz >= 1000.0f) snprintf(buf, sizeof(buf), "%.2fk", hz / 1000.0f);
|
|
else snprintf(buf, sizeof(buf), "%.0fHz", hz);
|
|
break;
|
|
}
|
|
case ParamControl::kFilterQ:
|
|
snprintf(buf, sizeof(buf), "%.2f",
|
|
static_cast<double>(filterQFromNorm(play.filter.settings.resonanceNorm)));
|
|
break;
|
|
case ParamControl::kFilterDrive:
|
|
snprintf(buf, sizeof(buf), "%.2f",
|
|
static_cast<double>(filterDriveDepthFromNorm(play.filter.settings.driveNorm)));
|
|
break;
|
|
case ParamControl::kFilterModAmt:
|
|
snprintf(buf, sizeof(buf), "%+.0f%%", play.filter.modAmount * 100.0); break;
|
|
case ParamControl::kFilterVel:
|
|
snprintf(buf, sizeof(buf), "%+.0f%%", play.filter.velAmount * 100.0); break;
|
|
case ParamControl::kFilterKeyTrack:
|
|
snprintf(buf, sizeof(buf), "%.0f%%", play.filter.keyTrack * 100.0); break;
|
|
case ParamControl::kFilterEnvAttack:
|
|
snprintf(buf, sizeof(buf), "%.3fs", play.filter.env.attackSeconds); break;
|
|
case ParamControl::kFilterEnvHold:
|
|
snprintf(buf, sizeof(buf), "%.3fs", play.filter.env.holdSeconds); break;
|
|
case ParamControl::kFilterEnvDecay:
|
|
snprintf(buf, sizeof(buf), "%.3fs", play.filter.env.decaySeconds); break;
|
|
case ParamControl::kFilterEnvSustain:
|
|
snprintf(buf, sizeof(buf), "%.0f%%", play.filter.env.sustainLevel * 100.0); break;
|
|
case ParamControl::kFilterEnvRelease:
|
|
snprintf(buf, sizeof(buf), "%.3fs", play.filter.env.releaseSeconds); break;
|
|
default:
|
|
// -2 (preview velocity) is labeled at its chrome call site; nothing else here.
|
|
break;
|
|
}
|
|
return std::string(buf);
|
|
}
|
|
|
|
EnvClampBounds ReaSamplerEditor::envClampBounds() const {
|
|
// Match the control-panel sliders' own domains so a node drag can never produce a param a
|
|
// slider couldn't. AHDSR seconds cap at kEnvTimeMaxSeconds; the Trigger fade/length
|
|
// fractions cap at 1.0 (the natural full-span bound the sliders use).
|
|
EnvClampBounds b;
|
|
b.maxAttackSeconds = kEnvTimeMaxSeconds;
|
|
b.maxHoldSeconds = kEnvTimeMaxSeconds;
|
|
b.maxDecaySeconds = kEnvTimeMaxSeconds;
|
|
b.maxReleaseSeconds = kEnvTimeMaxSeconds;
|
|
b.maxFadeInFraction = 1.0;
|
|
b.maxFadeOutFraction = 1.0;
|
|
b.maxLengthFraction = 1.0;
|
|
return b;
|
|
}
|
|
|
|
AmpEnvelope ReaSamplerEditor::packEnvelope(const PlaySeconds& play, std::int64_t frames,
|
|
std::int64_t startFrame) const {
|
|
AmpEnvelope env;
|
|
env.mode = (play.playMode == PlayMode::Trigger) ? EnvMode::Trigger : EnvMode::Gate;
|
|
// AHDSR seconds copy 1-to-1 (rate-free, the same domain the overlay draws).
|
|
env.attackSeconds = play.adsr.attackSeconds;
|
|
env.holdSeconds = play.adsr.holdSeconds;
|
|
env.decaySeconds = play.adsr.decaySeconds;
|
|
env.sustainLevel = play.adsr.sustainLevel;
|
|
env.releaseSeconds = play.adsr.releaseSeconds;
|
|
// Trigger: lengthFraction copies 1-to-1; the fades are derived — source frames over the played
|
|
// span (the trigger-seam converter, pack direction). startFrame is the effective start
|
|
// point so the fraction denominator matches the voice's actual post-start span. A zero play
|
|
// length yields 0 fractions.
|
|
env.lengthFraction = play.trigger.lengthFraction;
|
|
const std::int64_t playLen =
|
|
triggerPlayLength(play.trigger.lengthFraction, frames, startFrame);
|
|
env.fadeInFraction = framesToFadeFraction(play.trigger.fadeInFrames, playLen);
|
|
env.fadeOutFraction = framesToFadeFraction(play.trigger.fadeOutFrames, playLen);
|
|
return env;
|
|
}
|
|
|
|
void ReaSamplerEditor::unpackEnvelope(const AmpEnvelope& env, std::int64_t frames,
|
|
std::int64_t startFrame, PlaySeconds& play) const {
|
|
if (env.mode == EnvMode::Gate) {
|
|
play.adsr.attackSeconds = env.attackSeconds;
|
|
play.adsr.holdSeconds = env.holdSeconds;
|
|
play.adsr.decaySeconds = env.decaySeconds;
|
|
play.adsr.sustainLevel = env.sustainLevel;
|
|
play.adsr.releaseSeconds = env.releaseSeconds;
|
|
} else {
|
|
// Trigger: lengthFraction copies back; the fades convert fractions -> source frames over
|
|
// the played span (the trigger-seam converter, unpack direction). startFrame is the
|
|
// effective start point so the frame denominator matches the voice's actual
|
|
// post-start span. Keep the same (0,1] floor on lengthFraction the slider path enforces
|
|
// so a zero-length trigger never plays nothing.
|
|
play.trigger.lengthFraction = (std::max)(0.01, env.lengthFraction);
|
|
const std::int64_t playLen =
|
|
triggerPlayLength(play.trigger.lengthFraction, frames, startFrame);
|
|
play.trigger.fadeInFrames = fadeFractionToFrames(env.fadeInFraction, playLen);
|
|
play.trigger.fadeOutFrames = fadeFractionToFrames(env.fadeOutFraction, playLen);
|
|
}
|
|
}
|
|
|
|
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.
|
|
params_.keyTrack = clamp01(value) * kKeyTrackMax;
|
|
} else {
|
|
applyControl(id, params_.play, value, segment);
|
|
}
|
|
}
|
|
|
|
} // namespace reasampler::vst
|