Report the instrument's automatable parameters to the host under a frozen id table, in signal-flow order, with real units
42 of 44 ids issued: pitch key-track and Trigger length stay reserved pending a live path. Master gain reclassified Live — it never reloaded.
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
// instrument_params.cpp — the VST3 adapter over core/instrument/param: the Parameter subclass
|
||||
// whose toPlain/toNormalized ARE the taper, the one construction of the unit and parameter
|
||||
// lists, and the model projection both directions. It DECIDES nothing — the pure module owns
|
||||
// the frozen table, the laws and the formatter.
|
||||
//
|
||||
// The blob stays authoritative. A parameter is a THIRD SURFACE onto InstrumentParams/PlaySeconds
|
||||
// — a peer of the deck knob and the overlay node, never a second copy of the value. getState
|
||||
// serializes the model; the controller's own value list is a cache written FROM the model and
|
||||
// never read as truth.
|
||||
|
||||
#include "shell/instrument/reasampler_processor.h"
|
||||
|
||||
#include "base/source/fstring.h"
|
||||
#include "pluginterfaces/base/ustring.h"
|
||||
|
||||
#include "core/instrument/engine/master_gain.h"
|
||||
#include "core/instrument/param/param_format.h"
|
||||
#include "core/instrument/param/param_id.h"
|
||||
#include "core/instrument/param/param_units.h"
|
||||
#include "core/instrument/ui/deck_values.h"
|
||||
|
||||
using namespace Steinberg;
|
||||
using namespace Steinberg::Vst;
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
namespace param = instrument::param;
|
||||
using instrument::ui::DeckParam;
|
||||
|
||||
namespace {
|
||||
|
||||
void assign128(String128 dst, const char* src) {
|
||||
UString(dst, str16BufferSize(String128)).fromAscii(src);
|
||||
}
|
||||
|
||||
// One class for all of them: the law is per-control data inside the pure module, so a subclass
|
||||
// per unit category would model nothing that a DeckParam does not already say.
|
||||
class DeckParameter : public Parameter {
|
||||
public:
|
||||
explicit DeckParameter(const param::ParamRow& row) : deck_(row.deck) {
|
||||
assign128(info.title, row.title);
|
||||
assign128(info.shortTitle, row.shortTitle);
|
||||
assign128(info.units, param::unitStringFor(row.deck));
|
||||
info.id = row.id;
|
||||
info.unitId = row.unit;
|
||||
// Continuous, every one of them — and structurally so rather than by luck: stepCount > 0
|
||||
// is only meaningful for a discrete control, and every discrete control is reload or
|
||||
// rebuild tier and therefore not exposed at all. The editor's shift-snap is a DRAG
|
||||
// interaction and must never be published here: stepCount quantizes the parameter
|
||||
// permanently, for the host's automation too, and freezes into the forever contract.
|
||||
info.stepCount = 0;
|
||||
// COMPUTED from the default, never a normalized literal — a hand-written normalized
|
||||
// default is a second source of truth for it and drifts from the taper silently.
|
||||
info.defaultNormalizedValue = param::defaultNormalized(row.deck);
|
||||
// No kIsBypass on anything: the plugin is an instrument and exposes no bypass.
|
||||
info.flags = ParameterInfo::kCanAutomate;
|
||||
valueNormalized = info.defaultNormalizedValue;
|
||||
}
|
||||
|
||||
ParamValue toPlain(ParamValue normalized) const SMTG_OVERRIDE {
|
||||
return param::toPlain(deck_, normalized);
|
||||
}
|
||||
ParamValue toNormalized(ParamValue plain) const SMTG_OVERRIDE {
|
||||
return param::toNormalized(deck_, plain);
|
||||
}
|
||||
void toString(ParamValue normalized, String128 out) const SMTG_OVERRIDE {
|
||||
char digits[24];
|
||||
param::formatPlainFor(deck_, param::toPlain(deck_, normalized), digits, sizeof(digits));
|
||||
assign128(out, digits);
|
||||
}
|
||||
bool fromString(const TChar* text, ParamValue& normalized) const SMTG_OVERRIDE {
|
||||
String str(text);
|
||||
str.toMultiByte(kCP_Utf8);
|
||||
double plain = 0.0;
|
||||
if (!param::parsePlain(param::unitKindFor(deck_), str.text8(), plain)) return false;
|
||||
normalized = param::toNormalized(deck_, plain);
|
||||
return true;
|
||||
}
|
||||
|
||||
OBJ_METHODS(DeckParameter, Parameter)
|
||||
|
||||
private:
|
||||
DeckParam deck_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void ReaSamplerProcessor::buildParameterList() {
|
||||
// One unit per deck group that carries an exposed parameter, so a host can present the list
|
||||
// under its group names rather than as one flat run.
|
||||
struct UnitDesc { UnitID id; const char* name; };
|
||||
static const UnitDesc kUnits[] = {
|
||||
{param::kUnitPitch, "Pitch"},
|
||||
{param::kUnitPitchEnv, "Pitch Env"},
|
||||
{param::kUnitFilter, "Filter"},
|
||||
{param::kUnitFilterEnv, "Filter Env"},
|
||||
{param::kUnitAmp, "Amp Env"},
|
||||
{param::kUnitMaster, "Master"},
|
||||
};
|
||||
for (const UnitDesc& u : kUnits) {
|
||||
String128 name;
|
||||
assign128(name, u.name);
|
||||
addUnit(new Unit(name, u.id));
|
||||
}
|
||||
// Ascending id IS the presentation order, which is what makes identity order and
|
||||
// presentation order agree by construction rather than by maintenance.
|
||||
for (const param::ParamRow& row : param::exposedParams()) {
|
||||
parameters.addParameter(new DeckParameter(row));
|
||||
}
|
||||
}
|
||||
|
||||
tresult PLUGIN_API ReaSamplerProcessor::setParamNormalized(ParamID tag, ParamValue value) {
|
||||
const param::ParamRow* row = param::exposedRowFor(tag);
|
||||
if (!row) return kResultFalse;
|
||||
// Notification is suppressed for the duration: this write CAME from the host, and echoing it
|
||||
// back through performEdit would let a lane in write mode re-record its own playback.
|
||||
const bool wasSuppressed = paramNotifySuppressed_;
|
||||
paramNotifySuppressed_ = true;
|
||||
// The host's write takes the control's EXISTING commit tier and no other. Nothing here can
|
||||
// reach reloadInstrument or rebuildVoiceEngine, and that is structural: every reload- and
|
||||
// rebuild-tier control is omitted from the list, so no id maps to one.
|
||||
if (row->deck == DeckParam::kMasterGain) {
|
||||
setMasterGainLinear(instrument::engine::masterGainLinearFromNorm(value));
|
||||
} else {
|
||||
InstrumentParams params = instrumentParams();
|
||||
instrument::ui::setDeckParam(row->deck, params.play, value, /*segment=*/0);
|
||||
setInstrumentParams(params);
|
||||
publishLiveParams();
|
||||
}
|
||||
paramNotifySuppressed_ = wasSuppressed;
|
||||
// The container caches what the MODEL took, not what the host sent — a control whose write
|
||||
// clamped would otherwise read back the out-of-range value the clamp rejected.
|
||||
return EditControllerEx1::setParamNormalized(
|
||||
tag, modelParamNormalized(instrumentParams(), row->deck));
|
||||
}
|
||||
|
||||
double ReaSamplerProcessor::modelParamNormalized(const InstrumentParams& params,
|
||||
DeckParam deck) const {
|
||||
if (deck == DeckParam::kMasterGain) {
|
||||
return instrument::engine::masterGainNormFromLinear(masterGainLinear());
|
||||
}
|
||||
return instrument::ui::deckParamNorm(deck, params.play);
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::syncParamsFromModel() {
|
||||
const InstrumentParams params = instrumentParams();
|
||||
for (const param::ParamRow& row : param::exposedParams()) {
|
||||
// EditControllerEx1's own setter, NOT ours: this is the LOAD direction, and the SDK is
|
||||
// explicit that a controller must never pass a load back to the host through
|
||||
// IComponentHandler — it updates the GUI element only.
|
||||
EditControllerEx1::setParamNormalized(row.id, modelParamNormalized(params, row.deck));
|
||||
}
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::notifyParamsFromModel(const InstrumentParams& before,
|
||||
const InstrumentParams& after) {
|
||||
if (paramNotifySuppressed_) return;
|
||||
for (const param::ParamRow& row : param::exposedParams()) {
|
||||
if (row.deck == DeckParam::kMasterGain) continue; // its own funnel notifies it
|
||||
const double now = modelParamNormalized(after, row.deck);
|
||||
if (now == modelParamNormalized(before, row.deck)) continue;
|
||||
notifyParamChanged(row.id, now);
|
||||
}
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::notifyParamChanged(param::ParamId id, double normalized) {
|
||||
EditControllerEx1::setParamNormalized(id, normalized);
|
||||
if (!componentHandler) return;
|
||||
// A drag holds its own begin/end across the whole gesture so a host in touch or latch mode
|
||||
// sees one continuous edit; every other writer — a reset, an envelope-node drag, the bake's
|
||||
// reset — emits a degenerate one-point gesture, which is what makes the host DISPLAY follow
|
||||
// it instead of re-imposing the pre-write value on the next touch.
|
||||
const bool inGesture = openGestureId_ == id;
|
||||
if (!inGesture) beginEdit(id);
|
||||
performEdit(id, normalized);
|
||||
if (!inGesture) endEdit(id);
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::beginParamGesture(DeckParam deck) {
|
||||
const param::ParamId id = param::paramIdFor(deck);
|
||||
if (id == 0 || !param::isExposed(deck)) return;
|
||||
endParamGesture(); // a grab while one is open cannot leave the previous unclosed
|
||||
openGestureId_ = id;
|
||||
beginEdit(id);
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::endParamGesture() {
|
||||
if (openGestureId_ == 0) return;
|
||||
const param::ParamId id = openGestureId_;
|
||||
openGestureId_ = 0; // cleared FIRST: endEdit can re-enter through a host's own callback
|
||||
endEdit(id);
|
||||
}
|
||||
|
||||
} // namespace reasampler::vst
|
||||
Reference in New Issue
Block a user