Files
reasampler/src/shell/instrument/editor_controls.cpp
T

285 lines
14 KiB
C++

// editor_controls.cpp — the ReaSamplerEditor's parameter plumbing: the band-stack layout
// resolve every paint/hit-test path shares, the shell's half of the control-value binding (the
// per-instance controls the parameter set does not carry — key-track, voice count, master gain,
// preview velocity — plus each knob's plain value and its label), and the node-drag clamp
// bounds. The parameter-set half is the pure `deck_values` module. The orthogonal half — which
// stored struct each editor selection names — is editor_models. Value logic only.
#include "shell/instrument/reasampler_editor.h"
#include <algorithm>
#include <cmath> // isfinite (the gain's -inf label)
#include <cstdint>
#include <cstdio> // snprintf (deck value labels)
#include <string>
#include <vector>
#include "core/instrument/engine/master_gain.h" // master-gain dB<->linear<->knob taper
#include "core/instrument/param/param_format.h" // THE formatter every value label reads through
#include "core/instrument/param/param_id.h" // whether a control has a parameter row at all
#include "core/instrument/ui/bake_hold.h" // the Hold knob's ladder map
#include "core/instrument/ui/deck_groups.h" // sampleDeckGroups (the deck's composition)
#include "core/instrument/ui/deck_values.h" // the parameter-set binding
#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
using instrument::ui::computeSampleBands;
using instrument::ui::chromeRects;
using instrument::ui::deckHeight;
using instrument::ui::kDeckKnobSize;
using instrument::ui::kPad;
using instrument::ui::deckParamNorm;
using instrument::ui::kEnvTimeMaxSeconds;
using instrument::ui::resetDeckParam;
using instrument::ui::sampleDeckGroups;
using instrument::ui::setDeckParam;
using instrument::engine::masterGainLinearFromNorm;
using instrument::engine::masterGainNormFromLinear;
using util::clamp01;
namespace {
// The raw stored curve exponent for a curve-dial control id, read DIRECTLY off the field —
// never round-tripped through curveFromKnobNorm(knobNormFromCurve(x)): the knob-norm law's
// centre detent (curve_law.h) snaps anything near-neutral back to exactly 1.0, so a round trip
// can misreport a stored exponent that isn't neutral as "^1.00".
double curveExponentFor(int id, const PlaySeconds& play) {
using DeckParam = instrument::ui::DeckParam; // ReaSamplerEditor::ParamControl is an alias
switch (static_cast<DeckParam>(id)) {
case DeckParam::kAttackCurve: return play.adsr.attackCurve;
case DeckParam::kDecayCurve: return play.adsr.decayCurve;
case DeckParam::kReleaseCurve: return play.adsr.releaseCurve;
case DeckParam::kTrigAttackCurve: return play.trigAhd.attackCurve;
case DeckParam::kTrigDecayCurve: return play.trigAhd.decayCurve;
case DeckParam::kPitchEnvAttackCurve: return play.pitchEnv.shape.attackCurve;
case DeckParam::kPitchEnvDecayCurve: return play.pitchEnv.shape.decayCurve;
case DeckParam::kFilterEnvAttackCurve: return play.filter.env.attackCurve;
case DeckParam::kFilterEnvDecayCurve: return play.filter.env.decayCurve;
case DeckParam::kFilterEnvReleaseCurve: return play.filter.env.releaseCurve;
case DeckParam::kFilterTrigAttackCurve: return play.filter.trigEnv.attackCurve;
case DeckParam::kFilterTrigDecayCurve: return play.filter.trigEnv.decayCurve;
default: return util::kCurveNeutral;
}
}
} // 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 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));
fl.chrome = chromeRects(fl.bands.chrome, kDeckKnobSize);
return fl;
}
// The parameter-set binding is the pure deck_values module's; these three are the shell's thin
// int-id adapters onto it (ParamControl is an alias of DeckParam).
double ReaSamplerEditor::controlValue(int id, const PlaySeconds& play) const {
return deckParamNorm(static_cast<instrument::ui::DeckParam>(id), play);
}
void ReaSamplerEditor::applyControl(int id, PlaySeconds& play, double value,
int segment) const {
setDeckParam(static_cast<instrument::ui::DeckParam>(id), play, value, segment);
}
void ReaSamplerEditor::resetParamControl(int id) {
// kKeyTrack is the one knob whose value sits beside the play bundle, so it defaults from
// InstrumentParams rather than from PlaySeconds — the same split applyParamControl draws.
if (id == static_cast<int>(ParamControl::kKeyTrack)) {
params_.keyTrack = InstrumentParams{}.keyTrack;
return;
}
resetDeckParam(static_cast<instrument::ui::DeckParam>(id), params_.play);
}
double ReaSamplerEditor::liveSampleRate() const {
return processor_ ? processor_->sampleRate() : 0.0;
}
double ReaSamplerEditor::previewVelocity01() const {
if (!processor_) return static_cast<double>(kPreviewVelocityDefault) / 127.0;
return static_cast<double>(processor_->previewVelocity()) / 127.0;
}
double ReaSamplerEditor::bakeHoldNorm() const {
return instrument::ui::bakeHoldNorm(params_.bakeHold);
}
double ReaSamplerEditor::deckControlNorm(int id) const {
if (id == -2) return previewVelocity01(); // the chrome preview-velocity knob
if (id == kBakeHoldKnobId) return bakeHoldNorm();
switch (static_cast<ParamControl>(id)) {
case ParamControl::kKeyTrack:
return instrument::ui::keyTrackNormFrom(params_.keyTrack);
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);
}
}
instrument::ui::DeckEnableState ReaSamplerEditor::deckEnableState() const {
const PlaySeconds& play = params_.play;
return instrument::ui::DeckEnableState{
play.pitchEnv.enabled, play.filter.enabled,
play.ampSpline.mode == EnvMode::Spline,
play.pitchSpline.mode == EnvMode::Spline,
play.filterSpline.mode == EnvMode::Spline};
}
bool ReaSamplerEditor::loopControlsLive() const {
return effectivePlayMode(params_.play) == PlayMode::Gate;
}
instrument::ui::WaveMarks ReaSamplerEditor::waveMarksFor(const SetupMarkers& m) const {
using instrument::ui::WaveMark;
instrument::ui::WaveMarks w;
w.frame[static_cast<int>(WaveMark::kStart)] = m.start;
w.frame[static_cast<int>(WaveMark::kLoopStart)] = m.loopStart;
w.frame[static_cast<int>(WaveMark::kLoopEnd)] = m.loopEnd;
// The crossfade grows LEFT from the seam it closes, which is where it is audible.
w.frame[static_cast<int>(WaveMark::kCrossfade)] = m.loopEnd - m.crossfade;
// Trigger has no loop at all, so the pair and the fade are ABSENT rather than shown in an
// off state — a mark whose gesture the mode does not offer was read as broken, not as off.
// Gate keeps the pair whatever the enable says: that is the drag-to-set-loop affordance.
// The crossfade mark belongs to an ACTIVE loop: with the enable off there is no seam for it
// to sit on, and no length to drag.
const bool gate = loopControlsLive();
w.present[static_cast<int>(WaveMark::kStart)] = true;
w.present[static_cast<int>(WaveMark::kLoopStart)] = gate;
w.present[static_cast<int>(WaveMark::kLoopEnd)] = gate;
w.present[static_cast<int>(WaveMark::kCrossfade)] = gate && m.hasLoop;
return w;
}
instrument::ui::WaveMarks ReaSamplerEditor::grabbableMarks(const SetupMarkers& m) const {
// Drawn IFF grabbable is the product rule, so this is an exact alias of waveMarksFor and
// cannot currently diverge from it. Kept as its own seam anyway because paint and hit-test
// are separate questions in principle — but do NOT re-add a suppression here: the
// Gate-with-loop-off marks are drawn grey precisely so they can still be dragged, and
// dragging one is what turns the enable on.
return waveMarksFor(m);
}
void ReaSamplerEditor::setLoopEnabled(bool on) {
const auto frames = static_cast<std::int64_t>(monoPcmFor(selectedId_).size());
if (frames <= 0) return;
SetupMarkers m = pickedMarkers(frames);
if (m.hasLoop == on) return; // a no-op commit would buy a re-decode for nothing
m.hasLoop = on;
applyMarkers(m);
commitAndReload();
}
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;
}
if (id == kBakeHoldKnobId) {
// A parameter-set edit like the deck's own knobs: the commit lands on release.
params_.bakeHold = instrument::ui::bakeHoldFromNorm(norm);
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;
}
}
double ReaSamplerEditor::deckPlainValue(int id) const {
const auto deck = static_cast<instrument::ui::DeckParam>(id);
// A curve exponent is read off its stored field, never round-tripped through the knob law:
// that law's centre detent snaps anything near-neutral back to exactly 1.0, so a round trip
// would misreport a stored exponent that isn't neutral as 1.00. The host has only the norm
// and therefore cannot make this distinction — param/CLAUDE.md records the divergence.
if (instrument::ui::deckParamUnit(deck) == instrument::ui::UnitCategory::Exponent) {
return curveExponentFor(id, params_.play);
}
return instrument::param::toPlain(deck, deckControlNorm(id));
}
std::string ReaSamplerEditor::deckValueLabel(int id) const {
if (id < 0 || id >= static_cast<int>(ParamControl::kCount)) return {};
const auto deck = static_cast<instrument::ui::DeckParam>(id);
// The one deck knob with no plain-value layer at all: an already-integer count.
if (deck == ParamControl::kVoiceCount) {
char buf[24];
snprintf(buf, sizeof(buf), "%d", voiceCount_);
return std::string(buf);
}
if (instrument::param::paramIdFor(deck) == 0) return {};
// The digits come from the ONE formatter; everything the editor adds around them is static
// chrome — a constant prefix or suffix cannot diverge from what the host shows.
const double plain = deckPlainValue(id);
char digits[24];
instrument::param::formatPlainFor(deck, plain, digits, sizeof(digits));
const auto kind = instrument::param::unitKindFor(deck);
const char* caret =
instrument::ui::deckParamUnit(deck) == instrument::ui::UnitCategory::Exponent ? "^" : "";
// The gain at true silence reads "-inf", not "-infdB": there is no decibel value there.
if (kind == instrument::param::UnitKind::Decibels && !std::isfinite(plain)) {
return std::string(digits);
}
// The stage times are the one category that carries a space before its unit, and always did —
// this surface's own typography, not the host's (ParameterInfo::units is the bare string).
const char* gap = kind == instrument::param::UnitKind::Time ? " " : "";
return caret + std::string(digits) + gap + instrument::param::unitStringFor(deck);
}
EnvClampBounds ReaSamplerEditor::envClampBounds() const {
// Lives here, beside controlValue/applyControl, because it must MATCH them: a node drag can
// never produce a param a knob couldn't. Every stage time caps at kEnvTimeMaxSeconds; the
// Hold fractions and the sustain level are [0,1] by definition and need no bound here.
EnvClampBounds b;
b.maxAttackSeconds = kEnvTimeMaxSeconds;
b.maxHoldSeconds = kEnvTimeMaxSeconds;
b.maxDecaySeconds = kEnvTimeMaxSeconds;
b.maxReleaseSeconds = kEnvTimeMaxSeconds;
return b;
}
void ReaSamplerEditor::applyParamControl(int id, double value, int segment) {
if (id == static_cast<int>(ParamControl::kKeyTrack)) {
// keyTrack sits beside the play bundle, so it takes deck_values' own pair rather than
// the PlaySeconds binding.
params_.keyTrack = instrument::ui::keyTrackFromNorm(value);
} else {
applyControl(id, params_.play, value, segment);
}
}
} // namespace reasampler::vst