Skip to content

Commit

Permalink
Merge branch 'constrain-parameters'
Browse files Browse the repository at this point in the history
* constrain-parameters:
  UI: b/databubble.js: fix assertion
  ASE: processor.cc: use parameter->dconstrain()
  ASE: parameter: add dconstrain() to convert to double and constrain
  ASE: processor.cc: use Parameter::construct_hints()
  ASE: parameter: export construct_hints(), always use "stepped" hint for bools
  UI: b/propinput.js: give toggles preference over choices
  ASE: processor.cc: remove unused choices from boolean param
  ASE: serialize.cc: call jsonvalue_to_string() directly
  ASE: jsonapi.cc: call jsonobject_to_string() directly
  JSONIPC: jsonipc.hh: always write null instead of NAN, ±Inf
	Remove JsonWriteFlags from the (template) API.
  JSONIPC: Makefile: fix include path to external/rapidjson/
  EXTERNAL: rapidjson: update Tencent/rapidjson to 2023-09-28 11:36:59 +0200
	git -C external/rapidjson checkout f9d53419e912910fd8fa57d5705fa41425428c35
	Among other changes, this provides kWriteNanAndInfNullFlag.
  ASE: parameter: value_from_text(): yield integers for text inputs of choices

Signed-off-by: Tim Janik <[email protected]>
  • Loading branch information
tim-janik committed Nov 1, 2023
2 parents 8d56822 + 4df4d83 commit fff51b9
Show file tree
Hide file tree
Showing 10 changed files with 101 additions and 101 deletions.
4 changes: 2 additions & 2 deletions ase/jsonapi.cc
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ class JsonapiConnection : public WebSocketConnection, public CustomDataContainer
{
JsonapiConnectionP selfp = selfw.lock();
return_unless (selfp);
const String msg = jsonobject_to_string<Jsonipc::STRICT> ("method", id /*"Jsonapi/Trigger/_%%%"*/, "params", args);
const String msg = jsonobject_to_string ("method", id /*"Jsonapi/Trigger/_%%%"*/, "params", args);
if (logflags & 8)
selfp->log (string_format ("⬰ %s", msg));
selfp->send_text (msg);
Expand All @@ -175,7 +175,7 @@ class JsonapiConnection : public WebSocketConnection, public CustomDataContainer
if (selfp->is_open())
{
ValueS args { id };
const String msg = jsonobject_to_string<Jsonipc::STRICT> ("method", "Jsonapi/Trigger/killed", "params", args);
const String msg = jsonobject_to_string ("method", "Jsonapi/Trigger/killed", "params", args);
if (logflags & 8)
selfp->log (string_format ("↚ %s", msg));
selfp->send_text (msg);
Expand Down
121 changes: 77 additions & 44 deletions ase/parameter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "levenshtein.hh"
#include "unicode.hh"
#include "regex.hh"
#include "mathutils.hh"
#include "internal.hh"

namespace Ase {
Expand Down Expand Up @@ -31,8 +32,8 @@ Param::ExtraVals::ExtraVals (const ChoicesFunc &choicesfunc)
}

// == Parameter ==
static String
construct_hints (const String &hints, const String &more, double pmin, double pmax)
String
Parameter::construct_hints (const String &hints, const String &more, double pmin, double pmax)
{
String h = hints;
if (h.empty())
Expand Down Expand Up @@ -159,6 +160,26 @@ Parameter::rescale (double t) const
return value;
}

size_t
Parameter::match_choice (const ChoiceS &choices, const String &text)
{
for (size_t i = 0; i < choices.size(); i++)
if (text == choices[i].ident)
return i;
size_t selected = 0;
const String ltext = string_tolower (text);
float best = F32MAX;
for (size_t i = 0; i < choices.size(); i++) {
const size_t maxdist = std::max (choices[i].ident.size(), ltext.size());
const float dist = damerau_levenshtein_restricted (string_tolower (choices[i].ident), ltext) / maxdist;
if (dist < best) {
best = dist;
selected = i;
}
}
return selected;
}

Value
Parameter::constrain (const Value &value) const
{
Expand All @@ -170,50 +191,54 @@ Parameter::constrain (const Value &value) const
if (i >= 0 && i < choices.size())
return choices[i].ident;
}
int64_t selected = 0;
if (value.is_string()) {
const String text = value.as_string();
for (size_t i = 0; i < choices.size(); i++)
if (text == choices[i].ident)
return choices[i].ident;
const String ltext = string_tolower (text);
float best = F32MAX;
for (size_t i = 0; i < choices.size(); i++) {
const size_t maxdist = std::max (choices[i].ident.size(), ltext.size());
const float dist = damerau_levenshtein_restricted (string_tolower (choices[i].ident), ltext) / maxdist;
if (dist < best) {
best = dist;
selected = i;
}
}
}
const size_t selected = value.is_string() ? match_choice (choices, value.as_string()) : 0;
return choices.size() ? choices[selected].ident : initial_;
}
// text
if (is_text())
return value.as_string();
// numeric
return dconstrain (value);
}

