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

294 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 the value labels), 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: no painting, no window plumbing.
#include "shell/instrument/reasampler_editor.h"
#include <algorithm>
#include <cstdint>
#include <cstdio> // snprintf (deck value labels)
#include <string>
#include <vector>
#include "core/instrument/engine/filter/filter_params.h" // the filter's own control laws
#include "core/instrument/engine/master_gain.h" // master-gain dB<->linear<->knob taper
#include "core/instrument/ui/deck_groups.h" // sampleDeckGroups (the deck's composition)
#include "core/instrument/ui/deck_values.h" // the parameter-set binding + its ms units
#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::formatEnvTimeMs;
using instrument::ui::kEnvTimeMaxSeconds;
using instrument::ui::kKeyTrackMax;
using instrument::ui::resetDeckParam;
using instrument::ui::sampleDeckGroups;
using instrument::ui::setDeckParam;
using instrument::engine::formatMasterGainLabel;
using instrument::engine::masterGainLinearFromNorm;
using instrument::engine::masterGainNormFromLinear;
using instrument::engine::filter::filterCutoffHzFromNorm;
using instrument::engine::filter::filterDriveDepthFromNorm;
using instrument::engine::filter::filterQFromNorm;
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 wrapped height is the only interior measurement the allocator needs.
FaceLayout fl;
fl.deckDescs = sampleDeckGroups(params_.play.playMode);
fl.bands = computeSampleBands(w, h, deckHeight(fl.deckDescs, w - 2 * kPad));
fl.chrome = chromeRects(fl.bands.chrome, kDeckKnobSize);
return fl;
}
// 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::deckControlNorm(int id) const {
if (id == -2) return previewVelocity01(); // the chrome preview-velocity knob
switch (static_cast<ParamControl>(id)) {
case ParamControl::kKeyTrack:
return clamp01(params_.keyTrack / kKeyTrackMax);
case ParamControl::kVoiceCount:
return clamp01(static_cast<double>(voiceCount_ - kMinVoiceCount) /
static_cast<double>(kMaxVoiceCount - kMinVoiceCount));
case ParamControl::kMasterGain:
return masterGainNormFromLinear(processor_ ? processor_->masterGainLinear() : 1.0);
default:
return controlValue(id, params_.play);
}
}
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};
}
void ReaSamplerEditor::applyDeckKnob(int id, double norm) {
if (!processor_) return;
norm = clamp01(norm);
if (id == -2) {
// Preview velocity: live processor write (persisted per-instance; the setter clamps
// to MIDI 1..127 so the knob's bottom still strikes audibly).
processor_->setPreviewVelocity(static_cast<std::uint8_t>(norm * 127.0 + 0.5));
return;
}
switch (static_cast<ParamControl>(id)) {
case ParamControl::kVoiceCount: {
// Stepped: quantize the continuous drag to the integer count and track it live
// for the label/needle. The actual engine rebuild (setVoiceCount) fires ONCE on
// WM_LBUTTONUP — not per step — so a full drag (~31 steps) costs one rebuild,
// not thirty.
const int count =
kMinVoiceCount +
static_cast<int>(norm * (kMaxVoiceCount - kMinVoiceCount) + 0.5);
voiceCount_ = count;
return;
}
case ParamControl::kMasterGain:
// Post-mixer gain: one atomic store; the audio thread picks it up next block.
processor_->setMasterGainLinear(masterGainLinearFromNorm(norm));
return;
default:
applyParamControl(id, norm, 0);
return;
}
}
std::string ReaSamplerEditor::deckValueLabel(int id) const {
char buf[24];
buf[0] = '\0';
const PlaySeconds& play = params_.play;
switch (id == -2 ? ParamControl::kCount : static_cast<ParamControl>(id)) {
case ParamControl::kAttack:
formatEnvTimeMs(play.adsr.attackSeconds, buf, sizeof(buf)); break;
case ParamControl::kHold:
formatEnvTimeMs(play.adsr.holdSeconds, buf, sizeof(buf)); break;
case ParamControl::kDecay:
formatEnvTimeMs(play.adsr.decaySeconds, buf, sizeof(buf)); break;
case ParamControl::kSustain:
snprintf(buf, sizeof(buf), "%.0f%%", play.adsr.sustainLevel * 100.0); break;
case ParamControl::kRelease:
formatEnvTimeMs(play.adsr.releaseSeconds, buf, sizeof(buf)); break;
case ParamControl::kTrigLength:
snprintf(buf, sizeof(buf), "%.0f%%", play.trigger.lengthFraction * 100.0); break;
case ParamControl::kTrigAttack:
formatEnvTimeMs(play.trigAhd.attackSeconds, buf, sizeof(buf)); break;
case ParamControl::kTrigHold:
snprintf(buf, sizeof(buf), "%.0f%%", play.trigAhd.holdFraction * 100.0); break;
case ParamControl::kTrigDecay:
formatEnvTimeMs(play.trigAhd.decaySeconds, buf, sizeof(buf)); break;
case ParamControl::kPitchEnvAttack:
formatEnvTimeMs(play.pitchEnv.shape.attackSeconds, buf, sizeof(buf)); break;
case ParamControl::kPitchEnvHold:
snprintf(buf, sizeof(buf), "%.0f%%", play.pitchEnv.shape.holdFraction * 100.0); break;
case ParamControl::kPitchEnvDecay:
formatEnvTimeMs(play.pitchEnv.shape.decaySeconds, buf, sizeof(buf)); break;
case ParamControl::kPitchEnvDepth:
snprintf(buf, sizeof(buf), "%+.1fst", play.pitchEnv.peakSemitones); break;
case ParamControl::kKeyTrack:
snprintf(buf, sizeof(buf), "%.0f%%", params_.keyTrack * 100.0); break;
case ParamControl::kVoiceCount:
snprintf(buf, sizeof(buf), "%d", voiceCount_); break;
case ParamControl::kMasterGain:
formatMasterGainLabel(deckControlNorm(id), buf, sizeof(buf)); break;
// Filter readouts run the stored normalized positions back through the module's OWN
// laws, so what the label says is what the kernel is solved for.
case ParamControl::kFilterMorph: {
const double m = play.filter.settings.morphNorm;
snprintf(buf, sizeof(buf), "%.0f%%", m * 100.0);
break;
}
case ParamControl::kFilterCutoff: {
const float hz = filterCutoffHzFromNorm(play.filter.settings.cutoffNorm);
if (hz >= 1000.0f) snprintf(buf, sizeof(buf), "%.2fk", hz / 1000.0f);
else snprintf(buf, sizeof(buf), "%.0fHz", hz);
break;
}
case ParamControl::kFilterQ:
snprintf(buf, sizeof(buf), "%.2f",
static_cast<double>(filterQFromNorm(play.filter.settings.resonanceNorm)));
break;
case ParamControl::kFilterDrive:
snprintf(buf, sizeof(buf), "%.2f",
static_cast<double>(filterDriveDepthFromNorm(play.filter.settings.driveNorm)));
break;
case ParamControl::kFilterModAmt:
snprintf(buf, sizeof(buf), "%+.0f%%", play.filter.modAmount * 100.0); break;
case ParamControl::kFilterVel:
snprintf(buf, sizeof(buf), "%+.0f%%", play.filter.velAmount * 100.0); break;
case ParamControl::kFilterKeyTrack:
snprintf(buf, sizeof(buf), "%.0f%%", play.filter.keyTrack * 100.0); break;
case ParamControl::kFilterEnvAttack:
formatEnvTimeMs(play.filter.env.attackSeconds, buf, sizeof(buf)); break;
case ParamControl::kFilterEnvHold:
formatEnvTimeMs(play.filter.env.holdSeconds, buf, sizeof(buf)); break;
case ParamControl::kFilterEnvDecay:
formatEnvTimeMs(play.filter.env.decaySeconds, buf, sizeof(buf)); break;
case ParamControl::kFilterEnvSustain:
snprintf(buf, sizeof(buf), "%.0f%%", play.filter.env.sustainLevel * 100.0); break;
case ParamControl::kFilterEnvRelease:
formatEnvTimeMs(play.filter.env.releaseSeconds, buf, sizeof(buf)); break;
case ParamControl::kFilterTrigAttack:
formatEnvTimeMs(play.filter.trigEnv.attackSeconds, buf, sizeof(buf)); break;
case ParamControl::kFilterTrigHold:
snprintf(buf, sizeof(buf), "%.0f%%", play.filter.trigEnv.holdFraction * 100.0); break;
case ParamControl::kFilterTrigDecay:
formatEnvTimeMs(play.filter.trigEnv.decaySeconds, buf, sizeof(buf)); 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", curveExponentFor(id, play));
break;
default:
// -2 (preview velocity) is labeled at its chrome call site; nothing else here.
break;
}
return std::string(buf);
}
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 (0..200% over kKeyTrackMax); the knob maps 0..1.
params_.keyTrack = clamp01(value) * kKeyTrackMax;
} else {
applyControl(id, params_.play, value, segment);
}
}
} // namespace reasampler::vst