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:
@@ -17,6 +17,11 @@ subdirectories:
|
||||
- **`bake/`** — the resample bake's pure half: the programmed note resolved to a frame
|
||||
window, the offline render over a voice engine built for that render alone, and the
|
||||
ratified post-bake reset. See `bake/CLAUDE.md`.
|
||||
- **`param/`** — what the instrument tells a VST3 host about its automatable parameters,
|
||||
with no VST3 type in it: the FOREVER-FROZEN id table, the exposed set derived from
|
||||
`deckParamCommit`, the plain-value layer, and the one formatter per unit category. Sits
|
||||
ABOVE `ui/` — the list is a function of the commit predicate, never the reverse. See
|
||||
`param/CLAUDE.md`.
|
||||
- **`ui/`** — pure editor geometry/hit-test modules (the band-stack allocator and its band
|
||||
interiors, waveform, keyboard strip, capture browser, param controls, envelope
|
||||
overlay/edit). These are geometry-and-math only; the LICE draw + REAPER/VST3 plumbing is
|
||||
@@ -309,7 +314,7 @@ anything for a trigger shape.
|
||||
never buy itself a higher lowest-findable fundamental.
|
||||
- `time_stretch` — the TIME half beside `pitch_shift`'s PITCH half, header-only: `StretchCursor`, the per-output-frame source-feed schedule (a fractional cursor carrying its rate debt, loop-wrapped), plus the rate bounds and their clamp. Rate 1.0 is exactly one source frame per output frame with no residue, which is what makes the unity Preserve read bit-identical to the pre-stretch engine. The bounds are **measured**, not arbitrary — see the header.
|
||||
- `velocity_curve` — THE monotone spline, shared by every consumer: the three velocity transfer curves and the three spline EGs. `VelocityCurve` is evaluated as ONE OR MORE Fritsch–Carlson monotone cubic Hermite splines joined at its HARD points — a hard knot is a sub-curve boundary for tangent purposes (exactly what the point array's own ends already are), so the two adjacent segments meet at their natural angle instead of a shared derivative and the no-overshoot guarantee holds PER SEGMENT rather than globally. Points are smooth by default; the ceiling is `kMaxCurvePoints` = 128, a MUSICAL bound (long rhythmic phrases, ~two points per articulation event) and not a performance one — **do not lower it**. `eval(velocity)` is the COLD reader, called once per note-on or once per drawn pixel column; `SplineCursor` is the RT one, an indexed segment search plus one Hermite evaluation with the segment and its tangents cached across samples. Both share the same `segmentTangents`/`hermiteAt` free functions, so there is one spline and not two. It carries its own y `CurveDomain`: UNIPOLAR [0,1] is the amp's GAIN, defaulting to `flat()` (y=1, every velocity→unity — a deliberate non-back-compat replacement of the old fixed `velocity/127` path, Daniel-approved); BIPOLAR [−1,1] is the signed modulation shape for pitch and filter, defaulting to `zero()` so velocity modulates neither until a curve is drawn. A bipolar curve does not imply the absence of a depth beside it: the filter keeps its `velAmount` knob and the two compose multiplicatively (`velAmount × curve.eval(v)`, `play_params.h`), while the pitch curve's throw is the fixed `kVelocityPitchRangeSemitones`.
|
||||
- `master_gain` — pure dB↔linear taper math (FB1): normalized [0,1] ↔ dB ↔ linear for the post-mixer master gain control (−∞…+24 dB, norm 0 = true silence, unity ≈ 0.714). Shared by the editor knob and the processor multiply so the needle, persisted value, and audio multiply cannot drift.
|
||||
- `master_gain` — pure dB↔linear taper math (FB1): normalized [0,1] ↔ dB ↔ linear for the post-mixer master gain control (−∞…+24 dB, norm 0 = true silence, unity ≈ 0.714). Shared by the editor knob, the processor multiply and the host's `toPlain` so the needle, persisted value, audio multiply and reported dB cannot drift. Math only — the dB label is `param/param_format`'s, so the editor and the host cannot print it two ways.
|
||||
- `limiter` — the master bus's lookahead brickwall limiter, the stage after `master_gain`'s multiply: a 4x-oversampled TRUE-PEAK detector in the SIDECHAIN ONLY (the signal path is never oversampled), one stereo-linked gain, a baked −0.3 dBTP ceiling and **no makeup gain of any kind**. The gain law is a sliding MINIMUM of the per-sample target over the lookahead window followed by a MOVING AVERAGE of the same width: every term of that average is a minimum whose own window contains the sample being gained, so the ceiling is held **structurally** rather than by a tuned attack, and the one-pole release only ever slows the RISE so that bound survives it. Bypassed and settled, `process()` returns without reading or writing a sample — the byte-identical at-rest path, on the same discipline as `live == nullptr` and the filter's exact skip at `modAmount == 0`. `prepare()` owns every allocation and every transcendental. **Switching is a MUTE, never a blend:** unlimited signal is emitted at weight 1 (the untouched bypass buffer) or at weight 0 and never in between, because a fraction of an unlimited signal is a peak over the ceiling — so the fade always rides the limited path and the hard edge always lands on the bypassed side, against silence. Do not reintroduce an equal-gain dry/wet crossfade over the toggle.
|
||||
- `meter_ballistics` — the output meter's UI-side ballistics and dB scale: instantaneous rise, 20 dB/s fall, the 1.5 s peak hold and its release at the same rate, the clip latch, and the dB → normalized map over −60…+6 dBFS. The audio thread publishes raw block peaks and converts nothing; this module is what turns them into what the bar draws. Per-channel and stage-agnostic — the MASTER column's own state (both channels plus the gain-reduction lamp) composes it in `ui/master_meter`.
|
||||
|
||||
@@ -349,10 +354,10 @@ anything for a trigger shape.
|
||||
value is COPIED rather than round-tripped: that taper bypass is mandatory and must never be
|
||||
"simplified" back into a norm round trip), `deckParamUnit`/`snapDeckParamNorm` (THE snap-unit
|
||||
table, and where each control's full scale enters — a whole DISPLAYED percent is a different
|
||||
norm step at 0..100 %, 0..200 % and ±100 %), and `formatEnvTimeMs`, the
|
||||
ONE time-constant formatter: every displayed time constant reads in **ms**, never seconds, so
|
||||
two stage times are comparable at a glance. A display-unit decision only — nothing about the
|
||||
stored representation changes. Links the header-only `play_seconds`, deliberately not
|
||||
norm step at 0..100 %, 0..200 % and ±100 %). Display FORMATTING is not here — `param/`'s
|
||||
`param_format` owns the one formatter per unit category, because the host and the editor must
|
||||
be its two callers and neither may hold a second implementation. Links the header-only
|
||||
`play_seconds`, deliberately not
|
||||
`sample_map`: `PlaySeconds` is the whole of what a deck edits, and linking the mapping would
|
||||
drag the bank model and the WAV codec in behind it. The shell keeps only the controls the
|
||||
parameter set does not carry (key-track, voice count, master gain, preview velocity) and the
|
||||
|
||||
@@ -2,6 +2,8 @@ add_subdirectory(engine)
|
||||
add_subdirectory(map)
|
||||
add_subdirectory(note)
|
||||
add_subdirectory(ui)
|
||||
# After ui: the VST3 parameter identity reads the deck's commit predicate and its value binding.
|
||||
add_subdirectory(param)
|
||||
# Last: bake composes the three above it.
|
||||
add_subdirectory(bake)
|
||||
|
||||
|
||||
@@ -54,6 +54,16 @@ decision about what the render made obsolete.
|
||||
- **`kStageTimeMaxSeconds` (the stage-time ceiling `param_taper` owns) is not a reset-list
|
||||
candidate at all** — it bounds a knob's taper, is never itself a dialed value, and so has
|
||||
no disposition to classify against the ratified reset rule.
|
||||
- **A host automation lane outranks the reset, and the bake cannot clear it — a NAMED
|
||||
limitation, not a bug.** Every reset-class value that is also an exposed VST3 parameter is
|
||||
now notified to the host (the reset writes through `setInstrumentParams`, which is the one
|
||||
notification funnel), so the host's DISPLAY follows the reset. A lane, however, lives in the
|
||||
host's project data: if a reset-class parameter carries one, the host replays its curve onto
|
||||
audio that already has that processing baked in — double processing, and the "sounds as the
|
||||
dialled instrument sounded just before the click" claim does not hold in that case. There is
|
||||
no detection available: `IAutomationState` reports the host's automation mode for the whole
|
||||
plug-in, not per parameter, so both "refuse the bake" and "reset only the un-automated ones"
|
||||
are unbuildable rather than merely unattractive. The user's remedy is to remove the lane.
|
||||
|
||||
## Modules
|
||||
|
||||
|
||||
@@ -52,6 +52,13 @@ float filterDriveDepthFromNorm(float norm) {
|
||||
return static_cast<float>(kFilterDriveDepthMax * n * n);
|
||||
}
|
||||
|
||||
float filterNormFromDriveDepth(float depth) {
|
||||
if (!(depth > 0.0f)) return 0.0f; // also catches NaN
|
||||
if (depth >= kFilterDriveDepthMax) return 1.0f;
|
||||
return static_cast<float>(
|
||||
std::sqrt(static_cast<double>(depth) / static_cast<double>(kFilterDriveDepthMax)));
|
||||
}
|
||||
|
||||
float filterNormFromQ(float q) {
|
||||
if (!(q > kFilterQMin)) return 0.0f;
|
||||
if (q >= kFilterQMax) return 1.0f;
|
||||
|
||||
@@ -49,4 +49,9 @@ float filterNormFromQ(float q);
|
||||
// linear rather than merely close.
|
||||
float filterDriveDepthFromNorm(float norm);
|
||||
|
||||
// Exact inverse of filterDriveDepthFromNorm; out-of-range depth clamps to 0 or 1. The analytic
|
||||
// inverse of a frozen law is not a change to it — it has the standing the two inverses above
|
||||
// already have.
|
||||
float filterNormFromDriveDepth(float depth);
|
||||
|
||||
} // namespace reasampler::instrument::engine::filter
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
|
||||
namespace reasampler::instrument::engine {
|
||||
@@ -37,15 +36,4 @@ double masterGainNormFromLinear(double linear) {
|
||||
return masterGainNormFromDb(20.0 * std::log10(linear));
|
||||
}
|
||||
|
||||
void formatMasterGainLabel(double norm, char* buf, std::size_t len) {
|
||||
if (!buf || len == 0) return;
|
||||
norm = clamp01(norm);
|
||||
if (norm <= 0.0) {
|
||||
std::snprintf(buf, len, "-inf");
|
||||
return;
|
||||
}
|
||||
const double db = masterGainDbFromNorm(norm);
|
||||
std::snprintf(buf, len, "%+.1fdB", db);
|
||||
}
|
||||
|
||||
} // namespace reasampler::instrument::engine
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace reasampler::instrument::engine {
|
||||
|
||||
// norm 0 is -inf (true zero); norm just above 0 starts at the finite floor kMasterGainMinDb
|
||||
@@ -31,8 +29,4 @@ double masterGainLinearFromNorm(double norm);
|
||||
// true-zero and the floor aren't representable on the knob. Out-of-range/non-finite clamps.
|
||||
double masterGainNormFromLinear(double linear);
|
||||
|
||||
// "-inf" at the bottom, else a signed one-decimal dB string ("-12.0dB", "+2.4dB").
|
||||
// Writes at most `len` bytes including the terminator.
|
||||
void formatMasterGainLabel(double norm, char* buf, std::size_t len);
|
||||
|
||||
} // namespace reasampler::instrument::engine
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# src/core/instrument/param — the VST3 parameter surface's pure half
|
||||
|
||||
## Scope
|
||||
|
||||
What the instrument tells a VST3 host about its automatable parameters, with no VST3 type
|
||||
anywhere: the frozen id table, the exposed set derived from the deck's commit predicate, the
|
||||
plain-value layer (unit category, plain range, `toPlain` / `toNormalized`), and the one
|
||||
formatter per unit category. The VST3 shell (`shell/instrument/instrument_params`) adapts
|
||||
these onto `Steinberg::Vst::Parameter`; it decides nothing.
|
||||
|
||||
A sixth peer of `engine/` / `map/` / `note/` / `bake/` / `ui/`, and it sits ABOVE `ui/`: the
|
||||
parameter list is a function of `deckParamCommit` and the value binding, never the reverse.
|
||||
|
||||
## Invariants
|
||||
|
||||
### The id table is FOREVER-FROZEN
|
||||
|
||||
`param_id.h`'s header states the rule in full and is its one home. It sits on the same footing
|
||||
as the extension's `"STABLE_FOREVER_STRING"` command ids, the two VST3 class UIDs
|
||||
(`core/wire/reasampler_uid.h`) and the params-payload field order
|
||||
(`map/component_state_io.h`) — the fourth member of that family, not a new kind of rule.
|
||||
|
||||
**The table carries every assigned number, including numbers not issued today.** Membership of
|
||||
the parameter list is `isExposed`'s answer, not the table's. A row whose control is currently
|
||||
`Reload`-tier keeps its number reserved: the day that control gains a live path it is exposed
|
||||
under the number already written beside it, and no other id moves. That is what the
|
||||
block-and-step scheme buys, and it is why a refusal to promote a control is cheap.
|
||||
|
||||
### The list follows the predicate; the predicate is never bent to fill the list
|
||||
|
||||
A control is an exposed parameter **iff** `deckParamCommit` classifies it `Live` or
|
||||
`NoteOnLatched`. There is no second membership table and no per-control exception. Adding a
|
||||
parameter means giving a control a live path in `deck_groups`, at which point it qualifies by
|
||||
the same rule that excluded it.
|
||||
|
||||
### `toPlain` is a READ-side mapping and changes no stored value
|
||||
|
||||
Reporting Hz / Q / drive depth for the filter's four means **calling** `filter_params`' frozen
|
||||
laws, never replacing them: those four persist as normalized doubles in payload v9, so their
|
||||
laws are already wire-frozen. The same holds for `master_gain`'s dB sweep and `curve_law`'s
|
||||
exponent travel. Every law here is called; none is restated.
|
||||
|
||||
### ONE formatter per unit category, two callers
|
||||
|
||||
`param_format` returns the DIGITS of a plain value. The editor's knob label renders those digits
|
||||
plus its own static chrome (the unit suffix, a curve dial's `^`); the host receives the same
|
||||
digits from `getParamStringByValue` and the unit string from `ParameterInfo::units`. There is no
|
||||
second implementation on either side — that is why `formatEnvTimeMs` and `formatMasterGainLabel`
|
||||
no longer exist.
|
||||
|
||||
## Modules
|
||||
|
||||
- `param_id` — the frozen `ParamId` constants, the `ParamRow` table (id, `DeckParam`, `IUnitInfo`
|
||||
unit, title, shortTitle) in ascending id, `isExposed`, and the derived `exposedParams()`.
|
||||
Ascending id IS the presentation order, so identity order and presentation order agree by
|
||||
construction rather than by maintenance.
|
||||
- `param_units` — `UnitKind`, `unitStringFor`, `plainRangeFor`, the `toPlain` / `toNormalized`
|
||||
pair, and the defaults read off a default-constructed `PlaySeconds`.
|
||||
- `param_format` — the eight formatters and the digits parser behind `getParamValueByString`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **`defaultNormalized` is COMPUTED, never a literal.** It is `toNormalized(defaultPlain)` for
|
||||
every tapered control, so a host's reset-to-default and the editor's double-click land on the
|
||||
same value. The filter's four are the one exception and for the opposite reason: their stored
|
||||
value already IS the normalized one, so their default normalized value is that double verbatim
|
||||
and no taper participates in the reset path at all.
|
||||
- **Round-trip exactness at arbitrary values is NOT a property here and must not be asserted.**
|
||||
No log map satisfies `toNormalized(toPlain(n)) == n` in double, and demanding it would rule
|
||||
out the taper the range needs. Exactness is required at the defaults; monotonicity everywhere.
|
||||
- **A curve exponent inside the knob detent but not exactly neutral reads `1.00` to the host.**
|
||||
The detent lives in `curve_law`'s norm↔exponent map and the host's only handle is the norm, so
|
||||
the host cannot see an off-detent near-neutral exponent — reachable only by an overlay knot
|
||||
drag, which writes through `curveFromLevelAt` rather than the knob law. The editor's own label
|
||||
deliberately reads the stored field directly and still shows the true value; that divergence is
|
||||
structural to VST3, not a formatter defect.
|
||||
- **Master gain's plain value at norm 0 is `-inf`**, which is outside the declared −60…+24 range
|
||||
on purpose — norm 0 is true silence, not the floor. The formatter prints `-inf` there.
|
||||
- **The filter's four store their position as a `float`, so a not-yet-stored norm can display
|
||||
one digit differently.** A host previewing a value it has sent but that has not round-tripped
|
||||
through the stored float differs from the editor by up to a float ulp; at a value landing
|
||||
exactly on a display rounding boundary that is worth one integer percent on morph. Both
|
||||
surfaces read the MODEL in every settled state, so this is a transient of the write itself,
|
||||
not a standing divergence — `test_param_format` holds those four to the plain value rather
|
||||
than to the string for exactly this reason.
|
||||
@@ -0,0 +1,17 @@
|
||||
# The frozen id table and the derived exposed set. Links deck_groups alone: the exposed set IS
|
||||
# deckParamCommit's answer, and identity needs nothing else.
|
||||
reasampler_pure_library(param_id SOURCES param_id.cpp LINK PUBLIC deck_groups)
|
||||
reasampler_test(param_id LINK param_id)
|
||||
|
||||
# The norm <-> plain layer. deck_values carries the tapers' full scales and the two field
|
||||
# resolvers the defaults are read through; filter_params and master_gain are the frozen laws the
|
||||
# filter's four and the gain report through, CALLED rather than restated.
|
||||
reasampler_pure_library(param_units
|
||||
SOURCES param_units.cpp
|
||||
LINK PUBLIC deck_values param_taper curve_law master_gain filter)
|
||||
reasampler_test(param_units LINK param_units param_id)
|
||||
|
||||
reasampler_pure_library(param_format SOURCES param_format.cpp LINK PUBLIC param_units)
|
||||
# param_id is linked for the test only: the one-formatter-two-consumers assertion sweeps the
|
||||
# exposed set, which is identity's answer rather than this module's.
|
||||
reasampler_test(param_format LINK param_format param_id)
|
||||
@@ -0,0 +1,76 @@
|
||||
// param_format.cpp — see param_format.h.
|
||||
|
||||
#include "core/instrument/param/param_format.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
|
||||
namespace reasampler::instrument::param {
|
||||
|
||||
void formatPlain(UnitKind kind, double plain, char* buf, std::size_t len) {
|
||||
if (!buf || len == 0) return;
|
||||
switch (kind) {
|
||||
case UnitKind::Time:
|
||||
// Never switches to seconds, so the ceiling reads 10000 and not 10 — units is one
|
||||
// static string per parameter and cannot change with magnitude. Sub-10 ms keeps a
|
||||
// decimal so a short attack is not rounded to a bare "0".
|
||||
std::snprintf(buf, len, plain < 10.0 ? "%.1f" : "%.0f", plain);
|
||||
return;
|
||||
case UnitKind::Semitones:
|
||||
std::snprintf(buf, len, "%+.1f", plain);
|
||||
return;
|
||||
case UnitKind::PercentUnipolar:
|
||||
case UnitKind::PercentKeyTrack:
|
||||
std::snprintf(buf, len, "%.0f", plain);
|
||||
return;
|
||||
case UnitKind::PercentBipolar:
|
||||
std::snprintf(buf, len, "%+.0f", plain);
|
||||
return;
|
||||
case UnitKind::PercentRate:
|
||||
// One decimal, not integer percent: the snap grid is whole semitones and those do
|
||||
// not land on integer percent (+1 st = 105.946 %), so an integer display would print
|
||||
// a snapped position as a value the snap cannot produce.
|
||||
std::snprintf(buf, len, "%.1f", plain);
|
||||
return;
|
||||
case UnitKind::Decibels:
|
||||
if (!std::isfinite(plain)) { std::snprintf(buf, len, "-inf"); return; }
|
||||
std::snprintf(buf, len, "%+.1f", plain);
|
||||
return;
|
||||
case UnitKind::Hertz:
|
||||
// The "k" abbreviation is RETIRED: units is one static string per parameter, so a
|
||||
// magnitude-switching unit is not expressible, and keeping "12.8k" in the editor
|
||||
// alone would be exactly the host/editor divergence one formatter exists to forbid.
|
||||
std::snprintf(buf, len, "%.0f", plain);
|
||||
return;
|
||||
case UnitKind::Dimensionless:
|
||||
std::snprintf(buf, len, "%.2f", plain);
|
||||
return;
|
||||
}
|
||||
buf[0] = '\0';
|
||||
}
|
||||
|
||||
void formatPlainFor(DeckParam deck, double plain, char* buf, std::size_t len) {
|
||||
formatPlain(unitKindFor(deck), plain, buf, len);
|
||||
}
|
||||
|
||||
bool parsePlain(UnitKind kind, const char* text, double& plain) {
|
||||
if (!text) return false;
|
||||
if (kind == UnitKind::Decibels) {
|
||||
// The one non-numeric string any formatter emits, so the one the parser must recognise.
|
||||
for (const char* p = text; *p; ++p) {
|
||||
if (*p == 'i' && p[1] == 'n' && p[2] == 'f') {
|
||||
plain = -std::numeric_limits<double>::infinity();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
char* end = nullptr;
|
||||
const double value = std::strtod(text, &end);
|
||||
if (end == text) return false;
|
||||
plain = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace reasampler::instrument::param
|
||||
@@ -0,0 +1,26 @@
|
||||
// param_format.h — ONE formatter per unit category, and the editor and the host are both its
|
||||
// callers. It returns the DIGITS of a plain value: no embedded unit, no magnitude-switched unit,
|
||||
// no width-conditional abbreviation. The editor's knob label adds its own static chrome (the
|
||||
// unit suffix, a curve dial's "^"); the host receives these digits from getParamStringByValue and
|
||||
// the unit from ParameterInfo::units. There is no second implementation on either side — the two
|
||||
// surfaces disagreeing about what a value reads as is a defect class this closes structurally.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "core/instrument/param/param_units.h"
|
||||
|
||||
namespace reasampler::instrument::param {
|
||||
|
||||
// Writes at most `len` bytes including the terminator.
|
||||
void formatPlain(UnitKind kind, double plain, char* buf, std::size_t len);
|
||||
|
||||
// The digits a control reads as at `plain`. Convenience over formatPlain for the common case.
|
||||
void formatPlainFor(DeckParam deck, double plain, char* buf, std::size_t len);
|
||||
|
||||
// Digits -> plain, for getParamValueByString. False when the text carries no number; a trailing
|
||||
// unit suffix is tolerated, since a user retyping a displayed value keeps it.
|
||||
bool parsePlain(UnitKind kind, const char* text, double& plain);
|
||||
|
||||
} // namespace reasampler::instrument::param
|
||||
@@ -0,0 +1,105 @@
|
||||
// param_id.cpp — see param_id.h. The table is written out rather than derived: a derived id is
|
||||
// a function of something else, and every such input then has to never change. A literal table
|
||||
// makes the freeze visible AT THE POINT OF CHANGE — it cannot be renumbered by accident, because
|
||||
// renumbering it means editing the numbers.
|
||||
|
||||
#include "core/instrument/param/param_id.h"
|
||||
|
||||
namespace reasampler::instrument::param {
|
||||
|
||||
namespace {
|
||||
|
||||
// Within a block the order is the group's own semantic order — envelope stages in temporal
|
||||
// order, filter cells in solve order — seeded ONCE here and never re-seeded from cellIds. A
|
||||
// group's cell order is exactly as mobile as the deck's row order is; this sequence is a
|
||||
// property of THIS table, which is what lets DeckParam keep its "runtime-only, free to change"
|
||||
// licence.
|
||||
const std::vector<ParamRow>& table() {
|
||||
static const std::vector<ParamRow> kTable = {
|
||||
{kParamKeyTrackPitch, DeckParam::kKeyTrack, kUnitPitch, "Key Track", "KeyTrk"},
|
||||
{kParamRate, DeckParam::kRate, kUnitPitch, "Playback Rate", "Rate"},
|
||||
{kParamPitchOffset, DeckParam::kPitch, kUnitPitch, "Pitch Offset", "Pitch"},
|
||||
|
||||
{kParamPitchEnvAttack, DeckParam::kPitchEnvAttack, kUnitPitchEnv, "Pitch Env Attack", "PEnvA"},
|
||||
{kParamPitchEnvAttackCurve, DeckParam::kPitchEnvAttackCurve, kUnitPitchEnv, "Pitch Env Attack Curve", "PEnvAC"},
|
||||
{kParamPitchEnvHold, DeckParam::kPitchEnvHold, kUnitPitchEnv, "Pitch Env Hold", "PEnvH"},
|
||||
{kParamPitchEnvDecay, DeckParam::kPitchEnvDecay, kUnitPitchEnv, "Pitch Env Decay", "PEnvD"},
|
||||
{kParamPitchEnvDecayCurve, DeckParam::kPitchEnvDecayCurve, kUnitPitchEnv, "Pitch Env Decay Curve", "PEnvDC"},
|
||||
{kParamPitchEnvDepth, DeckParam::kPitchEnvDepth, kUnitPitchEnv, "Pitch Env Depth", "PEnvDp"},
|
||||
|
||||
{kParamFilterMorph, DeckParam::kFilterMorph, kUnitFilter, "Filter Morph", "Morph"},
|
||||
{kParamFilterCutoff, DeckParam::kFilterCutoff, kUnitFilter, "Filter Cutoff", "Cutoff"},
|
||||
{kParamFilterQ, DeckParam::kFilterQ, kUnitFilter, "Filter Q", "Q"},
|
||||
{kParamFilterDrive, DeckParam::kFilterDrive, kUnitFilter, "Filter Drive", "Drive"},
|
||||
{kParamFilterModAmount, DeckParam::kFilterModAmt, kUnitFilter, "Filter Env Amount", "FEnvAmt"},
|
||||
{kParamFilterVelAmount, DeckParam::kFilterVel, kUnitFilter, "Filter Vel Amount", "FVelAmt"},
|
||||
{kParamKeyTrackFilter, DeckParam::kFilterKeyTrack, kUnitFilter, "Filter Key Track", "FKeyTrk"},
|
||||
|
||||
{kParamFilterEnvAttack, DeckParam::kFilterEnvAttack, kUnitFilterEnv, "Filter Env Attack", "FEnvA"},
|
||||
{kParamFilterEnvAttackCurve, DeckParam::kFilterEnvAttackCurve, kUnitFilterEnv, "Filter Env Attack Curve", "FEnvAC"},
|
||||
{kParamFilterEnvHold, DeckParam::kFilterEnvHold, kUnitFilterEnv, "Filter Env Hold", "FEnvH"},
|
||||
{kParamFilterEnvDecay, DeckParam::kFilterEnvDecay, kUnitFilterEnv, "Filter Env Decay", "FEnvD"},
|
||||
{kParamFilterEnvDecayCurve, DeckParam::kFilterEnvDecayCurve, kUnitFilterEnv, "Filter Env Decay Curve", "FEnvDC"},
|
||||
{kParamFilterEnvSustain, DeckParam::kFilterEnvSustain, kUnitFilterEnv, "Filter Env Sustain", "FEnvS"},
|
||||
{kParamFilterEnvRelease, DeckParam::kFilterEnvRelease, kUnitFilterEnv, "Filter Env Release", "FEnvR"},
|
||||
{kParamFilterEnvReleaseCurve, DeckParam::kFilterEnvReleaseCurve, kUnitFilterEnv, "Filter Env Release Curve", "FEnvRC"},
|
||||
{kParamFilterTrigAttack, DeckParam::kFilterTrigAttack, kUnitFilterEnv, "Filter Trig Attack", "FTrgA"},
|
||||
{kParamFilterTrigAttackCurve, DeckParam::kFilterTrigAttackCurve, kUnitFilterEnv, "Filter Trig Attack Curve", "FTrgAC"},
|
||||
{kParamFilterTrigHold, DeckParam::kFilterTrigHold, kUnitFilterEnv, "Filter Trig Hold", "FTrgH"},
|
||||
{kParamFilterTrigDecay, DeckParam::kFilterTrigDecay, kUnitFilterEnv, "Filter Trig Decay", "FTrgD"},
|
||||
{kParamFilterTrigDecayCurve, DeckParam::kFilterTrigDecayCurve, kUnitFilterEnv, "Filter Trig Decay Curve", "FTrgDC"},
|
||||
|
||||
{kParamAmpAttack, DeckParam::kAttack, kUnitAmp, "Amp Attack", "AmpA"},
|
||||
{kParamAmpAttackCurve, DeckParam::kAttackCurve, kUnitAmp, "Amp Attack Curve", "AmpAC"},
|
||||
{kParamAmpHold, DeckParam::kHold, kUnitAmp, "Amp Hold", "AmpH"},
|
||||
{kParamAmpDecay, DeckParam::kDecay, kUnitAmp, "Amp Decay", "AmpD"},
|
||||
{kParamAmpDecayCurve, DeckParam::kDecayCurve, kUnitAmp, "Amp Decay Curve", "AmpDC"},
|
||||
{kParamAmpSustain, DeckParam::kSustain, kUnitAmp, "Amp Sustain", "AmpS"},
|
||||
{kParamAmpRelease, DeckParam::kRelease, kUnitAmp, "Amp Release", "AmpR"},
|
||||
{kParamAmpReleaseCurve, DeckParam::kReleaseCurve, kUnitAmp, "Amp Release Curve", "AmpRC"},
|
||||
{kParamTriggerLength, DeckParam::kTrigLength, kUnitAmp, "Trigger Length", "TrgLen"},
|
||||
{kParamAmpTrigAttack, DeckParam::kTrigAttack, kUnitAmp, "Amp Trig Attack", "ATrgA"},
|
||||
{kParamAmpTrigAttackCurve, DeckParam::kTrigAttackCurve, kUnitAmp, "Amp Trig Attack Curve", "ATrgAC"},
|
||||
{kParamAmpTrigHold, DeckParam::kTrigHold, kUnitAmp, "Amp Trig Hold", "ATrgH"},
|
||||
{kParamAmpTrigDecay, DeckParam::kTrigDecay, kUnitAmp, "Amp Trig Decay", "ATrgD"},
|
||||
{kParamAmpTrigDecayCurve, DeckParam::kTrigDecayCurve, kUnitAmp, "Amp Trig Decay Curve", "ATrgDC"},
|
||||
|
||||
{kParamMasterGain, DeckParam::kMasterGain, kUnitMaster, "Master Gain", "Gain"},
|
||||
};
|
||||
return kTable;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<ParamRow>& paramTable() { return table(); }
|
||||
|
||||
bool isExposed(DeckParam deck) {
|
||||
return ui::deckParamCommit(deck) != ui::LiveCommit::Reload;
|
||||
}
|
||||
|
||||
const std::vector<ParamRow>& exposedParams() {
|
||||
static const std::vector<ParamRow> kExposed = [] {
|
||||
std::vector<ParamRow> rows;
|
||||
for (const ParamRow& row : table()) {
|
||||
if (isExposed(row.deck)) rows.push_back(row);
|
||||
}
|
||||
return rows;
|
||||
}();
|
||||
return kExposed;
|
||||
}
|
||||
|
||||
const ParamRow* exposedRowFor(ParamId id) {
|
||||
for (const ParamRow& row : exposedParams()) {
|
||||
if (row.id == id) return &row;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ParamId paramIdFor(DeckParam deck) {
|
||||
for (const ParamRow& row : table()) {
|
||||
if (row.deck == deck) return row.id;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace reasampler::instrument::param
|
||||
@@ -0,0 +1,135 @@
|
||||
// param_id.h — the VST3 parameter identity space: the frozen id table, its DeckParam binding,
|
||||
// the deck-group units, and the exposed set DERIVED from the commit predicate. Pure: no VST3
|
||||
// type appears here, so the whole contract is provable without a host.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "core/instrument/ui/deck_groups.h" // DeckParam + deckParamCommit (the predicate)
|
||||
|
||||
namespace reasampler::instrument::param {
|
||||
|
||||
using ui::DeckParam;
|
||||
|
||||
// A host records this number into automation lanes inside project files this repo does not own
|
||||
// and cannot migrate.
|
||||
//
|
||||
// THE PARAMETER-ID TABLE IS FOREVER-FROZEN, on the same footing as the extension's
|
||||
// "STABLE_FOREVER_STRING" command ids, the two VST3 class UIDs (core/wire/reasampler_uid.h) and
|
||||
// the params-payload field order (map/component_state_io.h):
|
||||
// - No id is ever reassigned, reused or re-pointed. A control whose meaning genuinely changes
|
||||
// takes a NEW id; the old one is marked dead here and never re-issued.
|
||||
// - No exposed parameter's normalization ever changes — not its taper, not either range
|
||||
// endpoint, not its stepCount. The normalization IS the meaning of every recorded point.
|
||||
// - A parameter's meaning never depends on a mode. The Gate-face and Trigger-face stage times
|
||||
// are separate stored fields and take separate ids.
|
||||
// - A new control takes the next free slot inside its own group's block, never the next number
|
||||
// at the end of the table.
|
||||
// Display strings, titles and precision are NOT frozen — they are what a user reads, not what a
|
||||
// lane stores.
|
||||
using ParamId = std::uint32_t;
|
||||
|
||||
// Blocks of 100 per deck group in SIGNAL-FLOW order, steps of 10 within a block, a curve dial at
|
||||
// its outer knob's id + 1. Blocks start at 1000 so the first legitimate id is not also the most
|
||||
// likely bug value (a default-initialised ParamId). Nine free slots between neighbours put a
|
||||
// control added later numerically beside its siblings instead of at the end of the table.
|
||||
// 1500-1599 (VELOCITY) and 1600-1699 (VOICE) are RESERVED and empty — a control either group
|
||||
// ever gains lands in its own range rather than in whatever range happened to be free.
|
||||
enum : ParamId {
|
||||
kParamKeyTrackPitch = 1000,
|
||||
kParamRate = 1010,
|
||||
kParamPitchOffset = 1020,
|
||||
|
||||
kParamPitchEnvAttack = 1100,
|
||||
kParamPitchEnvAttackCurve= 1101,
|
||||
kParamPitchEnvHold = 1110,
|
||||
kParamPitchEnvDecay = 1120,
|
||||
kParamPitchEnvDecayCurve = 1121,
|
||||
kParamPitchEnvDepth = 1130,
|
||||
|
||||
kParamFilterMorph = 1200,
|
||||
kParamFilterCutoff = 1210,
|
||||
kParamFilterQ = 1220,
|
||||
kParamFilterDrive = 1230,
|
||||
kParamFilterModAmount = 1240,
|
||||
kParamFilterVelAmount = 1250,
|
||||
kParamKeyTrackFilter = 1260,
|
||||
|
||||
kParamFilterEnvAttack = 1300,
|
||||
kParamFilterEnvAttackCurve = 1301,
|
||||
kParamFilterEnvHold = 1310,
|
||||
kParamFilterEnvDecay = 1320,
|
||||
kParamFilterEnvDecayCurve = 1321,
|
||||
kParamFilterEnvSustain = 1330,
|
||||
kParamFilterEnvRelease = 1340,
|
||||
kParamFilterEnvReleaseCurve = 1341,
|
||||
kParamFilterTrigAttack = 1350,
|
||||
kParamFilterTrigAttackCurve = 1351,
|
||||
kParamFilterTrigHold = 1360,
|
||||
kParamFilterTrigDecay = 1370,
|
||||
kParamFilterTrigDecayCurve = 1371,
|
||||
|
||||
kParamAmpAttack = 1400,
|
||||
kParamAmpAttackCurve = 1401,
|
||||
kParamAmpHold = 1410,
|
||||
kParamAmpDecay = 1420,
|
||||
kParamAmpDecayCurve = 1421,
|
||||
kParamAmpSustain = 1430,
|
||||
kParamAmpRelease = 1440,
|
||||
kParamAmpReleaseCurve = 1441,
|
||||
kParamTriggerLength = 1450,
|
||||
kParamAmpTrigAttack = 1460,
|
||||
kParamAmpTrigAttackCurve = 1461,
|
||||
kParamAmpTrigHold = 1470,
|
||||
kParamAmpTrigDecay = 1480,
|
||||
kParamAmpTrigDecayCurve = 1481,
|
||||
|
||||
kParamMasterGain = 1700,
|
||||
};
|
||||
|
||||
// IUnitInfo units, one per deck group that carries an exposed parameter. 0 is the SDK's root
|
||||
// unit, so these start at 1. Softer than the id freeze but user-facing and cached by some hosts.
|
||||
enum : std::int32_t {
|
||||
kUnitRoot = 0,
|
||||
kUnitPitch = 1,
|
||||
kUnitPitchEnv = 2,
|
||||
kUnitFilter = 3,
|
||||
kUnitFilterEnv = 4,
|
||||
kUnitAmp = 5,
|
||||
kUnitMaster = 6,
|
||||
};
|
||||
|
||||
struct ParamRow {
|
||||
ParamId id;
|
||||
DeckParam deck;
|
||||
std::int32_t unit;
|
||||
const char* title; // survives truncation
|
||||
const char* shortTitle; // distinct, for a narrow host column
|
||||
};
|
||||
|
||||
// The WHOLE frozen assignment, in ascending id — which is also the presentation order, so
|
||||
// identity order and presentation order agree by construction rather than by maintenance.
|
||||
// Membership of the parameter list is NOT decided here: a row is issued to the host only if
|
||||
// isExposed() says so. A row whose control is not exposed today keeps its number reserved for
|
||||
// the day that control gains a live path, which is what the block-and-step scheme buys.
|
||||
const std::vector<ParamRow>& paramTable();
|
||||
|
||||
// A control is an exposed VST3 parameter IF AND ONLY IF its commit class is Live or
|
||||
// NoteOnLatched. Derived from deckParamCommit, never hand-maintained: the list follows the
|
||||
// predicate, and the predicate is never bent to fill the list.
|
||||
bool isExposed(DeckParam deck);
|
||||
|
||||
// paramTable() filtered by isExposed, still in ascending id. This is exactly what the host is
|
||||
// told, in the order it is told.
|
||||
const std::vector<ParamRow>& exposedParams();
|
||||
|
||||
// The row for an id, or null when the id is unknown or its control is not exposed.
|
||||
const ParamRow* exposedRowFor(ParamId id);
|
||||
|
||||
// The id a control is numbered as, or 0 when the control has no row at all. Answers for
|
||||
// unexposed rows too — the number is a property of the table, not of today's membership.
|
||||
ParamId paramIdFor(DeckParam deck);
|
||||
|
||||
} // namespace reasampler::instrument::param
|
||||
@@ -0,0 +1,232 @@
|
||||
// param_units.cpp — see param_units.h. Every law here is CALLED, never restated: the stage-time
|
||||
// and semitone tapers are param_taper's, the curve travel is curve_law's, the filter's four are
|
||||
// filter_params' own frozen laws, and the dB sweep is master_gain's.
|
||||
|
||||
#include "core/instrument/param/param_units.h"
|
||||
|
||||
#include "core/instrument/engine/filter/filter_params.h"
|
||||
#include "core/instrument/engine/master_gain.h"
|
||||
#include "core/instrument/map/play_seconds.h"
|
||||
#include "core/instrument/ui/deck_values.h"
|
||||
#include "core/instrument/ui/param_taper.h"
|
||||
#include "core/util/clamp01.h"
|
||||
#include "core/util/curve_law.h"
|
||||
|
||||
namespace reasampler::instrument::param {
|
||||
|
||||
namespace {
|
||||
|
||||
using map::PlaySeconds;
|
||||
using util::clamp01;
|
||||
|
||||
// A whole displayed percent is a different plain full scale per category; these are the two
|
||||
// non-100 ones, named rather than inlined so the range table and the maps cannot disagree.
|
||||
constexpr double kPercentFullScale = 100.0;
|
||||
const double kKeyTrackFullScale = kPercentFullScale * ui::kKeyTrackMax; // 0..200 %
|
||||
|
||||
} // namespace
|
||||
|
||||
UnitKind unitKindFor(DeckParam deck) {
|
||||
switch (deck) {
|
||||
case DeckParam::kAttack:
|
||||
case DeckParam::kHold:
|
||||
case DeckParam::kDecay:
|
||||
case DeckParam::kRelease:
|
||||
case DeckParam::kTrigAttack:
|
||||
case DeckParam::kTrigDecay:
|
||||
case DeckParam::kPitchEnvAttack:
|
||||
case DeckParam::kPitchEnvDecay:
|
||||
case DeckParam::kFilterEnvAttack:
|
||||
case DeckParam::kFilterEnvHold:
|
||||
case DeckParam::kFilterEnvDecay:
|
||||
case DeckParam::kFilterEnvRelease:
|
||||
case DeckParam::kFilterTrigAttack:
|
||||
case DeckParam::kFilterTrigDecay:
|
||||
return UnitKind::Time;
|
||||
case DeckParam::kPitch:
|
||||
case DeckParam::kPitchEnvDepth:
|
||||
return UnitKind::Semitones;
|
||||
case DeckParam::kSustain:
|
||||
case DeckParam::kTrigLength:
|
||||
case DeckParam::kTrigHold:
|
||||
case DeckParam::kPitchEnvHold:
|
||||
case DeckParam::kFilterEnvSustain:
|
||||
case DeckParam::kFilterTrigHold:
|
||||
case DeckParam::kFilterMorph:
|
||||
return UnitKind::PercentUnipolar;
|
||||
case DeckParam::kKeyTrack:
|
||||
case DeckParam::kFilterKeyTrack:
|
||||
return UnitKind::PercentKeyTrack;
|
||||
case DeckParam::kFilterModAmt:
|
||||
case DeckParam::kFilterVel:
|
||||
return UnitKind::PercentBipolar;
|
||||
case DeckParam::kRate:
|
||||
return UnitKind::PercentRate;
|
||||
case DeckParam::kMasterGain:
|
||||
return UnitKind::Decibels;
|
||||
case DeckParam::kFilterCutoff:
|
||||
return UnitKind::Hertz;
|
||||
default:
|
||||
// The twelve curve exponents, Q and drive. Everything else has no row at all.
|
||||
return UnitKind::Dimensionless;
|
||||
}
|
||||
}
|
||||
|
||||
const char* unitStringFor(DeckParam deck) {
|
||||
switch (unitKindFor(deck)) {
|
||||
case UnitKind::Time: return "ms";
|
||||
case UnitKind::Semitones: return "st";
|
||||
case UnitKind::PercentUnipolar:
|
||||
case UnitKind::PercentKeyTrack:
|
||||
case UnitKind::PercentBipolar:
|
||||
case UnitKind::PercentRate: return "%";
|
||||
case UnitKind::Decibels: return "dB";
|
||||
case UnitKind::Hertz: return "Hz";
|
||||
case UnitKind::Dimensionless: return "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
PlainRange plainRangeFor(DeckParam deck) {
|
||||
switch (deck) {
|
||||
case DeckParam::kFilterQ:
|
||||
return {static_cast<double>(engine::filter::kFilterQMin),
|
||||
static_cast<double>(engine::filter::kFilterQMax)};
|
||||
case DeckParam::kFilterDrive:
|
||||
return {0.0, static_cast<double>(engine::filter::kFilterDriveDepthMax)};
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (unitKindFor(deck)) {
|
||||
case UnitKind::Time: return {0.0, ui::kEnvTimeMaxSeconds * 1000.0};
|
||||
case UnitKind::Semitones: return {-ui::kPitchDepthMaxSemis, ui::kPitchDepthMaxSemis};
|
||||
case UnitKind::PercentUnipolar: return {0.0, kPercentFullScale};
|
||||
case UnitKind::PercentKeyTrack: return {0.0, kKeyTrackFullScale};
|
||||
case UnitKind::PercentBipolar: return {-kPercentFullScale, kPercentFullScale};
|
||||
case UnitKind::PercentRate: return {ui::kRateMinRatio * kPercentFullScale,
|
||||
ui::kRateMaxRatio * kPercentFullScale};
|
||||
case UnitKind::Decibels: return {engine::kMasterGainMinDb, engine::kMasterGainMaxDb};
|
||||
case UnitKind::Hertz: return {static_cast<double>(engine::filter::kFilterCutoffMinHz),
|
||||
static_cast<double>(engine::filter::kFilterCutoffMaxHz)};
|
||||
case UnitKind::Dimensionless: return {util::kCurveMin, util::kCurveMax};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
double toPlain(DeckParam deck, double normalized) {
|
||||
switch (deck) {
|
||||
case DeckParam::kFilterCutoff:
|
||||
return static_cast<double>(
|
||||
engine::filter::filterCutoffHzFromNorm(static_cast<float>(normalized)));
|
||||
case DeckParam::kFilterQ:
|
||||
return static_cast<double>(
|
||||
engine::filter::filterQFromNorm(static_cast<float>(normalized)));
|
||||
case DeckParam::kFilterDrive:
|
||||
return static_cast<double>(
|
||||
engine::filter::filterDriveDepthFromNorm(static_cast<float>(normalized)));
|
||||
case DeckParam::kMasterGain:
|
||||
// -inf at norm 0 — true silence, and the one plain value outside the declared range.
|
||||
return engine::masterGainDbFromNorm(normalized);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (unitKindFor(deck)) {
|
||||
case UnitKind::Time:
|
||||
return ui::timeSecondsFromNorm(normalized) * 1000.0;
|
||||
case UnitKind::Semitones:
|
||||
return ui::depthSemitonesFromNorm(normalized, ui::kPitchDepthMaxSemis);
|
||||
case UnitKind::PercentUnipolar:
|
||||
return clamp01(normalized) * kPercentFullScale;
|
||||
case UnitKind::PercentKeyTrack:
|
||||
return clamp01(normalized) * kKeyTrackFullScale;
|
||||
case UnitKind::PercentBipolar:
|
||||
return ui::deckBipolarFromNorm(normalized) * kPercentFullScale;
|
||||
case UnitKind::PercentRate:
|
||||
return ui::rateRatioFromNorm(normalized, ui::kRateMinRatio, ui::kRateMaxRatio) *
|
||||
kPercentFullScale;
|
||||
case UnitKind::Dimensionless:
|
||||
return util::curveFromKnobNorm(normalized);
|
||||
case UnitKind::Decibels:
|
||||
case UnitKind::Hertz:
|
||||
break; // handled above
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
double toNormalized(DeckParam deck, double plain) {
|
||||
switch (deck) {
|
||||
case DeckParam::kFilterCutoff:
|
||||
return static_cast<double>(
|
||||
engine::filter::filterNormFromCutoffHz(static_cast<float>(plain)));
|
||||
case DeckParam::kFilterQ:
|
||||
return static_cast<double>(
|
||||
engine::filter::filterNormFromQ(static_cast<float>(plain)));
|
||||
case DeckParam::kFilterDrive:
|
||||
return static_cast<double>(
|
||||
engine::filter::filterNormFromDriveDepth(static_cast<float>(plain)));
|
||||
case DeckParam::kMasterGain:
|
||||
return engine::masterGainNormFromDb(plain);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (unitKindFor(deck)) {
|
||||
case UnitKind::Time:
|
||||
return ui::timeNormFromSeconds(plain / 1000.0);
|
||||
case UnitKind::Semitones:
|
||||
return ui::depthNormFromSemitones(plain, ui::kPitchDepthMaxSemis);
|
||||
case UnitKind::PercentUnipolar:
|
||||
return clamp01(plain / kPercentFullScale);
|
||||
case UnitKind::PercentKeyTrack:
|
||||
return clamp01(plain / kKeyTrackFullScale);
|
||||
case UnitKind::PercentBipolar:
|
||||
return ui::deckNormFromBipolar(plain / kPercentFullScale);
|
||||
case UnitKind::PercentRate:
|
||||
return ui::rateNormFromRatio(plain / kPercentFullScale, ui::kRateMinRatio,
|
||||
ui::kRateMaxRatio);
|
||||
case UnitKind::Dimensionless:
|
||||
return util::knobNormFromCurve(plain);
|
||||
case UnitKind::Decibels:
|
||||
case UnitKind::Hertz:
|
||||
break; // handled above
|
||||
}
|
||||
return plain;
|
||||
}
|
||||
|
||||
bool storesNormalized(DeckParam deck) {
|
||||
PlaySeconds defaults;
|
||||
return ui::deckFloatField(deck, defaults) != nullptr;
|
||||
}
|
||||
|
||||
double defaultPlain(DeckParam deck) {
|
||||
PlaySeconds defaults;
|
||||
// The filter's four store the normalized position itself, so their plain default is that
|
||||
// stored position read THROUGH the law — the law is the display, never the storage.
|
||||
if (const float* stored = ui::deckFloatField(deck, defaults)) {
|
||||
return toPlain(deck, static_cast<double>(*stored));
|
||||
}
|
||||
if (deck == DeckParam::kMasterGain) return 0.0; // unity, and the sharpest exactness case
|
||||
const double* field = ui::deckDoubleField(deck, defaults);
|
||||
if (!field) return 0.0;
|
||||
switch (unitKindFor(deck)) {
|
||||
case UnitKind::Time: return *field * 1000.0; // stored seconds
|
||||
case UnitKind::PercentUnipolar: return *field * kPercentFullScale;
|
||||
case UnitKind::PercentKeyTrack: return *field * kPercentFullScale; // stored 0..2
|
||||
case UnitKind::PercentBipolar: return *field * kPercentFullScale;
|
||||
case UnitKind::PercentRate: return *field * kPercentFullScale; // stored ratio
|
||||
case UnitKind::Semitones:
|
||||
case UnitKind::Dimensionless: return *field; // already the plain unit
|
||||
case UnitKind::Decibels:
|
||||
case UnitKind::Hertz: break; // handled above
|
||||
}
|
||||
return *field;
|
||||
}
|
||||
|
||||
double defaultNormalized(DeckParam deck) {
|
||||
PlaySeconds defaults;
|
||||
if (const float* stored = ui::deckFloatField(deck, defaults)) {
|
||||
return static_cast<double>(*stored); // verbatim: no taper on the reset path
|
||||
}
|
||||
return toNormalized(deck, defaultPlain(deck));
|
||||
}
|
||||
|
||||
} // namespace reasampler::instrument::param
|
||||
@@ -0,0 +1,64 @@
|
||||
// param_units.h — the plain-value layer the host reads a parameter through: the unit category,
|
||||
// the plain range, and the norm <-> plain pair. `toPlain` IS the taper's forward map and
|
||||
// `toNormalized` its inverse, so the host's normalization, the knob's needle angle and the
|
||||
// overlay node's position are the SAME function rather than three that agree today.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/instrument/ui/deck_groups.h" // DeckParam
|
||||
|
||||
namespace reasampler::instrument::param {
|
||||
|
||||
using ui::DeckParam;
|
||||
|
||||
// The eight DISPLAY categories. A category fixes the units string and the digit precision; the
|
||||
// norm <-> plain LAW is per control, because three of the dimensionless controls (the curve
|
||||
// exponents, Q, drive) share a display and share no law.
|
||||
enum class UnitKind {
|
||||
Time, // ms, 0..10000
|
||||
Semitones, // st, -24..+24, always signed
|
||||
PercentUnipolar, // %, 0..100
|
||||
PercentKeyTrack, // %, 0..200
|
||||
PercentBipolar, // %, -100..+100, always signed
|
||||
PercentRate, // %, 50..200, one decimal
|
||||
Decibels, // dB, -60..+24, always signed; norm 0 reads -inf
|
||||
Hertz, // Hz, 20..20000
|
||||
Dimensionless, // no unit, two decimals
|
||||
};
|
||||
|
||||
struct PlainRange {
|
||||
double min = 0.0;
|
||||
double max = 1.0;
|
||||
};
|
||||
|
||||
UnitKind unitKindFor(DeckParam deck);
|
||||
|
||||
// The units string ParameterInfo carries — "" for the dimensionless category. Carried SEPARATELY
|
||||
// from the digits, which is the SDK's own convention (RangeParameter::toString prints the number;
|
||||
// the Parameter constructor takes units as its own argument).
|
||||
const char* unitStringFor(DeckParam deck);
|
||||
|
||||
PlainRange plainRangeFor(DeckParam deck);
|
||||
|
||||
// A straight line drawn in a host automation lane is NOT linear in these plain units, and that is
|
||||
// deliberate: exponential in ms, linear in octaves on cutoff, linear in dB on master gain, a
|
||||
// linear pitch glide on rate, and slow-near-zero on the two centre-expanded semitone throws. It
|
||||
// follows from reporting real units over a musically-shaped taper; the remedy for a user who
|
||||
// wants a literal-units ramp is the host's own curve tools, never a change to the taper.
|
||||
double toPlain(DeckParam deck, double normalized);
|
||||
double toNormalized(DeckParam deck, double plain);
|
||||
|
||||
// The filter's four tone controls STORE their normalized position (payload v9), so their default
|
||||
// normalized value is that stored double verbatim and no taper participates in a host's
|
||||
// reset-to-default. Reporting Hz / Q / drive depth for them means CALLING their frozen laws, not
|
||||
// replacing them.
|
||||
bool storesNormalized(DeckParam deck);
|
||||
|
||||
// The default, read off a default-constructed PlaySeconds — there is no second table of defaults,
|
||||
// and no normalized default is ever written as a literal. defaultNormalized is COMPUTED as
|
||||
// toNormalized(defaultPlain) for every tapered control, which is what makes a host's
|
||||
// reset-to-default and the editor's double-click land on the same value.
|
||||
double defaultPlain(DeckParam deck);
|
||||
double defaultNormalized(DeckParam deck);
|
||||
|
||||
} // namespace reasampler::instrument::param
|
||||
@@ -204,6 +204,11 @@ LiveCommit deckParamCommit(DeckParam id) {
|
||||
// The one note-on-latched control; the header owns why.
|
||||
case DeckParam::kRate:
|
||||
return LiveCommit::NoteOnLatched;
|
||||
// Live by the tier's own definition — one atomic store the audio thread picks up at the
|
||||
// next block, no bridge read and no re-decode. It reaches the audio beside the live
|
||||
// block rather than through it, which is why it carries no LiveValues field; that is a
|
||||
// question of ROUTE, and this predicate answers TIER.
|
||||
case DeckParam::kMasterGain:
|
||||
case DeckParam::kPitch:
|
||||
case DeckParam::kAttack:
|
||||
case DeckParam::kHold:
|
||||
@@ -270,7 +275,6 @@ LiveCommit deckParamCommit(DeckParam id) {
|
||||
case DeckParam::kVoiceCount:
|
||||
case DeckParam::kVoiceMode:
|
||||
case DeckParam::kMonoTrigger:
|
||||
case DeckParam::kMasterGain:
|
||||
case DeckParam::kLimiterEnable:
|
||||
// MASTER's two readouts reach no parameter at all — the same footing as the overlay
|
||||
// radios above.
|
||||
|
||||
@@ -164,6 +164,11 @@ DeckParam curveParamFor(DeckParam knob);
|
||||
// Both amp shapes are live: the Trigger fade pair that used to reload folded into the AHD and
|
||||
// inherited its routing, so a Trigger-mode instance now tracks its amplitude knobs too.
|
||||
//
|
||||
// kMasterGain is Live and is the one live control that does NOT ride the live block: it is a
|
||||
// lock-free atomic on the processor which the audio thread applies as a post-sum multiply. The
|
||||
// tier answers "does an edit reach the audio without a reload", not "which mechanism carries
|
||||
// it" — classifying it Reload would have said a gain move re-decodes the WAV, which it never did.
|
||||
//
|
||||
// kRate is the one NoteOnLatched control, and the reason is a real feature rather than a
|
||||
// plumbing detail: loop points and contours both scale with rate, and both are note-on folds —
|
||||
// resolveLoop runs once per note-on and a contour resolves against the note's own span. A live
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
#include "core/instrument/engine/filter/filter_morph.h" // MorphLaw (the law toggle's value)
|
||||
#include "core/instrument/engine/master_gain.h" // the dB taper the whole-dB snap reads
|
||||
@@ -374,10 +373,4 @@ double snapDeckParamNorm(DeckParam id, double norm) {
|
||||
return norm;
|
||||
}
|
||||
|
||||
void formatEnvTimeMs(double seconds, char* buf, std::size_t len) {
|
||||
if (!buf || len == 0) return;
|
||||
const double ms = seconds * 1000.0;
|
||||
std::snprintf(buf, len, ms < 10.0 ? "%.1f ms" : "%.0f ms", ms);
|
||||
}
|
||||
|
||||
} // namespace reasampler::instrument::ui
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
// deck_values.h — the deck's control-id <-> parameter-set BINDING and its display units: the
|
||||
// normalized 0..1 a knob shows, the write back into the stored seconds/fractions/positions, the
|
||||
// double-click reset, and the ms time-constant formatter. Split from the editor shell so the
|
||||
// whole domain map is provable without a host; deck_groups owns WHICH controls exist, this owns
|
||||
// what each one's value MEANS.
|
||||
// deck_values.h — the deck's control-id <-> parameter-set BINDING and its snap units: the
|
||||
// normalized 0..1 a knob shows, the write back into the stored seconds/fractions/positions, and
|
||||
// the double-click reset. Split from the editor shell so the whole domain map is provable
|
||||
// without a host; deck_groups owns WHICH controls exist, this owns what each one's value MEANS,
|
||||
// and param/param_format owns how it READS.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "core/instrument/engine/time_stretch.h" // kStretchRateMin/Max (Rate's own range)
|
||||
#include "core/instrument/map/play_seconds.h" // PlaySeconds (the deck's edit target)
|
||||
#include "core/instrument/ui/deck_groups.h" // DeckParam
|
||||
@@ -75,9 +73,4 @@ UnitCategory deckParamUnit(DeckParam id);
|
||||
// whole displayed percent and therefore take different norm steps.
|
||||
double snapDeckParamNorm(DeckParam id, double norm);
|
||||
|
||||
// A time constant as MILLISECONDS, e.g. "12 ms". Never switches to seconds: the editor reads in
|
||||
// one unit so two stage times are comparable at a glance. Sub-10 ms keeps one decimal so a short
|
||||
// attack is not rounded to a bare "0 ms". Writes at most `len` bytes including the terminator.
|
||||
void formatEnvTimeMs(double seconds, char* buf, std::size_t len);
|
||||
|
||||
} // namespace reasampler::instrument::ui
|
||||
|
||||
Reference in New Issue
Block a user