double
Parameter::dconstrain (const Value &value) const
{
// choices
if (is_choice()) {
const ChoiceS choices = this->choices();
if (value.is_numeric()) {
int64_t i = value.as_int();
if (i >= 0 && i < choices.size())
return i;
}
const size_t selected = value.is_string() ? match_choice (choices, value.as_string()) : 0;
return choices.size() ? selected : initial_.is_numeric() ? initial_.as_double() : 0;
}
// numeric
double val = value.as_double();
const auto [fmin, fmax, step] = range();
if (std::abs (fmax - fmin) < F32EPS)
return fmin;
val = std::max (val, fmin);
val = std::min (val, fmax);
if (step > F32EPS && has_hint ("stepped")) {
double t = val - fmin;
t /= step;
t = std::round (t);
// round halfway cases down, so:
// 0 -> -0.5…+0.5 yields -0.5
// 1 -> -0.5…+0.5 yields +0.5
constexpr const auto nearintoffset = 0.5 - DOUBLE_EPSILON; // round halfway case down
const double t = std::floor ((val - fmin) / step + nearintoffset);
val = fmin + t * step;
val = std::min (val, fmax);
}
return val;
}

static MinMaxStep
minmaxstep_from_initialval (const Param::InitialVal &iv)
minmaxstep_from_initialval (const Param::InitialVal &iv, bool *isbool)
{
MinMaxStep range;
std::visit ([&range] (auto &&arg)
std::visit ([&] (auto &&arg)
{
using T = std::decay_t<decltype (arg)>;
if constexpr (std::is_same_v<T, bool>)
Expand Down Expand Up @@ -244,6 +269,8 @@ minmaxstep_from_initialval (const Param::InitialVal &iv)
range = { 0, 0, 0 }; // strings have no numeric range
else
static_assert (sizeof (T) < 0, "unimplemented InitialVal type");
if (isbool)
*isbool = std::is_same_v<T, bool>;
}, iv);
return range;
}
Expand All @@ -255,16 +282,18 @@ value_from_initialval (const Param::InitialVal &iv)
std::visit ([&value] (auto &&arg)
{
using T = std::decay_t<decltype (arg)>;
if constexpr (std::is_same_v<T, bool> ||
std::is_same_v<T, int8_t> ||
std::is_same_v<T, uint8_t> ||
std::is_same_v<T, int16_t> ||
std::is_same_v<T, uint16_t> ||
std::is_same_v<T, int32_t> ||
std::is_same_v<T, uint32_t> ||
std::is_same_v<T, int64_t>)
value = int64_t (arg);
else if constexpr (std::is_same_v<T, uint64_t>)
if constexpr (std::is_same_v<T, bool>)
value = arg;
else if constexpr (std::is_same_v<T, int8_t> ||
std::is_same_v<T, uint8_t> ||
std::is_same_v<T, int16_t> ||
std::is_same_v<T, uint16_t> ||
std::is_same_v<T, int32_t>)
value = int32_t (arg);
else if constexpr (std::is_same_v<T, uint32_t>)
value = arg;
else if constexpr (std::is_same_v<T, int64_t> ||
std::is_same_v<T, uint64_t>)
value = int64_t (arg);
else if constexpr (std::is_same_v<T, float> ||
std::is_same_v<T, double>)
Expand Down Expand Up @@ -302,21 +331,22 @@ Parameter::Parameter (const Param &initparam)
if (!p.group.empty())
store ("group", p.group);
const auto choicesp = std::get_if<ChoiceS> (&p.extras);
bool isbool = false;
if (choicesfuncp)
extras_ = *choicesfuncp;
else if (choicesp)
extras_ = *choicesp;
else if (fmin != fmax)
extras_ = range;
else
extras_ = minmaxstep_from_initialval (p.initial);
extras_ = minmaxstep_from_initialval (p.initial, &isbool);
initial_ = value_from_initialval (p.initial);
if (!p.hints.empty()) {
String choice = choicesp || choicesfuncp ? "choice" : "";
String text = choicesfuncp || initial_.is_string() ? "text" : "";
String dynamic = choicesfuncp ? "dynamic" : "";
store ("hints", construct_hints (p.hints, text + ":" + choice + ":" + dynamic, fmin, fmax));
}
String hints = p.hints.empty() ? STANDARD : p.hints;
String choice = choicesp || choicesfuncp ? "choice" : "";
String text = choicesfuncp || initial_.is_string() ? "text" : "";
String dynamic = choicesfuncp ? "dynamic" : "";
String stepped = isbool ? "stepped" : "";
store ("hints", construct_hints (p.hints, text + ":" + choice + ":" + dynamic + ":" + stepped, fmin, fmax));
}

String
Expand Down Expand Up @@ -356,10 +386,13 @@ Parameter::value_to_text (const Value &value) const
Value
Parameter::value_from_text (const String &text) const
{
if (is_choice() || is_text())
if (is_choice()) {
const ChoiceS choices = this->choices();
return int64_t (match_choice (choices, text));
}
if (is_text())
return constrain (text).as_string();
else
return constrain (string_to_double (text));
return constrain (string_to_double (text));
}

// == guess_nick ==
Expand Down
3 changes: 3 additions & 0 deletions ase/parameter.hh
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ struct Parameter {
double normalize (double val) const;
double rescale (double t) const;
Value constrain (const Value &value) const;
double dconstrain (const Value &value) const;
void initialsync (const Value &v);
/*ctor*/ Parameter () = default;
/*ctor*/ Parameter (const Param&);
Expand All @@ -69,6 +70,8 @@ struct Parameter {
// helpers
String value_to_text (const Value &value) const;
Value value_from_text (const String &text) const;
static String construct_hints (const String &hints, const String &more, double pmin = 0, double pmax = 0);
static size_t match_choice (const ChoiceS &choices, const String &text);
private:
using ExtrasV = std::variant<MinMaxStep,ChoiceS,ChoicesFunc>;
StringS details_;
Expand Down
44 changes: 6 additions & 38 deletions ase/processor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -156,23 +156,6 @@ AudioProcessor::start_group (const String &groupname) const
tls_param_group = groupname;
}

static CString
construct_hints (String hints, double pmin, double pmax, const String &more = "")
{
if (hints.empty())
hints = AudioProcessor::STANDARD;
if (hints.back() != ':')
hints = hints + ":";
for (const auto &s : string_split (more))
if (!s.empty() && "" == string_option_find (hints, s, ""))
hints += s + ":";
if (hints[0] != ':')
hints = ":" + hints;
if (pmax > 0 && pmax == -pmin)
hints += "bidir:";
return hints;
}

/// Helper for add_param() to generate the sequentially next ParamId.
ParamId
AudioProcessor::nextid () const
Expand Down Expand Up @@ -227,7 +210,7 @@ AudioProcessor::add_param (Id32 id, const String &clabel, const String &nickname
.initial = value,
.unit = unit,
.extras = MinMaxStep { pmin, pmax, 0 },
.hints = construct_hints (hints, pmin, pmax),
.hints = Parameter::construct_hints (hints, "", pmin, pmax),
.blurb = blurb,
.descr = description,
}, value);
Expand Down Expand Up @@ -259,7 +242,7 @@ AudioProcessor::add_param (Id32 id, const String &clabel, const String &nickname
.nick = nickname,
.initial = value,
.extras = centries,
.hints = construct_hints (hints, 0, pmax, "choice"),
.hints = Parameter::construct_hints (hints, "choice", 0, pmax),
.blurb = blurb,
.descr = description,
}, value);
Expand All @@ -285,17 +268,14 @@ AudioProcessor::add_param (Id32 id, const String &clabel, const String &nickname
{
assert_return (uint (id) > 0, ParamId (0));
using namespace MakeIcon;
static const ChoiceS centries { { "on"_icon, "Off" }, { "off"_icon, "On" } };
const double value = boolvalue ? 1.0 : 0.0;
return add_param (id, Param {
.label = clabel,
.nick = nickname,
.initial = value,
.extras = centries,
.hints = construct_hints (hints, false, true, "toggle"),
.initial = boolvalue,
.hints = Parameter::construct_hints (hints, "toggle", false, true),
.blurb = blurb,
.descr = description,
}, value);
}, boolvalue);
}

