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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ void ReaSamplerEditor::onMouseUp(int x, int y) {
|
||||
const Rect curveRect = dragCurveRect_;
|
||||
drag_ = DragKind::kNone;
|
||||
dragParamId_ = -1;
|
||||
dragInnerCellId_ = -1;
|
||||
curvePointIndex_ = -1;
|
||||
// hover_ is deliberately not re-resolved during a drag (see resolveHover's caller), so it
|
||||
// still names wherever the drag started. Re-resolve now against the release position, for
|
||||
|
||||
@@ -19,6 +19,7 @@ using namespace reasampler::instrument::ui;
|
||||
bool ReaSamplerEditor::deckKnobDisabled(int id) const {
|
||||
switch (static_cast<ParamControl>(id)) {
|
||||
case ParamControl::kPitchEnvAttack:
|
||||
case ParamControl::kPitchEnvHold:
|
||||
case ParamControl::kPitchEnvDecay:
|
||||
case ParamControl::kPitchEnvDepth:
|
||||
return !params_.play.pitchEnv.enabled;
|
||||
@@ -34,6 +35,9 @@ bool ReaSamplerEditor::deckKnobDisabled(int id) const {
|
||||
case ParamControl::kFilterEnvDecay:
|
||||
case ParamControl::kFilterEnvSustain:
|
||||
case ParamControl::kFilterEnvRelease:
|
||||
case ParamControl::kFilterTrigAttack:
|
||||
case ParamControl::kFilterTrigHold:
|
||||
case ParamControl::kFilterTrigDecay:
|
||||
return !params_.play.filter.enabled;
|
||||
default:
|
||||
return false;
|
||||
@@ -46,6 +50,14 @@ bool ReaSamplerEditor::mouseDownDeck(const FaceLayout& fl, int x, int y) {
|
||||
|
||||
const DeckLayout dl = layoutDeck(fl.deckDescs, band.x, band.y, band.width);
|
||||
const DeckHit hit = hitTestDeck(dl, x, y);
|
||||
if (hit.kind == DeckHitKind::CaptionRadio) {
|
||||
// Exclusive across the three envelope decks, and clicking the active one clears it —
|
||||
// "no envelope shown" is a state the user can get back to, not an error.
|
||||
const OverlayEnv picked = overlayEnvForRadio(hit.id);
|
||||
overlayEnv_ = (overlayEnv_ == picked) ? OverlayEnv::kNone : picked;
|
||||
invalidate(); // view state only: no parameter write, no reload
|
||||
return true;
|
||||
}
|
||||
if (hit.kind == DeckHitKind::CaptionToggle || hit.kind == DeckHitKind::RowToggle) {
|
||||
switch (static_cast<ParamControl>(hit.id)) {
|
||||
case ParamControl::kVoiceMode: {
|
||||
@@ -87,9 +99,14 @@ bool ReaSamplerEditor::mouseDownDeck(const FaceLayout& fl, int x, int y) {
|
||||
if (hit.kind == DeckHitKind::Knob) {
|
||||
// Knobs of a disabled group are drawn but inert.
|
||||
if (deckKnobDisabled(hit.id)) return true;
|
||||
// A grab on the inner disc drags the CURVE control instead, but only where the stage
|
||||
// is sloped; on a Hold or Sustain cell the inner region is just more of the knob.
|
||||
const ParamControl curve = curveParamFor(static_cast<ParamControl>(hit.id));
|
||||
const bool inner = hit.inner && curve != ParamControl::kCount;
|
||||
drag_ = DragKind::kDeckKnob;
|
||||
dragParamId_ = hit.id;
|
||||
dragKnobStartValue_ = deckControlNorm(hit.id);
|
||||
dragParamId_ = inner ? static_cast<int>(curve) : hit.id;
|
||||
dragInnerCellId_ = inner ? hit.id : -1;
|
||||
dragKnobStartValue_ = deckControlNorm(dragParamId_);
|
||||
// Processor-side knobs (voice count / master gain) are transient live writes with no
|
||||
// parameter-set mutation, so they need no rollback snapshot.
|
||||
dragStartParams_ = params_;
|
||||
@@ -120,6 +137,12 @@ ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverDeck(const FaceLayout& fl,
|
||||
const DeckLayout dl = layoutDeck(fl.deckDescs, band.x, band.y, band.width);
|
||||
const DeckHit dh = hitTestDeck(dl, x, y);
|
||||
if (dh.kind == DeckHitKind::None) return {};
|
||||
if (dh.kind == DeckHitKind::CaptionRadio) return {HoverKind::kEnvRadio, dh.id};
|
||||
if (dh.kind == DeckHitKind::Knob && dh.inner &&
|
||||
curveParamFor(static_cast<ParamControl>(dh.id)) != ParamControl::kCount) {
|
||||
// Indexed by the OUTER cell id so the paint side can find the cell it belongs to.
|
||||
return {HoverKind::kInnerDial, dh.id};
|
||||
}
|
||||
return {HoverKind::kControl, dh.id};
|
||||
}
|
||||
|
||||
|
||||
@@ -28,11 +28,12 @@ bool ReaSamplerEditor::mouseDownWaveform(const FaceLayout& fl, int x, int y) {
|
||||
if (frames <= 0) return false;
|
||||
const OverlayArea overlay = waveformOverlayArea(fl.bands.waveform);
|
||||
|
||||
// Envelope nodes first (they sit on top of the markers), then the wave markers.
|
||||
// Envelope nodes first (they sit on top of the markers), then the wave markers. With no
|
||||
// envelope overlay-active there are no nodes at all and the markers take every grab.
|
||||
const double rate = liveSampleRate();
|
||||
if (rate > 0.0) {
|
||||
if (rate > 0.0 && overlayEnv_ != OverlayEnv::kNone) {
|
||||
const std::int64_t startFrame = params_.startPoint.value_or(0);
|
||||
const AmpEnvelope env = packEnvelope(params_.play, frames, startFrame);
|
||||
const StageEnvelope env = packEnvelope(overlayEnv_, params_.play, frames, startFrame);
|
||||
const double totalSeconds = static_cast<double>(frames) / rate;
|
||||
const NodeHit nh = nodeAtPoint(env, overlay, totalSeconds, x, y);
|
||||
if (nh.hit) {
|
||||
@@ -42,7 +43,6 @@ bool ReaSamplerEditor::mouseDownWaveform(const FaceLayout& fl, int x, int y) {
|
||||
dragStartY_ = y;
|
||||
dragStartEnv_ = env;
|
||||
dragSampleFrames_ = frames;
|
||||
dragStartFrame_ = startFrame;
|
||||
dragStartParams_ = params_;
|
||||
return true; // node moves once the cursor drags
|
||||
}
|
||||
@@ -68,18 +68,17 @@ void ReaSamplerEditor::dragWaveform(const FaceLayout& fl, int x, int y) {
|
||||
|
||||
if (drag_ == DragKind::kEnvNode) {
|
||||
// Resolve the grabbed envelope node's new params from the pixel delta (through the
|
||||
// pure envelope_edit inverse map, clamped + monotonic), then unpack them back onto
|
||||
// the parameter set. The AmpEnvelope was snapshotted at grab (dragStartEnv_) so the
|
||||
// delta is absolute.
|
||||
// pure envelope_edit inverse map, clamped), then unpack them back onto the parameter
|
||||
// set. The StageEnvelope was snapshotted at grab (dragStartEnv_) so the delta is
|
||||
// absolute.
|
||||
const std::int64_t frames = dragSampleFrames_;
|
||||
const double rate = liveSampleRate();
|
||||
if (frames <= 0 || rate <= 0.0) return;
|
||||
const double totalSeconds = static_cast<double>(frames) / rate;
|
||||
const AmpEnvelope edited = resolveNodeDrag(dragStartEnv_, envNode_, overlay, totalSeconds,
|
||||
envClampBounds(), dx, y - dragStartY_);
|
||||
unpackEnvelope(edited, frames, dragStartFrame_, params_.play);
|
||||
// In Gate the node IS a live AHDSR control, so the sounding note follows the drag;
|
||||
// Trigger's nodes rewrite the play span and still commit on release.
|
||||
const StageEnvelope edited = resolveNodeDrag(dragStartEnv_, envNode_, overlay,
|
||||
totalSeconds, envClampBounds(), dx,
|
||||
y - dragStartY_);
|
||||
unpackEnvelope(overlayEnv_, edited, params_.play);
|
||||
if (dragCommitsLive(DragKind::kEnvNode)) commitLive();
|
||||
invalidate(); // live feedback; commit on WM_LBUTTONUP
|
||||
return;
|
||||
|
||||
@@ -156,6 +156,42 @@ inline void drawKnobFace(LICE_IBitmap* bmp, const instrument::ui::Rect& knobRect
|
||||
toLice(ui::roleColor(needleRole)), 1.0f, 0, true);
|
||||
}
|
||||
|
||||
// The concentric INNER dial: a second value on the same cell, drawn in the categorical
|
||||
// tertiary accent so it reads as a different KIND of control rather than a louder one — the
|
||||
// same purple the overlay traces the envelope in, which is what ties a segment's knot to its
|
||||
// dial by eye. Shares the outer knob's value<->angle map (param_slider's), so both needles
|
||||
// point the same way for the same normalized value.
|
||||
inline void drawInnerDial(LICE_IBitmap* bmp, const instrument::ui::Rect& innerRect,
|
||||
double value01, ui::InteractionState st) {
|
||||
using instrument::ui::KnobArc;
|
||||
using instrument::ui::KnobGeometry;
|
||||
using instrument::ui::KnobPoint;
|
||||
const KnobGeometry kg = instrument::ui::computeKnob(innerRect);
|
||||
if (kg.radius <= 1.0) return;
|
||||
constexpr double kDegToRad = 3.14159265358979323846 / 180.0;
|
||||
const KnobArc arc{};
|
||||
const float cx = static_cast<float>(kg.centerX);
|
||||
const float cy = static_cast<float>(kg.centerY);
|
||||
const float r = static_cast<float>(kg.radius) - 0.5f;
|
||||
const bool disabled = (st == ui::InteractionState::Disabled);
|
||||
const bool hot = (st == ui::InteractionState::Dragging || st == ui::InteractionState::Hover);
|
||||
|
||||
LICE_FillCircle(bmp, cx, cy, r - 1.f, toLice(ui::roleColorState(ui::Role::BgPanel, st)), 1.0f,
|
||||
0, true);
|
||||
const double v = value01 < 0.0 ? 0.0 : (value01 > 1.0 ? 1.0 : value01);
|
||||
const float a0 = static_cast<float>((arc.startDeg - 360.0) * kDegToRad);
|
||||
const float av = static_cast<float>(
|
||||
(arc.startDeg + v * instrument::ui::knobSweepDeg(arc) - 360.0) * kDegToRad);
|
||||
const ui::Role arcRole = disabled ? ui::Role::TextDim
|
||||
: (hot ? ui::Role::AccentHot : ui::Role::AccentTertiary);
|
||||
LICE_Arc(bmp, cx, cy, r, a0, av, toLice(ui::roleColor(arcRole)), 1.0f, 0, true);
|
||||
const KnobPoint tip = instrument::ui::knobNeedlePoint(kg, arc, v);
|
||||
LICE_Line(bmp, static_cast<int>(cx + 0.5f), static_cast<int>(cy + 0.5f),
|
||||
static_cast<int>(tip.x + 0.5f), static_cast<int>(tip.y + 0.5f),
|
||||
toLice(ui::roleColor(disabled ? ui::Role::TextDim : ui::Role::AccentTertiary)),
|
||||
1.0f, 0, true);
|
||||
}
|
||||
|
||||
#endif // _WIN32
|
||||
|
||||
} // namespace reasampler::vst
|
||||
|
||||
@@ -60,11 +60,13 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
|
||||
case ParamControl::kDecay: return "Decay";
|
||||
case ParamControl::kSustain: return "Sustain";
|
||||
case ParamControl::kRelease: return "Release";
|
||||
case ParamControl::kTrigFadeIn: return "Fade In";
|
||||
case ParamControl::kTrigLength: return "Len %";
|
||||
case ParamControl::kTrigFadeOut: return "Fade Out";
|
||||
case ParamControl::kTrigAttack: return "Attack";
|
||||
case ParamControl::kTrigHold: return "Hold";
|
||||
case ParamControl::kTrigDecay: return "Decay";
|
||||
case ParamControl::kKeyTrack: return "Key Trk";
|
||||
case ParamControl::kPitchEnvAttack: return "P.Att";
|
||||
case ParamControl::kPitchEnvHold: return "P.Hold";
|
||||
case ParamControl::kPitchEnvDecay: return "P.Dec";
|
||||
case ParamControl::kPitchEnvDepth: return "P.Depth";
|
||||
case ParamControl::kVoiceCount: return "Voices";
|
||||
@@ -81,6 +83,9 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
|
||||
case ParamControl::kFilterEnvDecay: return "F.Dec";
|
||||
case ParamControl::kFilterEnvSustain: return "F.Sus";
|
||||
case ParamControl::kFilterEnvRelease: return "F.Rel";
|
||||
case ParamControl::kFilterTrigAttack: return "F.Att";
|
||||
case ParamControl::kFilterTrigHold: return "F.Hold";
|
||||
case ParamControl::kFilterTrigDecay: return "F.Dec";
|
||||
default: return "";
|
||||
}
|
||||
};
|
||||
@@ -103,6 +108,22 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
|
||||
}
|
||||
kitText(bmp, g.caption, caption, Font::Micro, Role::TextDim);
|
||||
|
||||
// The overlay-select radio: filled in the tertiary accent (the colour the overlay
|
||||
// traces in) when this group's envelope is the one on the waveform, hollow otherwise.
|
||||
if (g.captionRadio.id >= 0) {
|
||||
const bool on = (overlayEnv_ == overlayEnvForRadio(g.captionRadio.id));
|
||||
const bool hov = isHovered(HoverKind::kEnvRadio, g.captionRadio.id);
|
||||
const Rect& rb = g.captionRadio.box;
|
||||
LICE_DrawRect(bmp, rb.x, rb.y, rb.width - 1, rb.height - 1,
|
||||
toLice(roleColor(on || hov ? Role::AccentTertiary
|
||||
: Role::LineHairline)),
|
||||
1.0f, 0);
|
||||
if (on) {
|
||||
LICE_FillRect(bmp, rb.x + 3, rb.y + 3, rb.width - 6, rb.height - 6,
|
||||
toLice(roleColor(Role::AccentTertiary)), 1.0f, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// The compact caption toggle (right-anchored in the caption row, never full-width).
|
||||
if (g.captionToggle.id >= 0) {
|
||||
switch (static_cast<ParamControl>(g.captionToggle.id)) {
|
||||
@@ -142,7 +163,7 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
|
||||
// The knobs. A dependent group's knobs draw Disabled (not hidden) — stable geometry.
|
||||
// The predicate is the input side's, so the drawn state and the inert grab agree.
|
||||
for (const DeckCellLayout& c : g.cells) {
|
||||
if (c.id < 0) continue; // reserved blank cell (the Trigger face's two spares)
|
||||
if (c.id < 0) continue; // reserved blank cell (the Trigger face's spare)
|
||||
const bool disabled = deckKnobDisabled(c.id);
|
||||
const bool dragging = (drag_ == DragKind::kDeckKnob && dragParamId_ == c.id);
|
||||
const bool hov = !disabled && isHovered(HoverKind::kControl, c.id);
|
||||
@@ -151,9 +172,28 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
|
||||
: (dragging ? InteractionState::Dragging
|
||||
: (hov ? InteractionState::Hover : InteractionState::Rest));
|
||||
drawKnobFace(bmp, c.knob, deckControlNorm(c.id), st);
|
||||
const std::string label = (dragging || hov)
|
||||
? deckValueLabel(c.id)
|
||||
: std::string(knobName(static_cast<ParamControl>(c.id)));
|
||||
|
||||
// The inner dial rides only the knobs whose stage is sloped — deck_groups owns
|
||||
// that rule, so a Hold or Sustain cell simply has no curve id and draws none.
|
||||
const ParamControl curve = curveParamFor(static_cast<ParamControl>(c.id));
|
||||
const bool innerDragging =
|
||||
(drag_ == DragKind::kDeckKnob && dragInnerCellId_ == c.id);
|
||||
const bool innerHov = !disabled && isHovered(HoverKind::kInnerDial, c.id);
|
||||
if (curve != ParamControl::kCount) {
|
||||
const InteractionState ist =
|
||||
disabled ? InteractionState::Disabled
|
||||
: (innerDragging ? InteractionState::Dragging
|
||||
: (innerHov ? InteractionState::Hover
|
||||
: InteractionState::Rest));
|
||||
drawInnerDial(bmp, c.inner, deckControlNorm(static_cast<int>(curve)), ist);
|
||||
}
|
||||
|
||||
// One label band, so the inner dial's readout takes it while the inner dial is the
|
||||
// one being touched.
|
||||
std::string label;
|
||||
if (innerDragging || innerHov) label = deckValueLabel(static_cast<int>(curve));
|
||||
else if (dragging || hov) label = deckValueLabel(c.id);
|
||||
else label = std::string(knobName(static_cast<ParamControl>(c.id)));
|
||||
kitTextCentered(bmp, c.label, label.c_str(), Font::Micro, Role::TextDim);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,37 +100,49 @@ void ReaSamplerEditor::paintWaveform(LICE_IBitmap* bmp, const Rect& band) {
|
||||
|
||||
void ReaSamplerEditor::paintEnvelopeOverlay(LICE_IBitmap* bmp, const OverlayArea& waveArea,
|
||||
std::int64_t frames) {
|
||||
if (overlayEnv_ == OverlayEnv::kNone) return; // no envelope selected is a resting state
|
||||
const Rect& area = waveArea.rect;
|
||||
if (frames <= 0 || area.width <= 0 || area.height <= 0) return;
|
||||
const double rate = liveSampleRate();
|
||||
if (rate <= 0.0) return;
|
||||
const double totalSeconds = static_cast<double>(frames) / rate;
|
||||
const std::int64_t startFrame = params_.startPoint.value_or(0);
|
||||
const AmpEnvelope env = packEnvelope(params_.play, frames, startFrame);
|
||||
const StageEnvelope env = packEnvelope(overlayEnv_, params_.play, frames, startFrame);
|
||||
const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, waveArea, totalSeconds);
|
||||
|
||||
// Trace the polyline in the categorical secondary accent (teal) so it reads as a distinct
|
||||
// curve over the waveform. Clip x to the wave rect (a Gate release tail maps past the right).
|
||||
const LICE_pixel line = toLice(roleColor(Role::AccentSecondary));
|
||||
for (std::size_t i = 1; i < poly.size(); ++i) {
|
||||
const int x0 = (std::max)(area.x, (std::min)(area.right() - 1, poly[i - 1].x));
|
||||
const int x1 = (std::max)(area.x, (std::min)(area.right() - 1, poly[i].x));
|
||||
LICE_Line(bmp, x0, poly[i - 1].y, x1, poly[i].y, line, 1.0f, 0, true);
|
||||
// Trace the polyline in the categorical TERTIARY accent (purple): the waveform behind it is
|
||||
// drawn in the primary lime, and the secondary teal this used to use sits too close to that
|
||||
// hue to separate from it. Clip x to the wave rect. Knots are handles, not line vertices.
|
||||
const LICE_pixel line = toLice(roleColor(Role::AccentTertiary));
|
||||
const EnvVertex* prev = nullptr;
|
||||
for (const EnvVertex& v : poly) {
|
||||
if (v.knot) continue;
|
||||
if (prev != nullptr) {
|
||||
const int x0 = (std::max)(area.x, (std::min)(area.right() - 1, prev->x));
|
||||
const int x1 = (std::max)(area.x, (std::min)(area.right() - 1, v.x));
|
||||
LICE_Line(bmp, x0, prev->y, x1, v.y, line, 1.0f, 0, true);
|
||||
}
|
||||
prev = &v;
|
||||
}
|
||||
// Draggable node handles: a small square per draggable node (Origin + ReleaseStart are
|
||||
// draw-only). Lit accent-hot when this node is the grabbed one. Every vertex is
|
||||
// guaranteed in-bounds (edge nodes like ReleaseEnd at area.right()-1 must get handles);
|
||||
// the handle square is additionally clamped inside the band so a 6px box on an edge
|
||||
// node never overhangs into the neighbouring bands.
|
||||
const LICE_pixel handle = toLice(roleColor(Role::AccentPrimary));
|
||||
// Handles: a square per draggable stage node, a ROUND knot per curvable segment. Lit
|
||||
// accent-hot when this node is the grabbed one. Every vertex is guaranteed in-bounds; the
|
||||
// handle is additionally clamped inside the band so one on an edge node never overhangs
|
||||
// into the neighbouring bands.
|
||||
const LICE_pixel handle = toLice(roleColor(Role::AccentTertiary));
|
||||
const LICE_pixel handleHot = toLice(roleColor(Role::AccentHot));
|
||||
for (const EnvVertex& v : poly) {
|
||||
if (v.node == EnvNode::Origin || v.node == EnvNode::ReleaseStart) continue;
|
||||
if (v.node == EnvNode::Origin || v.node == EnvNode::ReleaseEnd) continue;
|
||||
const bool grabbed = (drag_ == DragKind::kEnvNode && envNode_ == v.node);
|
||||
const int r = 3;
|
||||
const int hx = (std::max)(area.x + r, (std::min)(area.right() - 1 - r, v.x));
|
||||
const int hy = (std::max)(area.y + r, (std::min)(area.bottom() - 1 - r, v.y));
|
||||
LICE_FillRect(bmp, hx - r, hy - r, 2 * r, 2 * r, grabbed ? handleHot : handle, 1.0f, 0);
|
||||
if (v.knot) {
|
||||
LICE_FillCircle(bmp, static_cast<float>(hx), static_cast<float>(hy),
|
||||
static_cast<float>(r), grabbed ? handleHot : handle, 1.0f, 0, true);
|
||||
} else {
|
||||
LICE_FillRect(bmp, hx - r, hy - r, 2 * r, 2 * r, grabbed ? handleHot : handle, 1.0f,
|
||||
0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -244,6 +244,7 @@ LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam,
|
||||
}
|
||||
self->drag_ = DragKind::kNone;
|
||||
self->dragParamId_ = -1;
|
||||
self->dragInnerCellId_ = -1; // inner-dial drag state (peer reset)
|
||||
self->curvePointIndex_ = -1; // curve-node drag state (peer reset)
|
||||
// No cursor position is available here to re-resolve hover (unlike
|
||||
// onMouseUp's release coordinates), so clear rather than leave it naming
|
||||
|
||||
@@ -151,7 +151,7 @@ bool ReaSamplerEditor::dragCommitsLive(DragKind kind, int paramId) const {
|
||||
const LiveDragKind k = kind == DragKind::kDeckKnob ? LiveDragKind::kDeckKnob
|
||||
: kind == DragKind::kEnvNode ? LiveDragKind::kEnvNode
|
||||
: LiveDragKind::kOther;
|
||||
return instrument::ui::liveCommitFor(k, paramId, params_.play.playMode);
|
||||
return instrument::ui::liveCommitFor(k, paramId);
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::loadSelection(const std::string& id) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#include "core/instrument/ui/deck_groups.h" // DeckParam / DeckGroupId / sampleDeckGroups
|
||||
#include "core/instrument/ui/editor_geometry.h" // Rect (shared sub-rect type)
|
||||
#include "core/instrument/ui/envelope_edit.h" // EnvClampBounds / NodeHit (envelope node hit-test/edit)
|
||||
#include "core/instrument/ui/envelope_overlay.h" // AmpEnvelope / EnvNode (envelope overlay draw seam)
|
||||
#include "core/instrument/ui/envelope_overlay.h" // StageEnvelope / EnvNode (envelope overlay draw seam)
|
||||
#include "core/instrument/ui/knob_deck.h" // DeckGroupDesc / DeckLayout (the deck band)
|
||||
#include "core/instrument/ui/sample_bands.h" // SampleBands (the band-stack allocator)
|
||||
#include "core/instrument/ui/sample_chrome.h" // ChromeRects (chrome-band interior)
|
||||
@@ -41,7 +41,6 @@ using instrument::map::PlaySeconds;
|
||||
using instrument::map::SampleChoice;
|
||||
using instrument::map::SampleRefEntry;
|
||||
using instrument::map::SampleRefs;
|
||||
using instrument::ui::AmpEnvelope;
|
||||
using instrument::ui::ChromeRects;
|
||||
using instrument::ui::DeckGroupDesc;
|
||||
using instrument::ui::EnvClampBounds;
|
||||
@@ -49,6 +48,7 @@ using instrument::ui::EnvNode;
|
||||
using instrument::ui::OverlayArea;
|
||||
using instrument::ui::Rect;
|
||||
using instrument::ui::SampleBands;
|
||||
using instrument::ui::StageEnvelope;
|
||||
|
||||
class ReaSamplerProcessor;
|
||||
|
||||
@@ -81,6 +81,11 @@ private:
|
||||
enum class DragKind { kNone, kRootMarker, kWaveMarker, kScrollThumb, kEnvNode,
|
||||
kCurveNode, kDeckKnob };
|
||||
|
||||
// Which envelope the waveform overlay is drawing and editing. Exclusive, and kNone is a
|
||||
// valid resting state — the editor opens there. Transient view state: never persisted,
|
||||
// never a parameter.
|
||||
enum class OverlayEnv { kNone, kAmp, kPitch, kFilter };
|
||||
|
||||
// Controls on the setup surface. The int value is the opaque control id the pure
|
||||
// knob_deck hit-test returns; the shell maps it to the one parameter set or a
|
||||
// processor-side per-instance setter. The id space and the deck's group composition are
|
||||
@@ -107,6 +112,8 @@ private:
|
||||
kChanStereo, // the stereo channel-mode segment
|
||||
kPreview, // the preview-trigger button
|
||||
kControl, // a knob-deck element (index = control id)
|
||||
kInnerDial, // a knob cell's inner curve dial (index = the OUTER control id)
|
||||
kEnvRadio, // an envelope deck's overlay-select radio (index = radio control id)
|
||||
kCurveNode, // a velocity-curve control point (index = point index)
|
||||
kVelKnob, // the chrome preview-velocity radial knob
|
||||
kStripKey, // a piano-strip key (index = MIDI note); carries the name tooltip
|
||||
@@ -302,8 +309,8 @@ private:
|
||||
// pitch envelope) — wall-clock seconds, rate-free; the build resolves to frames.
|
||||
|
||||
// The normalized [0,1] display value for control `id` given `play` (seconds -> 0..1 over
|
||||
// a fixed ceiling, sustain 0..1 as-is, %-length/fade frames -> 0..1, semitone depth
|
||||
// centered at 0.5).
|
||||
// a fixed ceiling, levels and fractions as-is, semitone depth centered at 0.5, curve
|
||||
// exponents over their logarithmic travel).
|
||||
double controlValue(int id, const PlaySeconds& play) const;
|
||||
|
||||
// Applies a committed control interaction to `play`: a knob's normalized `value` or a
|
||||
@@ -315,22 +322,22 @@ private:
|
||||
// over the knob's 0..1).
|
||||
void applyParamControl(int id, double value, int segment);
|
||||
|
||||
// The Trigger fade-in/out knob full-scale, in source frames: kFadeMaxSeconds resolved
|
||||
// against the live rate — never a baked-in rate. Returns 0 when the rate is unknown.
|
||||
double fadeMaxFrames() const;
|
||||
// The overlay speaks one StageEnvelope whichever envelope is active; pack/unpack are the
|
||||
// only place that knows which stored struct each `which` maps onto, so the drawn shape and
|
||||
// a committed node drag can never disagree about it. AHDSR seconds are rate-free and copy
|
||||
// 1-to-1; an AHD additionally needs the wall-clock span its Hold fraction is taken against,
|
||||
// which is where `frames`/`startFrame` and the live rate come in.
|
||||
|
||||
// envelope_overlay's AmpEnvelope stores Trigger fades as fractions of the played span,
|
||||
// while the parameter set stores source frames — pack/unpack own that conversion (see
|
||||
// envelope_overlay.h's trigger-seam note). `frames` is total source frames; AHDSR
|
||||
// seconds are rate-free and copy 1-to-1.
|
||||
// PACK (draw): play params -> StageEnvelope. `startFrame` is the effective start point.
|
||||
StageEnvelope packEnvelope(OverlayEnv which, const PlaySeconds& play, std::int64_t frames,
|
||||
std::int64_t startFrame) const;
|
||||
|
||||
// PACK (draw): play params -> AmpEnvelope. `startFrame` is the effective start point.
|
||||
AmpEnvelope packEnvelope(const PlaySeconds& play, std::int64_t frames,
|
||||
std::int64_t startFrame) const;
|
||||
// UNPACK (commit): an edited StageEnvelope -> the play params, in place.
|
||||
void unpackEnvelope(OverlayEnv which, const StageEnvelope& env, PlaySeconds& play) const;
|
||||
|
||||
// UNPACK (commit): an edited AmpEnvelope -> the play params, in place.
|
||||
void unpackEnvelope(const AmpEnvelope& env, std::int64_t frames, std::int64_t startFrame,
|
||||
PlaySeconds& play) const;
|
||||
// The radio control id that selects `which`, and its inverse. One table, so the deck's
|
||||
// radio and the overlay can never drift apart.
|
||||
static OverlayEnv overlayEnvForRadio(int radioId);
|
||||
|
||||
// Clamp bounds envelope_edit uses, matching the sliders' own domains so a node drag can
|
||||
// never produce a param a slider couldn't.
|
||||
@@ -417,16 +424,21 @@ private:
|
||||
WaveMarker waveMarker_ = WaveMarker::kStart;
|
||||
SetupMarkers dragStartMarkers_;
|
||||
std::int64_t dragSampleFrames_ = 0; // decoded length of the sample under the drag
|
||||
std::int64_t dragStartFrame_ = 0; // effective start point at grab time; for env-node drag
|
||||
|
||||
// Scrollbar-thumb drag: the offset at grab time. kDeckKnob drag: which control id.
|
||||
int dragStartScrollOffset_ = 0;
|
||||
int dragParamId_ = -1; // control id under a kDeckKnob drag; -2 = preview-vel knob
|
||||
// The cell whose INNER dial is under a kDeckKnob drag (dragParamId_ then holds the curve
|
||||
// control), so the paint side can light the right ring. -1 when the grab was the outer knob.
|
||||
int dragInnerCellId_ = -1;
|
||||
|
||||
// Envelope-node drag: which node + the AmpEnvelope snapshotted at grab (absolute-delta
|
||||
// Which envelope the overlay draws and edits (kNone = none, the opening state).
|
||||
OverlayEnv overlayEnv_ = OverlayEnv::kNone;
|
||||
|
||||
// Envelope-node drag: which node + the StageEnvelope snapshotted at grab (absolute-delta
|
||||
// contract, per envelope_edit's grabEnv).
|
||||
EnvNode envNode_ = EnvNode::Origin;
|
||||
AmpEnvelope dragStartEnv_{};
|
||||
StageEnvelope dragStartEnv_{};
|
||||
|
||||
// Velocity-curve node drag: which point, the curve snapshotted at grab
|
||||
// (resolvePointDrag's absolute-delta contract), and the grab-time box rect.
|
||||
|
||||
Reference in New Issue
Block a user