// 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); // WHERE a control's value actually lives. The host's read and write paths branch on this, and // the exposed set is asserted against it: a control promoted into the list with no home would // otherwise no-op silently in BOTH directions, with nothing to catch it at compile time. enum class ValueHome { None, // not a scalar control at all — a toggle, a radio, a curve-popup cell ParamSetNorm, // the filter's four: the stored double IS the normalized position ParamSet, // every other knob the parameter set carries InstanceScalar, // beside the parameter set: master gain, and the pitch key-track scalar }; ValueHome valueHomeFor(DeckParam deck); // 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