/// Variant of AudioProcessor::add_param() with sequential `id` generation.
Expand Down Expand Up @@ -342,19 +322,7 @@ AudioProcessor::set_param (Id32 paramid, const double value, bool sendnotify)
ParameterC parameter = pparam->parameter;
double v = value;
if (parameter)
{
const auto [fmin, fmax, stepping] = parameter->range();
v = CLAMP (v, fmin, fmax);
if (stepping > 0)
{
// round halfway cases down, so:
// 0 -> -0.5…+0.5 yields -0.5
// 1 -> -0.5…+0.5 yields +0.5
constexpr const auto nearintoffset = 0.5 - DOUBLE_EPSILON; // round halfway case down
v = stepping * uint64_t ((v - fmin) / stepping + nearintoffset);
v = CLAMP (fmin + v, fmin, fmax);
}
}
v = parameter->dconstrain (value);
const bool need_notify = const_cast<PParam*> (pparam)->assign (v);
if (need_notify && sendnotify && !pparam->aprop_.expired())
enotify_enqueue_mt (PARAMCHANGE);
Expand Down
2 changes: 1 addition & 1 deletion ase/serialize.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Jsonipc::InstanceMap::Wrapper*
Writ::InstanceMap::wrapper_from_json (const Jsonipc::JsonValue &value)
{
if (!value.IsNull())
warning ("Ase::Writ: non persistent object cannot resolve: %s*", Jsonipc::jsonvalue_to_string<Jsonipc::RELAXED> (value));
warning ("Ase::Writ: non persistent object cannot resolve: %s*", Jsonipc::jsonvalue_to_string (value));
//return nullptr;
return this->Jsonipc::InstanceMap::wrapper_from_json (value);
}
Expand Down
2 changes: 1 addition & 1 deletion jsonipc/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ OBJECTS = testjsonipc.o
DEFS = -DSTANDALONE
DEPS = jsonipc.hh Makefile

INCLUDES = -I../$(builddir)/external/
INCLUDES = -I../external/rapidjson/include/

$(OBJECTS): %.o: %.cc $(DEPS)
$(CXX) $(DEFS) $(CXXFLAGS) $(INCLUDES) -c $<
Expand Down
Loading

0 comments on commit fff51b9

Please sign in to comment.