Anklang 0.3.0-460-gc4ef46ba
ASE — Anklang Sound Engine (C++)

« « « Anklang Documentation
Loading...
Searching...
No Matches
parameter.cc
Go to the documentation of this file.
1 // This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0
2#include "parameter.hh"
3#include "api.hh"
4#include "levenshtein.hh"
5#include "unicode.hh"
6#include "regex.hh"
7#include "strings.hh"
8#include "mathutils.hh"
9#include "internal.hh"
10
11namespace Ase {
12
13// == Param ==
15Param::fetch (const String &key) const
16{
17 return kvpairs_fetch (metadata, key);
18}
19
20void
21Param::store (const String &key, const String &v)
22{
23 kvpairs_assign (metadata, key + '=' + v);
24}
25
26// == ParamExtraVals ==
27ParamExtraVals::ParamExtraVals (double vmin, double vmax, double step)
28{
30 Base::operator= (MinMaxStep { vmin, vmax, step });
31}
32
33ParamExtraVals::ParamExtraVals (const MinMaxStep &range)
34{
36 Base::operator= (range);
37}
38ParamExtraVals::ParamExtraVals (const std::initializer_list<Choice> &choices)
39{
40 *this = ChoiceS (choices);
41}
42
43ParamExtraVals::ParamExtraVals (const ChoiceS &choices)
44{
46 Base::operator= (choices);
47}
48
49ParamExtraVals::ParamExtraVals (const ChoicesFunc &choicesfunc)
50{
52 Base::operator= (choicesfunc);
53}
54
55// == Parameter ==
57Parameter::construct_hints (const String &hints, const String &more, double pmin, double pmax)
58{
59 String h = hints;
60 if (h.empty())
61 h = STANDARD;
62 if (h[0] != ':')
63 h = ":" + h;
64 if (h.back() != ':')
65 h = h + ":";
66 if (pmax > 0 && pmax == -pmin && "" == string_option_find (h, "bidir", ""))
67 h += "bidir:";
68 if (pmin != pmax && "" == string_option_find (h, "range", ""))
69 h += "range:";
70 for (const auto &s : string_split (more, ":"))
71 if (!s.empty() && "" == string_option_find (h, s, ""))
72 h += s + ":";
73 if (h.back() != ':')
74 h = h + ":";
75 return h;
76}
77
79Parameter::nick () const
80{
81 String nick = fetch ("nick");
82 if (nick.empty())
83 nick = parameter_guess_nick (label());
84 return nick;
85}
86
87bool
88Parameter::has (const String &key) const
89{
90 return key == "ident" || kvpairs_search (metadata_, key) >= 0;
91}
92
94Parameter::fetch (const String &key) const
95{
96 return_unless (key != "ident", cident);
97 return kvpairs_fetch (metadata_, key);
98}
99
100void
101Parameter::store (const String &key, const String &value)
102{
103 assert_return (key.size() > 0);
104 if (key == "ident")
105 cident = value;
106 else {
107 const std::string kv = key + '=' + value;
108 kvpairs_assign (metadata_, kv);
109 }
110}
111
114{
115 if (const auto mms = std::get_if<MinMaxStep> (&extras_))
116 return *mms;
117 const ChoiceS cs = choices();
118 if (cs.size())
119 return { 0, cs.size() -1, 1 };
120 return { NAN, NAN, NAN };
121}
122
123ChoiceS
124Parameter::choices () const
125{
126 if (const auto cs = std::get_if<ChoiceS> (&extras_))
127 return *cs;
128 if (const auto cf = std::get_if<ChoicesFunc> (&extras_))
129 return (*cf) (cident);
130 return {};
131}
132
133bool
134Parameter::is_numeric () const
135{
136 if (is_text())
137 return false;
138 auto [i, a, s] = range();
139 return i != a;
140}
141
142void
143Parameter::initialsync (const Value &v)
144{
145 initial_ = v;
146}
147
148bool
149Parameter::has_hint (const String &hint) const
150{
151 const String hints_ = hints();
152 size_t pos = 0;
153 while ((pos = hints_.find (hint, pos)) != std::string::npos) {
154 if ((pos == 0 || hints_[pos-1] == ':') && (pos + hint.size() == hints_.size() || hints_[pos + hint.size()] == ':'))
155 return true;
156 pos += hint.size();
157 }
158 return false;
159}
160
161double
162Parameter::normalize (double val) const
163{
164 const auto [fmin, fmax, step] = range();
165 if (std::abs (fmax - fmin) < F32EPS)
166 return 0;
167 const double normalized = (val - fmin) / (fmax - fmin);
168 assert_return (normalized >= 0.0 && normalized <= 1.0, normalized);
169 return normalized;
170}
171
172double
173Parameter::rescale (double t) const
174{
175 const auto [fmin, fmax, step] = range();
176 const double value = fmin + t * (fmax - fmin);
177 assert_return (t >= 0.0 && t <= 1.0, value);
178 return value;
179}
180
181size_t
182Parameter::match_choice (const ChoiceS &choices, const String &text)
183{
184 for (size_t i = 0; i < choices.size(); i++)
185 if (text == choices[i].ident)
186 return i;
187 size_t selected = 0;
188 const String ltext = string_tolower (text);
189 float best = F32MAX;
190 for (size_t i = 0; i < choices.size(); i++) {
191 const size_t maxdist = std::max (choices[i].ident.size(), ltext.size());
192 const float dist = damerau_levenshtein_restricted (string_tolower (choices[i].ident), ltext) / maxdist;
193 if (dist < best) {
194 best = dist;
195 selected = i;
196 }
197 }
198 return selected;
199}
200
201Value
202Parameter::constrain (const Value &value) const
203{
204 // choices
205 if (is_choice()) {
206 const ChoiceS choices = this->choices();
207 if (value.is_numeric()) {
208 int64_t i = value.as_int();
209 if (i >= 0 && i < choices.size())
210 return choices[i].ident;
211 }
212 const size_t selected = value.is_string() ? match_choice (choices, value.as_string()) : 0;
213 return choices.size() ? choices[selected].ident : initial_;
214 }
215 // text
216 if (is_text())
217 return value.as_string();
218 // numeric
219 return dconstrain (value);
220}
221
222double
223Parameter::dconstrain (const Value &value) const
224{
225 // choices
226 if (is_choice()) {
227 const ChoiceS choices = this->choices();
228 if (value.is_numeric()) {
229 int64_t i = value.as_int();
230 if (i >= 0 && i < choices.size())
231 return i;
232 }
233 const size_t selected = value.is_string() ? match_choice (choices, value.as_string()) : 0;
234 return choices.size() ? selected : initial_.is_numeric() ? initial_.as_double() : 0;
235 }
236 // text
237 if (is_text())
238 return value.as_double();
239 // numeric
240 double val = value.as_double();
241 const auto [fmin, fmax, step] = range();
242 if (std::abs (fmax - fmin) < F32EPS)
243 return fmin;
244 val = std::max (val, fmin);
245 val = std::min (val, fmax);
246 if (step > F32EPS && has_hint ("stepped")) {
247 // round halfway cases down, so:
248 // 0 -> -0.5…+0.5 yields -0.5
249 // 1 -> -0.5…+0.5 yields +0.5
250 constexpr const auto nearintoffset = 0.5 - DOUBLE_EPSILON; // round halfway case down
251 const double t = std::floor ((val - fmin) / step + nearintoffset);
252 val = fmin + t * step;
253 val = std::min (val, fmax);
254 }
255 return val;
256}
257
258static MinMaxStep
259minmaxstep_from_initialval (const Param::InitialVal &iv, bool *isbool)
260{
261 MinMaxStep range;
262 std::visit ([&] (auto &&arg)
263 {
264 using T = std::decay_t<decltype (arg)>;
265 if constexpr (std::is_same_v<T, bool>)
266 range = { 0, 1, 1 };
267 else if constexpr (std::is_same_v<T, int8_t>)
268 range = { -128, 127, 1 };
269 else if constexpr (std::is_same_v<T, uint8_t>)
270 range = { 0, 255, 1 };
271 else if constexpr (std::is_same_v<T, int16_t>)
272 range = { -32768, 32767, 1 };
273 else if constexpr (std::is_same_v<T, uint16_t>)
274 range = { 0, 65536, 1 };
275 else if constexpr (std::is_same_v<T, int32_t>)
276 range = { -2147483648, 2147483647, 1 };
277 else if constexpr (std::is_same_v<T, uint32_t>)
278 range = { 0, 4294967295, 1 };
279 else if constexpr (std::is_same_v<T, int64_t>)
280 range = { -9223372036854775807-1, 9223372036854775807, 1 };
281 else if constexpr (std::is_same_v<T, uint64_t>)
282 range = { 0, 18446744073709551615ull, 1 };
283 else if constexpr (std::is_same_v<T, float>)
284 range = { -F32MAX, F32MAX, 0 };
285 else if constexpr (std::is_same_v<T, double>)
286 range = { -D64MAX, D64MAX, 0 };
287 else if constexpr (std::is_same_v<T, const char*> ||
289 range = { 0, I31MAX, 1 }; // strings can contain Quark IDs
290 else
291 static_assert (sizeof (T) < 0, "unimplemented InitialVal type");
292 if (isbool)
293 *isbool = std::is_same_v<T, bool>;
294 }, iv);
295 return range;
296}
297
298static Value
299value_from_initialval (const Param::InitialVal &iv)
300{
301 Value value;
302 std::visit ([&value] (auto &&arg)
303 {
304 using T = std::decay_t<decltype (arg)>;
305 if constexpr (std::is_same_v<T, bool>)
306 value = arg;
307 else if constexpr (std::is_same_v<T, int8_t> ||
312 value = int32_t (arg);
313 else if constexpr (std::is_same_v<T, uint32_t>)
314 value = arg;
315 else if constexpr (std::is_same_v<T, int64_t> ||
317 value = int64_t (arg);
318 else if constexpr (std::is_same_v<T, float> ||
320 value = arg;
321 else if constexpr (std::is_same_v<T, const char*>)
322 value = arg;
323 else if constexpr (std::is_same_v<T, std::string>)
324 value = arg;
325 else
326 static_assert (sizeof (T) < 0, "unimplemented InitialVal type");
327 }, iv);
328 return value;
329}
330
331Parameter::Parameter (const Param &initparam)
332{
333 const Param &p = initparam;
334 cident = kvpairs_fetch (p.metadata, "ident");
335 if (!p.ident.empty()) {
336 cident = string_to_ncname (p.ident);
337 kvpairs_assign (metadata_, "ident=" + cident);
338 } else if (cident.empty()) {
339 cident = string_to_ncname (p.label, '_');
340 kvpairs_assign (metadata_, "ident=" + cident);
341 }
342 metadata_ = p.metadata; // enable fetch()
343 const auto choicesfuncp = std::get_if<ChoicesFunc> (&p.extras);
345 if (const auto rangep = std::get_if<MinMaxStep> (&p.extras))
346 range = *rangep;
347 const auto [fmin, fmax, step] = range;
348 if (!p.label.empty())
349 store ("label", p.label);
350 if (!p.nick.empty())
351 store ("nick", p.nick);
352 if (!p.unit.empty())
353 store ("unit", p.unit);
354 const auto choicesp = std::get_if<ChoiceS> (&p.extras);
355 bool isbool = false;
356 if (choicesfuncp)
357 extras_ = *choicesfuncp;
358 else if (choicesp)
359 extras_ = *choicesp;
360 else if (fmin != fmax)
361 extras_ = range;
362 else
363 extras_ = minmaxstep_from_initialval (p.initial, &isbool);
364 initial_ = value_from_initialval (p.initial);
365 String hints = p.hints.empty() ? STANDARD : p.hints;
366 String choice = choicesp || choicesfuncp ? "choice" : "";
367 String text = choicesfuncp || initial_.is_string() ? "text" : "";
368 String dynamic = choicesfuncp ? "dynamic" : "";
369 String stepped = isbool ? "stepped" : "";
370 store ("hints", construct_hints (p.hints, fetch ("hints") + ":" + text + ":" + choice + ":" + dynamic + ":" + stepped, fmin, fmax));
371 assert_return (!cident.empty());
372}
373
374String
375Parameter::value_to_text (const Value &value) const
376{
377 if (is_choice())
378 return constrain (value).as_string();
379 const Value::Type type = value.index();
380 if (type != Value::BOOL && type != Value::INT64 && type != Value::DOUBLE)
381 return value.as_string();
382 double val = value.as_double();
383 String unit = this->unit();
384 if (unit == "Hz" && fabs (val) >= 1000)
385 {
386 unit = "kHz";
387 val /= 1000;
388 }
389 int fdigits = 2; // fractional digits
390 if (fabs (val) < 10)
391 fdigits = 2;
392 else if (fabs (val) < 100)
393 fdigits = 1;
394 else if (fabs (val) < 1000)
395 fdigits = 0;
396 else
397 fdigits = 0;
398 const auto [fmin, fmax, step] = range();
399 const bool need_sign = fmin < 0;
400 String s = need_sign ? string_format ("%+.*f", fdigits, val) : string_format ("%.*f", fdigits, val);
401 if (fdigits == 0 && fabs (val) == 100 && unit == "%")
402 s += "."; // use '100. %' for fixed with of percent numbers
403 if (!unit.empty())
404 s += " " + unit;
405 return s;
406}
407
408Value
409Parameter::value_from_text (const String &text) const
410{
411 if (is_choice()) {
412 const ChoiceS choices = this->choices();
413 return int64_t (match_choice (choices, text));
414 }
415 if (is_text())
416 return constrain (text).as_string();
417 return constrain (string_to_double (text));
418}
419
420// == ParameterMap ==
421ParameterMap::Entry
423{
424 return { *this, id };
425}
426
427void
428ParameterMap::Entry::operator= (const Param &param)
429{
430 const char *const FUNC = "ParameterMap::Entry::operator=";
432 ParameterC old = pmap[id];
433 if (old)
434 warning ("%s: reassign parameter '%s' with: %s\n", FUNC, old->ident(), param.ident);
435 else if (id > 0) {
436 auto it = pmap.find (id - 1);
437 if (it != pmap.end() && it->second) {
438 // catch erroneous CnP reuse
439 if (it->second->ident() == param.ident)
440 warning ("%s: duplicate %s: '%s' (param_id=%u)", FUNC, "ident", param.ident, id);
441 else if (it->second->label() == param.label)
442 warning ("%s: duplicate %s: '%s' (param_id=%u)", FUNC, "label", param.label, id);
443 }
444 }
445 if (!map.group.empty() && param.fetch ("group").empty()) {
446 Param mparam = param;
447 mparam.store ("group", map.group);
448 pmap[id] = std::make_shared<Parameter> (mparam);
449 } else
450 pmap[id] = std::make_shared<Parameter> (param);
451}
452
453// == Property ==
454String
456{
457 return kvpairs_fetch (metadata, "hints");
458}
459
460String
462{
463 return kvpairs_fetch (metadata, "blurb");
464}
465
466String
468{
469 return kvpairs_fetch (metadata, "descr");
470}
471
472String
474{
475 return kvpairs_fetch (metadata, "group");
476}
477
478// == guess_nick ==
480
481// Fast version of Re::search (R"(\d)")
482static ssize_t
483search_first_digit (const String &s)
484{
485 for (size_t i = 0; i < s.size(); ++i)
486 if (isdigit (s[i]))
487 return i;
488 return -1;
489}
490
491// Fast version of Re::search (R"(\d\d?\b)")
492static ssize_t
493search_last_digits (const String &s)
494{
495 for (size_t i = 0; i < s.size(); ++i)
496 if (isdigit (s[i])) {
497 if (isdigit (s[i+1]) && !isalnum (s[i+2]))
498 return i;
499 else if (!isalnum (s[i+1]))
500 return i;
501 }
502 return -1;
503}
504
505// Exract up to 3 useful letters or words from label
506static String3
507make_nick3 (const String &label)
508{
509 // split words
510 const StringS words = Re::findall (R"(\b\w+)", label); // TODO: allow Re caching
511
512 // single word nick, give precedence to digits
513 if (words.size() == 1) {
514 const ssize_t d = search_first_digit (words[0]);
515 if (d > 0 && isdigit (words[0][d + 1])) // A11
516 return { words[0].substr (0, 1), words[0].substr (d, 2), "" };
517 if (d > 0) // Aa1
518 return { words[0].substr (0, 2), words[0].substr (d, 1), "" };
519 else // Aaa
520 return { words[0].substr (0, 3), "", "" };
521 }
522
523 // two word nick, give precedence to second word digits
524 if (words.size() == 2) {
525 const ssize_t e = search_last_digits (words[1]);
526 if (e >= 0 && isdigit (words[1][e+1])) // A22
527 return { words[0].substr (0, 1), words[1].substr (e, 2), "" };
528 if (e > 0) // AB2
529 return { words[0].substr (0, 1), words[1].substr (0, 1), words[1].substr (e, 1) };
530 if (e == 0) // Aa2
531 return { words[0].substr (0, 2), words[1].substr (e, 1), "" };
532 const ssize_t d = search_first_digit (words[0]);
533 if (d > 0) // A1B
534 return { words[0].substr (0, 1), words[0].substr (d, 1), words[1].substr (0, 1) };
535 if (words[1].size() > 1) // ABb
536 return { words[0].substr (0, 1), words[1].substr (0, 2), "" };
537 else // AaB
538 return { words[0].substr (0, 2), words[1].substr (0, 1), "" };
539 }
540
541 // 3+ word nick
542 if (words.size() >= 3) {
543 ssize_t i, e = -1; // digit pos in last possible word
544 for (i = words.size() - 1; i > 1; i--) {
545 e = search_last_digits (words[i]);
546 if (e >= 0)
547 break;
548 }
549 if (e >= 0 && isdigit (words[i][e + 1])) // A77
550 return { words[0].substr (0, 1), words[i].substr (e, 2), "" };
551 if (e >= 0 && i + 1 < words.size()) // A7G
552 return { words[0].substr (0, 1), words[i].substr (e, 1), words[i+1].substr (0, 1) };
553 if (e > 0) // AF7
554 return { words[0].substr (0, 1), words[i].substr (0, 1), words[i].substr (e, 1) };
555 if (e == 0 && i >= 3) // AE7
556 return { words[0].substr (0, 1), words[i-1].substr (0, 1), words[i].substr (e, 1) };
557 if (e == 0 && i >= 2) // AB7
558 return { words[0].substr (0, 1), words[1].substr (0, 1), words[i].substr (e, 1) };
559 if (e == 0) // Aa7
560 return { words[0].substr (0, 2), words[i].substr (e, 1), "" };
561 if (words.back().size() >= 2) // AFf
562 return { words[0].substr (0, 1), words.back().substr (0, 2), "" };
563 else // AEF
564 return { words[0].substr (0, 1), words[words.size()-1].substr (0, 1), words.back().substr (0, 1) };
565 }
566
567 // pathological name
568 return { words[0].substr (0, 3), "", "" };
569}
570
571// Re::sub (R"(([a-zA-Z])([0-9]))", "$1 $2", s)
572static String
573spaced_nums (String s)
574{
575 for (ssize_t i = s.size() - 1; i > 0; i--)
576 if (isdigit (s[i]) && !isdigit (s[i-1]) && !isspace (s[i-1]))
577 s.insert (s.begin() + i, ' ');
578 return s;
579}
580
582String
583parameter_guess_nick (const String &parameter_label)
584{
585 // seperate numbers from words, increases word count
586 String string = spaced_nums (parameter_label);
587
588 // use various letter extractions to construct nick portions
589 const auto& [a, b, c] = make_nick3 (string);
590
591 // combine from right to left to increase word variance
592 String nick;
593 if (c.size() > 0)
594 nick = a.substr (0, 1) + b.substr (0, 1) + c.substr (0, 1);
595 else if (b.size() > 0)
596 nick = a.substr (0, 1) + b.substr (0, 2);
597 else
598 nick = a.substr (0, 3);
599 return nick;
600}
601
602} // Ase
String descr() const
Elaborate description, e.g. for help dialogs (metadata).
Definition parameter.cc:467
String blurb() const
Short description for user interface tooltips (metadata).
Definition parameter.cc:461
String hints() const
Hints for parameter handling (metadata).
Definition parameter.cc:455
String group() const
Group name for parameters of similar function (metadata).
Definition parameter.cc:473
static StringS findall(const String &regex, const String &input, Flags=DEFAULT)
Find regex in input and return non-overlapping matches.
Definition regex.cc:233
T empty(T... args)
T floor(T... args)
fmax
fmin
T insert(T... args)
#define assert_return(expr,...)
Return from the current function if expr is unmet and issue an assertion warning.
Definition internal.hh:29
#define return_unless(cond,...)
Return silently if cond does not evaluate to true with return value ...
Definition internal.hh:71
isdigit
isspace
T max(T... args)
T min(T... args)
The Anklang C++ API namespace.
Definition api.hh:9
std::string string_format(const char *format, const Args &...args) __attribute__((__format__(__printf__
Format a string similar to sprintf(3) with support for std::string and std::ostringstream convertible...
StringS string_split(const String &string, const String &splitter, size_t maxn)
Definition strings.cc:343
String parameter_guess_nick(const String &parameter_label)
Create a few letter nick name from a multi word parameter label.
Definition parameter.cc:583
String string_tolower(const String &str)
Convert all string characters into Unicode lower case characters.
Definition strings.cc:136
std::tuple< double, double, double > MinMaxStep
Min, max, stepping for double ranges.
Definition parameter.hh:12
std::vector< String > StringS
Convenience alias for a std::vector<std::string>.
Definition cxxaux.hh:36
std::function< ChoiceS(const CString &)> ChoicesFunc
Handler to generate all possible parameter choices dynamically.
Definition parameter.hh:15
String string_option_find(const String &optionlist, const String &feature, const String &fallback)
Retrieve the option value from an options list separated by ':' or ';' or fallback.
Definition strings.cc:1404
double string_to_double(const String &string)
Parse a double from a string, trying locale specific characters and POSIX/C formatting.
Definition strings.cc:674
std::string String
Convenience alias for std::string.
Definition cxxaux.hh:35
constexpr const char STANDARD[]
STORAGE GUI READABLE WRITABLE.
Definition api.hh:14
float damerau_levenshtein_restricted(const std::string &source, const std::string &target, const float ci, const float cd, const float cs, const float ct)
String string_to_ncname(const String &input, uint32_t substitute)
Definition unicode.cc:339
constexpr const double DOUBLE_EPSILON
Double round-off error at 1.0, equals 2^-53.
Definition mathutils.hh:10
T size(T... args)
typedef int64_t
Structured initializer for Parameter.
Definition parameter.hh:31
StringS metadata
Array of "key=value" pairs.
Definition parameter.hh:41
String label
Preferred user interface name.
Definition parameter.hh:35
String ident
Identifier used for serialization (can be derived from untranslated label).
Definition parameter.hh:34
Entry operator[](uint32_t id)
Slot subscription for new Parameter creation.
Definition parameter.cc:422
String group
Group to be applied to all newly inserted Parameter objects.
Definition parameter.hh:95
MinMaxStep range() const
Min, max, stepping for double ranges.
Definition parameter.cc:113
double as_double() const
Convert Value to double or return 0.
Definition value.cc:77
bool is_numeric(bool boolisnumeric=true) const
Checks if Value is a DOUBLE, INT64, or BOOL.
Definition value.cc:51
String as_string() const
Convert Value to a string, not very useful for RECORD or ARRAY.
Definition value.cc:95
T substr(T... args)
typedef ssize_t
T visit(T... args)