instrument: one staged-envelope system — per-segment curves, the sustain-less AHD, and a shared overlay for all three envelopes
Trigger's fade pair folds into the AHD (and goes live); the release anchors right; Preserve rings its synthetic tail out instead of cutting it. Payload v10.
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
// 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.
|
||||
// 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.
|
||||
|
||||
#include "shell/instrument/reasampler_editor.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath> // log/exp (the curve knob's logarithmic travel)
|
||||
#include <cstdint>
|
||||
#include <cstdio> // snprintf (deck value labels)
|
||||
#include <string>
|
||||
@@ -14,17 +16,17 @@
|
||||
|
||||
#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/map/trigger_seam.h" // triggerPlayLength (the Trigger play span)
|
||||
#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 "core/util/curve_law.h" // the ONE curve-exponent domain
|
||||
#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 namespace reasampler::instrument::map; // PlaySeconds vocabulary + trigger_seam
|
||||
using instrument::ui::computeSampleBands;
|
||||
using instrument::ui::chromeRects;
|
||||
using instrument::ui::deckHeight;
|
||||
@@ -44,17 +46,26 @@ 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
|
||||
// only 0..1). Every stage-time knob spans [0, kEnvTimeMaxSeconds] seconds — rate-free, exactly
|
||||
// what the parameter set stores; the build resolves seconds->frames at the live rate. No knob
|
||||
// on this surface stores a source-frame count any more, so none needs a rate to draw.
|
||||
constexpr double kEnvTimeMaxSeconds = 2.0; // every stage-time knob's ceiling (seconds)
|
||||
constexpr double kPitchDepthMaxSemis = 24.0; // pitch depth throw: +/-24 st, centered
|
||||
constexpr double kKeyTrackMax = 2.0; // key-track slider ceiling (0..200%)
|
||||
|
||||
// A curve exponent's knob travel is LOGARITHMIC: 0.5 is the linear neutral, so the two halves
|
||||
// of the throw are the reciprocal shaping directions and the neutral sits at a centre detent.
|
||||
double curveFromNorm(double norm) {
|
||||
const double t = clamp01(norm);
|
||||
return std::exp(std::log(util::kCurveMin) +
|
||||
t * (std::log(util::kCurveMax) - std::log(util::kCurveMin)));
|
||||
}
|
||||
double normFromCurve(double curve) {
|
||||
const double c = util::clampCurve(curve);
|
||||
return clamp01((std::log(c) - std::log(util::kCurveMin)) /
|
||||
(std::log(util::kCurveMax) - std::log(util::kCurveMin)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ReaSamplerEditor::FaceLayout ReaSamplerEditor::faceLayout(int w, int h) const {
|
||||
@@ -69,16 +80,9 @@ ReaSamplerEditor::FaceLayout ReaSamplerEditor::faceLayout(int w, int h) const {
|
||||
}
|
||||
|
||||
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();
|
||||
// Wall-clock seconds -> normalized over the seconds ceiling; fractions and normalized
|
||||
// control positions pass through; curve exponents take the log travel.
|
||||
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;
|
||||
@@ -87,12 +91,23 @@ double ReaSamplerEditor::controlValue(int id, const PlaySeconds& play) const {
|
||||
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::kAttackCurve: return normFromCurve(play.adsr.attackCurve);
|
||||
case ParamControl::kDecayCurve: return normFromCurve(play.adsr.decayCurve);
|
||||
case ParamControl::kReleaseCurve: return normFromCurve(play.adsr.releaseCurve);
|
||||
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::kTrigAttack: return secToNorm(play.trigAhd.attackSeconds);
|
||||
case ParamControl::kTrigHold: return clamp01(play.trigAhd.holdFraction);
|
||||
case ParamControl::kTrigDecay: return secToNorm(play.trigAhd.decaySeconds);
|
||||
case ParamControl::kTrigAttackCurve: return normFromCurve(play.trigAhd.attackCurve);
|
||||
case ParamControl::kTrigDecayCurve: return normFromCurve(play.trigAhd.decayCurve);
|
||||
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::kPitchEnvAttack:return secToNorm(play.pitchEnv.shape.attackSeconds);
|
||||
case ParamControl::kPitchEnvHold: return clamp01(play.pitchEnv.shape.holdFraction);
|
||||
case ParamControl::kPitchEnvDecay: return secToNorm(play.pitchEnv.shape.decaySeconds);
|
||||
case ParamControl::kPitchEnvAttackCurve:
|
||||
return normFromCurve(play.pitchEnv.shape.attackCurve);
|
||||
case ParamControl::kPitchEnvDecayCurve:
|
||||
return normFromCurve(play.pitchEnv.shape.decayCurve);
|
||||
case ParamControl::kPitchEnvDepth:
|
||||
// Signed depth centered at 0.5 (0.5 == 0 semitones).
|
||||
return clamp01(0.5 + play.pitchEnv.peakSemitones / (2.0 * kPitchDepthMaxSemis));
|
||||
@@ -113,19 +128,26 @@ double ReaSamplerEditor::controlValue(int id, const PlaySeconds& play) const {
|
||||
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);
|
||||
case ParamControl::kFilterEnvAttackCurve:
|
||||
return normFromCurve(play.filter.env.attackCurve);
|
||||
case ParamControl::kFilterEnvDecayCurve:
|
||||
return normFromCurve(play.filter.env.decayCurve);
|
||||
case ParamControl::kFilterEnvReleaseCurve:
|
||||
return normFromCurve(play.filter.env.releaseCurve);
|
||||
case ParamControl::kFilterTrigAttack: return secToNorm(play.filter.trigEnv.attackSeconds);
|
||||
case ParamControl::kFilterTrigHold: return clamp01(play.filter.trigEnv.holdFraction);
|
||||
case ParamControl::kFilterTrigDecay: return secToNorm(play.filter.trigEnv.decaySeconds);
|
||||
case ParamControl::kFilterTrigAttackCurve:
|
||||
return normFromCurve(play.filter.trigEnv.attackCurve);
|
||||
case ParamControl::kFilterTrigDecayCurve:
|
||||
return normFromCurve(play.filter.trigEnv.decayCurve);
|
||||
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;
|
||||
@@ -138,17 +160,33 @@ void ReaSamplerEditor::applyControl(int id, PlaySeconds& play, double value,
|
||||
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::kAttackCurve: play.adsr.attackCurve = curveFromNorm(value); break;
|
||||
case ParamControl::kDecayCurve: play.adsr.decayCurve = curveFromNorm(value); break;
|
||||
case ParamControl::kReleaseCurve: play.adsr.releaseCurve = curveFromNorm(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::kTrigAttack: play.trigAhd.attackSeconds = normToSec(value); break;
|
||||
case ParamControl::kTrigHold: play.trigAhd.holdFraction = clamp01(value); break;
|
||||
case ParamControl::kTrigDecay: play.trigAhd.decaySeconds = normToSec(value); break;
|
||||
case ParamControl::kTrigAttackCurve:
|
||||
play.trigAhd.attackCurve = curveFromNorm(value); break;
|
||||
case ParamControl::kTrigDecayCurve:
|
||||
play.trigAhd.decayCurve = curveFromNorm(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::kPitchEnvAttack:
|
||||
play.pitchEnv.shape.attackSeconds = normToSec(value); break;
|
||||
case ParamControl::kPitchEnvHold:
|
||||
play.pitchEnv.shape.holdFraction = clamp01(value); break;
|
||||
case ParamControl::kPitchEnvDecay:
|
||||
play.pitchEnv.shape.decaySeconds = normToSec(value); break;
|
||||
case ParamControl::kPitchEnvAttackCurve:
|
||||
play.pitchEnv.shape.attackCurve = curveFromNorm(value); break;
|
||||
case ParamControl::kPitchEnvDecayCurve:
|
||||
play.pitchEnv.shape.decayCurve = curveFromNorm(value); break;
|
||||
case ParamControl::kPitchEnvDepth:
|
||||
play.pitchEnv.peakSemitones = (clamp01(value) - 0.5) * 2.0 * kPitchDepthMaxSemis;
|
||||
break;
|
||||
@@ -179,6 +217,22 @@ void ReaSamplerEditor::applyControl(int id, PlaySeconds& play, double value,
|
||||
play.filter.env.sustainLevel = clamp01(value); break;
|
||||
case ParamControl::kFilterEnvRelease:
|
||||
play.filter.env.releaseSeconds = normToSec(value); break;
|
||||
case ParamControl::kFilterEnvAttackCurve:
|
||||
play.filter.env.attackCurve = curveFromNorm(value); break;
|
||||
case ParamControl::kFilterEnvDecayCurve:
|
||||
play.filter.env.decayCurve = curveFromNorm(value); break;
|
||||
case ParamControl::kFilterEnvReleaseCurve:
|
||||
play.filter.env.releaseCurve = curveFromNorm(value); break;
|
||||
case ParamControl::kFilterTrigAttack:
|
||||
play.filter.trigEnv.attackSeconds = normToSec(value); break;
|
||||
case ParamControl::kFilterTrigHold:
|
||||
play.filter.trigEnv.holdFraction = clamp01(value); break;
|
||||
case ParamControl::kFilterTrigDecay:
|
||||
play.filter.trigEnv.decaySeconds = normToSec(value); break;
|
||||
case ParamControl::kFilterTrigAttackCurve:
|
||||
play.filter.trigEnv.attackCurve = curveFromNorm(value); break;
|
||||
case ParamControl::kFilterTrigDecayCurve:
|
||||
play.filter.trigEnv.decayCurve = curveFromNorm(value); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -187,18 +241,6 @@ 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;
|
||||
@@ -267,16 +309,18 @@ std::string ReaSamplerEditor::deckValueLabel(int id) const {
|
||||
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::kTrigAttack:
|
||||
snprintf(buf, sizeof(buf), "%.3fs", play.trigAhd.attackSeconds); break;
|
||||
case ParamControl::kTrigHold:
|
||||
snprintf(buf, sizeof(buf), "%.0f%%", play.trigAhd.holdFraction * 100.0); break;
|
||||
case ParamControl::kTrigDecay:
|
||||
snprintf(buf, sizeof(buf), "%.3fs", play.trigAhd.decaySeconds); break;
|
||||
case ParamControl::kPitchEnvAttack:
|
||||
snprintf(buf, sizeof(buf), "%.3fs", play.pitchEnv.attackSeconds); break;
|
||||
snprintf(buf, sizeof(buf), "%.3fs", play.pitchEnv.shape.attackSeconds); break;
|
||||
case ParamControl::kPitchEnvHold:
|
||||
snprintf(buf, sizeof(buf), "%.0f%%", play.pitchEnv.shape.holdFraction * 100.0); break;
|
||||
case ParamControl::kPitchEnvDecay:
|
||||
snprintf(buf, sizeof(buf), "%.3fs", play.pitchEnv.decaySeconds); break;
|
||||
snprintf(buf, sizeof(buf), "%.3fs", play.pitchEnv.shape.decaySeconds); break;
|
||||
case ParamControl::kPitchEnvDepth:
|
||||
snprintf(buf, sizeof(buf), "%+.1fst", play.pitchEnv.peakSemitones); break;
|
||||
case ParamControl::kKeyTrack:
|
||||
@@ -322,6 +366,27 @@ std::string ReaSamplerEditor::deckValueLabel(int id) const {
|
||||
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;
|
||||
case ParamControl::kFilterTrigAttack:
|
||||
snprintf(buf, sizeof(buf), "%.3fs", play.filter.trigEnv.attackSeconds); break;
|
||||
case ParamControl::kFilterTrigHold:
|
||||
snprintf(buf, sizeof(buf), "%.0f%%", play.filter.trigEnv.holdFraction * 100.0); break;
|
||||
case ParamControl::kFilterTrigDecay:
|
||||
snprintf(buf, sizeof(buf), "%.3fs", play.filter.trigEnv.decaySeconds); break;
|
||||
// Every curve exponent reads the same way: the neutral shows as 1.00.
|
||||
case ParamControl::kAttackCurve:
|
||||
case ParamControl::kDecayCurve:
|
||||
case ParamControl::kReleaseCurve:
|
||||
case ParamControl::kTrigAttackCurve:
|
||||
case ParamControl::kTrigDecayCurve:
|
||||
case ParamControl::kPitchEnvAttackCurve:
|
||||
case ParamControl::kPitchEnvDecayCurve:
|
||||
case ParamControl::kFilterEnvAttackCurve:
|
||||
case ParamControl::kFilterEnvDecayCurve:
|
||||
case ParamControl::kFilterEnvReleaseCurve:
|
||||
case ParamControl::kFilterTrigAttackCurve:
|
||||
case ParamControl::kFilterTrigDecayCurve:
|
||||
snprintf(buf, sizeof(buf), "^%.2f", curveFromNorm(controlValue(id, play)));
|
||||
break;
|
||||
default:
|
||||
// -2 (preview velocity) is labeled at its chrome call site; nothing else here.
|
||||
break;
|
||||
@@ -330,61 +395,118 @@ std::string ReaSamplerEditor::deckValueLabel(int id) const {
|
||||
}
|
||||
|
||||
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).
|
||||
// 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.
|
||||
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;
|
||||
ReaSamplerEditor::OverlayEnv ReaSamplerEditor::overlayEnvForRadio(int radioId) {
|
||||
switch (static_cast<ParamControl>(radioId)) {
|
||||
case ParamControl::kAmpEnvSelect: return OverlayEnv::kAmp;
|
||||
case ParamControl::kPitchEnvSelect: return OverlayEnv::kPitch;
|
||||
case ParamControl::kFilterEnvSelect: return OverlayEnv::kFilter;
|
||||
default: return OverlayEnv::kNone;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
env.fadeInFraction = framesToFadeFraction(play.trigger.fadeInFrames, playLen);
|
||||
env.fadeOutFraction = framesToFadeFraction(play.trigger.fadeOutFrames, playLen);
|
||||
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:
|
||||
case OverlayEnv::kNone:
|
||||
if (trigger) packAhd(play.trigAhd, t0, playSpan, env);
|
||||
else packAhdsr(play.adsr, env);
|
||||
break;
|
||||
}
|
||||
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::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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user