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:
2026-08-02 15:14:16 -04:00
parent c7afa3a80f
commit bfaa0f2614
38 changed files with 1742 additions and 218 deletions
+32 -117
View File
@@ -13,11 +13,12 @@
#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/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 + its ms units
#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
@@ -33,18 +34,13 @@ 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 {
@@ -220,117 +216,36 @@ void ReaSamplerEditor::applyDeckKnob(int id, double norm) {
}
}
std::string ReaSamplerEditor::deckValueLabel(int id) const {
char buf[24];
buf[0] = '\0';
const PlaySeconds& play = params_.play;
switch (id < 0 ? 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::kRate: {
// One decimal below 100 % only: the taper is linear in semitones, so the lower half
// spends 50 percentage points on the same twelve semitones the upper half spends
// 100 on — a whole percent is twice as coarse a step down there.
const double pct = play.playRate * 100.0;
snprintf(buf, sizeof(buf), pct < 100.0 ? "%.1f%%" : "%.0f%%", pct);
break;
}
case ParamControl::kPitch:
snprintf(buf, sizeof(buf), "%+.1fst", play.pitchOffsetSemitones); 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:
// The chrome knobs (preview velocity, bake Hold) are labeled at their own call
// site; nothing else here.
break;
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 std::string(buf);
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.
char digits[24];
instrument::param::formatPlainFor(deck, deckPlainValue(id), digits, sizeof(digits));
const char* caret =
instrument::ui::deckParamUnit(deck) == instrument::ui::UnitCategory::Exponent ? "^" : "";
return caret + std::string(digits) + instrument::param::unitStringFor(deck);
}
EnvClampBounds ReaSamplerEditor::envClampBounds() const {