From 36c7f0a3c916d85be037ff01085fcbdce60f92ed Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Thu, 5 Oct 2023 16:00:37 +0200 Subject: [PATCH 01/50] MISC: package.json.in: reduce wait delay for rebuilds Signed-off-by: Tim Janik --- misc/package.json.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/package.json.in b/misc/package.json.in index 42ecf182..52344a47 100644 --- a/misc/package.json.in +++ b/misc/package.json.in @@ -6,7 +6,7 @@ "serve": "run-p sound-engine monitor", "sound-engine": "lib/AnklangSynthEngine $ASEOPT", "ui/rebuild": "make ui/rebuild -C $npm_package_config_srcdir -j", - "monitor": "nodemon --ext '*' --watch $npm_package_config_srcdir/ui/ --delay 3500ms --on-change-only --exec 'npm run -s ui/rebuild' --exitcrash" + "monitor": "nodemon --ext '*' --watch $npm_package_config_srcdir/ui/ --delay 2500ms --on-change-only --exec 'npm run -s ui/rebuild' --exitcrash" }, "dependencies": { "vue": "=3.3.4" From 1918cd6f0a0c223aa790d5c0ae29c843fbde8f19 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sat, 7 Oct 2023 03:17:39 +0200 Subject: [PATCH 02/50] ASE: cxxaux.hh: add ASE_ASSERT_ALWAYS() Signed-off-by: Tim Janik --- ase/cxxaux.hh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ase/cxxaux.hh b/ase/cxxaux.hh index e91f752b..060f29e4 100644 --- a/ase/cxxaux.hh +++ b/ase/cxxaux.hh @@ -90,6 +90,9 @@ using VoidF = std::function; /// Like ASE_ASSERT_WARN(), enabled if expensive `expr` are allowed. #define ASE_ASSERT_PARANOID(expr) do { if (ASE_ISLIKELY (expr)) break; ::Ase::assertion_failed (#expr); } while (0) +/// Return from the current function if `expr` evaluates to false and issue an assertion warning. +#define ASE_ASSERT_ALWAYS(expr, ...) do { if (ASE_ISLIKELY (expr)) break; ::Ase::assertion_failed (#expr); __builtin_trap(); } while (0) + /// Delete copy ctor and assignment operator. #define ASE_CLASS_NON_COPYABLE(ClassName) \ /*copy-ctor*/ ClassName (const ClassName&) = delete; \ From dcf88cb6ba116cd051dd521e6c249f0ca258d983 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sat, 7 Oct 2023 03:18:38 +0200 Subject: [PATCH 03/50] ASE: loop: add exec_once() Signed-off-by: Tim Janik --- ase/loop.cc | 34 ++++++++++++++++++++++++++++++++++ ase/loop.hh | 2 ++ 2 files changed, 36 insertions(+) diff --git a/ase/loop.cc b/ase/loop.cc index 8cf81e98..5708b252 100644 --- a/ase/loop.cc +++ b/ase/loop.cc @@ -259,6 +259,40 @@ EventLoop::remove (uint id) warning ("%s: failed to remove loop source: %u", __func__, id); } +bool +EventLoop::exec_once (uint delay_ms, uint *once_id, const VoidSlot &vfunc, int priority) +{ + assert_return (once_id != nullptr, false); + assert_return (priority >= 1 && priority <= PRIORITY_CEILING, false); + if (!vfunc) { + clear_source (once_id); + return false; + } + auto once_handler = [vfunc,once_id]() { *once_id = 0; vfunc(); }; + EventSourceP source = TimedSource::create (once_handler, delay_ms, 0); + source->loop_ = this; + source->id_ = alloc_id(); + source->loop_state_ = WAITING; + source->priority_ = priority; + uint warn_id = 0; + { + std::lock_guard locker (main_loop_->mutex()); + if (*once_id) { + EventSourceP &source = find_source_L (*once_id); + if (source) + remove_source_Lm (source); + else + warn_id = *once_id; + } + sources_.push_back (source); + *once_id = source->id_; + } + if (warn_id) + warning ("%s: failed to remove loop source: %u", __func__, once_id); + wakeup(); + return true; +} + /* void EventLoop::change_priority (EventSource *source, int priority) { * // ensure that source belongs to this * // reset all source->pfds[].idx = UINT_MAX diff --git a/ase/loop.hh b/ase/loop.hh index c5d2237e..01ec2254 100644 --- a/ase/loop.hh +++ b/ase/loop.hh @@ -113,6 +113,8 @@ public: = PRIORITY_NORMAL); /// Execute a single dispatcher callback for prepare, check, dispatch. uint exec_usignal (int8 signum, const USignalSlot &sl, int priority = PRIORITY_NOW -1); /// Execute a signal callback for prepare, check, dispatch. + bool exec_once (uint delay_ms, uint *once_id, const VoidSlot &vfunc, int priority + = PRIORITY_NORMAL); ///< Execute a callback once, re-schedules the callback if `0 != *once_id`. /// Execute a callback after a specified timeout with adjustable initial timeout, returning true repeats callback. template uint exec_timer (BoolVoidFunctor &&bvf, uint delay_ms, int64 repeat_ms = -1, int priority = PRIORITY_NORMAL); From 73f177a3a1ca77a6b5a32251dbf5010a715ac541 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sun, 24 Sep 2023 00:39:41 +0200 Subject: [PATCH 04/50] ASE: memory.hh: declare CStringS Signed-off-by: Tim Janik --- ase/memory.hh | 1 + 1 file changed, 1 insertion(+) diff --git a/ase/memory.hh b/ase/memory.hh index 47625fa7..e8b3aa12 100644 --- a/ase/memory.hh +++ b/ase/memory.hh @@ -193,6 +193,7 @@ public: friend std::ostream& operator<< (std::ostream &os, CString c) { os << c.string(); return os; } static constexpr const std::string::size_type npos = -1; }; +using CStringS = std::vector; } // Ase From eefa58f24bad5ce81f9df15dd61febfad1cde447 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Mon, 2 Oct 2023 16:27:20 +0200 Subject: [PATCH 05/50] ASE: defs.hh: add F32EPS and F32MAX Signed-off-by: Tim Janik --- ase/defs.hh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ase/defs.hh b/ase/defs.hh index 6d457c49..56fa493a 100644 --- a/ase/defs.hh +++ b/ase/defs.hh @@ -14,6 +14,8 @@ static constexpr uint32_t U32MAX = +4294967295u; // 2^32-1 static constexpr int32_t I31MAX = +2147483647; // 2^31-1 static constexpr int32_t I31MIN = -2147483648; // -2^31 static constexpr float M23MAX = 16777216; // 2^(1+23); IEEE-754 Float Mantissa maximum +static constexpr float F32EPS = 5.9604644775390625e-08; // 2^-24, round-off error at 1.0 +static constexpr float F32MAX = 3.40282347e+38; // 7f7fffff, 2^128 * (1 - F32EPS) static constexpr double M52MAX = 9007199254740992; // 2^(1+52); IEEE-754 Double Mantissa maximum static constexpr double D64MAX = 1.7976931348623157e+308; // 0x7fefffff ffffffff, IEEE-754 Double Maximum static constexpr int64_t AUDIO_BLOCK_MAX_RENDER_SIZE = 2048; From 1cfe736e25cb9fbd7e890adee092177cca23bc9e Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Tue, 3 Oct 2023 00:20:10 +0200 Subject: [PATCH 06/50] ASE: levenshtein: compute (un)restricted Damerau-Levenshtein string distances Signed-off-by: Tim Janik --- ase/levenshtein.cc | 223 +++++++++++++++++++++++++++++++++++++++++++++ ase/levenshtein.hh | 25 +++++ 2 files changed, 248 insertions(+) create mode 100644 ase/levenshtein.cc create mode 100644 ase/levenshtein.hh diff --git a/ase/levenshtein.cc b/ase/levenshtein.cc new file mode 100644 index 00000000..459bb89d --- /dev/null +++ b/ase/levenshtein.cc @@ -0,0 +1,223 @@ +// This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 +#include "levenshtein.hh" +#include "internal.hh" +#include +#include + +// https://dl.acm.org/doi/10.1145/1963190.1963191 — Indexing methods for approximate dictionary searching: Comparative analysis: ACM Journal of Experimental Algorithmics: Vol 16 + +namespace Ase { + +/** Damerau-Levenshtein Distance with restricted transposition. + * Calculate the restricted Damerau-Levenshtein string distance with quadratic time complexity and + * linear memory requirement. Memory: 12 * max(|source|,|target|) + constant. + */ +float +damerau_levenshtein_restricted (const std::string &source, const std::string &target, + const float ci, const float cd, const float cs, const float ct) +{ + // Compute optimal string alignment distance or restricted edit distance: + // https://en.wikipedia.org/wiki/Damerau-Levenshtein_distance + auto min3 = [] (float a, float b, float c) { return std::min (a, std::min (b, c)); }; + ssize_t n = source.size(), m = target.size(); + // increase cache locality + if (m > n) + return damerau_levenshtein_restricted (target, source, cd, ci, cs, ct); + const char *a = source.data(), *b = target.data(); + // remove common prefix + while (n && m && *a == *b) + a++, b++, n--, m--; + // remove common suffix + while (n && m && a[n-1] == b[m-1]) + n--, m--; + // handle zero length strings + if (!n) + return m * ci; + if (!m) + return n * cd; + // allocate 3 rows: row2=row[i-2], row1=row[i-1], row0=row[i] + std::vector rowmem (3 * (m+1)); + float *row2 = rowmem.data(), *row1 = row2 + m+1, *row0 = row1 + m+1; + // the last row corresponds to source + insertions -> target + for (ssize_t j = 0; j <= m; j++) + row1[j] = j * ci; + // fill the matrix, note that a and b are 1-indexed + for (ssize_t i = 1; i <= n; i++) { + row0[0] = i * cd; // i && !j corresponds to source + deletion -> target + for (ssize_t j = 1; j <= m; j++) { + const bool ue = a[i-1] != b[j-1]; + row0[j] = min3 (row1[j] + cd, // deletion + row0[j-1] + ci, // insertion + row1[j-1] + cs * ue); // substitution + if (ue && i > 1 && j > 1 && a[i-1] == b[j-2] && a[i-2] == b[j-1]) + row0[j] = std::min (row0[j], row2[j-2] + ct); + } + // shift rows along + float *const next = row2; + row2 = row1; + row1 = row0; + row0 = next; + } + return row1[m]; +} + +template +struct L2DMatrix { + std::vector v; + const size_t sa, sb; + L2DMatrix (size_t a, size_t b, T init = {}) : + sa (a), sb (b) + { + v.resize (sa * sb, init); + } + T& + operator() (size_t a, size_t b) + { + static T tmp{}; + assert_return (a < sa, tmp); + assert_return (b < sb, tmp); + return v[a * sb + b]; + } +}; + +// Damerau-Levenshtein Distance with edited transpositions, quadratic memory requirement +static float +damerau_levenshtein_unrestricted (const std::string_view &source, const std::string_view &target, + const float ci, const float cd, const float cs, const float ct) +{ + auto u = [] (char c) { return uint8_t (c); }; + const size_t lp = source.size(), ls = target.size(); + const char *p = source.data()-1, *s = target.data()-1; // strings p,s are 1-indexed + // Computation of the unrestricted Damerau-Levenshtein distance between strings p and s + L2DMatrix C (lp+1, ls+1); // C: [0..|p|,0..|s|] + const int S = 256; // |Σ| for unsigned char + std::vector CP (S, 0); // CP: [1..|Σ|]; we use 0…255 however + for (int i = 0; i <= lp; i++) + C(i,0) = i * ci; + for (int j = 0; j <= ls; j++) + C(0,j) = j * cd; + // CP[0…255] is 0-initialized; // CP[1…|Σ|]:=0 + for (int i = 1; i <= lp; i++) { + int CS = 0; + for (int j = 1; j <= ls; j++) { + const float d = p[i] == s[j] ? 0 : cs; // if p[i]=s[j] then d:=0 else d:=1 + C(i,j) = std::min (C(i-1,j) + ci, // insertion cost + C(i,j-1) + cd); // deletion cost + C(i,j) = std::min (C(i,j), + C(i-1,j-1) + d); // substitution cost + const int i_ = CP[u(s[j])]; // CP[c] stores the largest index i_ 0 and j_ > 0) + C(i,j) = std::min (C(i,j), C(i_-1,j_-1) + (i-i_)-1 + (j-j_)-1 + ct); + if (p[i] == s[j]) + CS = j; + } + CP[u(p[i])] = i; + } + return C(lp,ls); +} + +/** Damerau-Levenshtein Distance with unrestricted transpositions. + * Calculate the unrestricted Damerau-Levenshtein string distance with quadratic time complexity and + * quadratic memory requirement. Memory: 4 * |source|*|target| + constant. + */ +float +damerau_levenshtein_distance (const std::string &source, const std::string &target, + const float ci, const float cd, const float cs, const float ct) +{ + size_t n = source.size(), m = target.size(); + const char *a = source.data(), *b = target.data(); + // remove common prefix + while (n && m && *a == *b) + a++, b++, n--, m--; + // remove common suffix + while (n && m && a[n-1] == b[m-1]) + n--, m--; + // handle zero length strings + if (!n) + return m; + if (!m) + return n; + // calc Damerau-Levenshtein Distance on differing fragments only + if (m <= n) + return damerau_levenshtein_unrestricted ({a, n}, {b, m}, ci, cd, cs, ct); + else // swap strings to increase cache locality + return damerau_levenshtein_unrestricted ({b, m}, {a, n}, cd, ci, cs, ct); +} + +} // Ase + + +#include "testing.hh" + +namespace { // Anon +using namespace Ase; + +TEST_INTEGRITY (levenshtein_tests); +static void +levenshtein_tests() +{ + // damerau_levenshtein_restricted - no editing of transposed character pairs + TCMP (0, ==, damerau_levenshtein_restricted ("", "")); + TCMP (0, ==, damerau_levenshtein_restricted ("A", "A")); + TCMP (0, ==, damerau_levenshtein_restricted ("AZ", "AZ")); + TCMP (1, ==, damerau_levenshtein_restricted ("", "1")); + TCMP (2, ==, damerau_levenshtein_restricted ("", "12")); + TCMP (3, ==, damerau_levenshtein_restricted ("", "123")); + TCMP (1, ==, damerau_levenshtein_restricted ("1", "")); + TCMP (2, ==, damerau_levenshtein_restricted ("12", "")); + TCMP (3, ==, damerau_levenshtein_restricted ("123", "")); + TCMP (1, ==, damerau_levenshtein_restricted ("A", "B")); + TCMP (1, ==, damerau_levenshtein_restricted ("AB", "BA")); + TCMP (3, ==, damerau_levenshtein_restricted ("ABC", "CA")); // restricted edit distance: CA -> A -> AB -> ABC + TCMP (3, ==, damerau_levenshtein_restricted ("123", "abc")); + TCMP (5, ==, damerau_levenshtein_restricted ("12345", "abc")); + TCMP (4, ==, damerau_levenshtein_restricted ("123", "abcd")); + TCMP (1, ==, damerau_levenshtein_restricted ("AaaaB", "AaaaC")); + TCMP (2, ==, damerau_levenshtein_restricted ("Aa_aB", "AaaaC")); + TCMP (2, ==, damerau_levenshtein_restricted ("aAaaB", "AaaaC")); + TCMP (3, ==, damerau_levenshtein_restricted ("___Ab#-##^^^", "___bA##+#^^^")); + TCMP (1, ==, damerau_levenshtein_restricted ("_ABC", "ABC")); + TCMP (1, ==, damerau_levenshtein_restricted ("ABCD", "BCD")); + TCMP (3, ==, damerau_levenshtein_restricted ("BADCFE", "ABCDEF")); + TCMP (2, ==, damerau_levenshtein_restricted ("AAAArzxyAzxy", "AArzxyAzxy")); + TCMP (3, ==, damerau_levenshtein_restricted ("ab+cd+ef", "ba+dc+fe")); + TCMP (5, ==, damerau_levenshtein_restricted ("ab+cd+ef", "ba_dc_fe")); + TCMP (3, ==, damerau_levenshtein_restricted ("kitten", "sitting")); + TCMP (4, ==, damerau_levenshtein_restricted ("AGTACGCA", "TATGC")); // -A -G C2T -A + TCMP (2, ==, damerau_levenshtein_restricted ("a cat", "an act")); + TCMP (4, ==, damerau_levenshtein_restricted ("a cat", "an abct")); // +n -c +b +c + + // damerau_levenshtein_distance - allows insert/delete between transposed character pair + TCMP (0, ==, damerau_levenshtein_distance ("", "")); + TCMP (0, ==, damerau_levenshtein_distance ("A", "A")); + TCMP (0, ==, damerau_levenshtein_distance ("AZ", "AZ")); + TCMP (1, ==, damerau_levenshtein_distance ("", "1")); + TCMP (2, ==, damerau_levenshtein_distance ("", "12")); + TCMP (3, ==, damerau_levenshtein_distance ("", "123")); + TCMP (1, ==, damerau_levenshtein_distance ("1", "")); + TCMP (2, ==, damerau_levenshtein_distance ("12", "")); + TCMP (3, ==, damerau_levenshtein_distance ("123", "")); + TCMP (1, ==, damerau_levenshtein_distance ("A", "B")); + TCMP (1, ==, damerau_levenshtein_distance ("AB", "BA")); + TCMP (2, ==, damerau_levenshtein_distance ("ABC", "CA")); // edits in adjacent transpositions: CA -> AC -> ABC + TCMP (3, ==, damerau_levenshtein_distance ("123", "abc")); + TCMP (5, ==, damerau_levenshtein_distance ("12345", "abc")); + TCMP (4, ==, damerau_levenshtein_distance ("123", "abcd")); + TCMP (1, ==, damerau_levenshtein_distance ("AaaaB", "AaaaC")); + TCMP (2, ==, damerau_levenshtein_distance ("Aa_aB", "AaaaC")); + TCMP (2, ==, damerau_levenshtein_distance ("aAaaB", "AaaaC")); + TCMP (3, ==, damerau_levenshtein_distance ("___Ab#-##^^^", "___bA##+#^^^")); + TCMP (1, ==, damerau_levenshtein_distance ("_ABC", "ABC")); + TCMP (1, ==, damerau_levenshtein_distance ("ABCD", "BCD")); + TCMP (3, ==, damerau_levenshtein_distance ("BADCFE", "ABCDEF")); + TCMP (2, ==, damerau_levenshtein_distance ("AAAArzxyAzxy", "AArzxyAzxy")); + TCMP (3, ==, damerau_levenshtein_distance ("ab+cd+ef", "ba+dc+fe")); + TCMP (5, ==, damerau_levenshtein_distance ("ab+cd+ef", "ba_dc_fe")); + TCMP (3, ==, damerau_levenshtein_distance ("kitten", "sitting")); + TCMP (4, ==, damerau_levenshtein_distance ("AGTACGCA", "TATGC")); // -A -G C2T -A + TCMP (2, ==, damerau_levenshtein_distance ("a cat", "an act")); + TCMP (3, ==, damerau_levenshtein_distance ("a cat", "an abct")); // +n ca2ac +b +} + +} // Anon diff --git a/ase/levenshtein.hh b/ase/levenshtein.hh new file mode 100644 index 00000000..4f2f75f0 --- /dev/null +++ b/ase/levenshtein.hh @@ -0,0 +1,25 @@ +// This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 +#ifndef __ASE_LEVENSHTEIN_HH__ +#define __ASE_LEVENSHTEIN_HH__ + +#include "cxxaux.hh" + +namespace Ase { + +// Damerau-Levenshtein Distance with restricted transposition, memory requirement: 12 * max(|source|,|target|) + constant +float damerau_levenshtein_restricted (const std::string &source, const std::string &target, + const float ci = 1, // insertion + const float cd = 1, // deletion + const float cs = 1, // substitution + const float ct = 1); // transposition + +// Damerau-Levenshtein Distance with unrestricted transpositions, memory requirement: 4 * |source|*|target| + constant +float damerau_levenshtein_distance (const std::string &source, const std::string &target, + const float ci = 1, // insertion + const float cd = 1, // deletion + const float cs = 1, // substitution + const float ct = 1); // transposition + +} // Ase + +#endif // __ASE_LEVENSHTEIN_HH__ From d6b0747b882bbd96d8697d15d3b10e0ceb161dba Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Thu, 5 Oct 2023 03:28:34 +0200 Subject: [PATCH 07/50] ASE: unicode: add string_to_ncname() and string_is_ncname() Signed-off-by: Tim Janik --- ase/unicode.cc | 67 +++++++++++++++++++++++++++++++++++++++++++++++--- ase/unicode.hh | 2 ++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/ase/unicode.cc b/ase/unicode.cc index 4a22e8a1..a8557d1c 100644 --- a/ase/unicode.cc +++ b/ase/unicode.cc @@ -179,6 +179,64 @@ string_from_unicode (const std::vector &codepoints) return string_from_unicode (codepoints.data(), codepoints.size()); } +/** Check `c` to be a NameStartChar, according to the QName EBNF. + * See https://en.wikipedia.org/wiki/QName + */ +static bool +codepoint_is_namestartchar (uint32_t c) +{ + const bool ok = + std::isalpha (c) || c == '_' || + (c >= 0xC0 && c <= 0xD6) || (c >= 0xD8 && c <= 0xF6) || + (c >= 0xF8 && c <= 0x2FF) || (c >= 0x370 && c <= 0x37D) || (c >= 0x37F && c <= 0x1FFF) || + (c >= 0x200C && c <= 0x200D) || (c >= 0x2070 && c <= 0x218F) || (c >= 0x2C00 && c <= 0x2FEF) || + (c >= 0x3001 && c <= 0xD7FF) || (c >= 0xF900 && c <= 0xFDCF) || (c >= 0xFDF0 && c <= 0xFFFD) || + (c >= 0x10000 && c <= 0xEFFFF); + return ok; +} + +/** Check `c` to be a NameChar, according to the QName EBNF. + * See https://en.wikipedia.org/wiki/QName + */ +static bool +codepoint_is_ncname (uint32_t c) +{ + const bool ok = + codepoint_is_namestartchar (c) || + c == '-' || c == '.' || (c >= '0' && c <= '9') || + c == 0xB7 || (c >= 0x0300 && c <= 0x036F) || (c >= 0x203F && c <= 0x2040); + return ok; +} + +bool +string_is_ncname (const String &input) +{ + std::vector tmp; + utf8_to_unicode (input, tmp); + for (auto c : tmp) + if (!codepoint_is_ncname (c)) + return false; + return true; +} + +String +string_to_ncname (const String &input, uint32_t substitute) +{ + std::vector ucstring; + utf8_to_unicode (input, ucstring); + for (auto it = ucstring.begin(); it != ucstring.end(); /**/) + if (!codepoint_is_ncname (*it)) { + if (substitute) + *it++ = substitute; + else + it = ucstring.erase (it); + } else + ++it; + if (!ucstring.empty() && !codepoint_is_namestartchar (ucstring[0])) + ucstring.insert (ucstring.begin(), '_'); + return string_from_unicode (ucstring); +} + } // Ase // == Testing == @@ -188,10 +246,9 @@ string_from_unicode (const std::vector &codepoints) namespace { // Anon using namespace Ase; -TEST_INTEGRITY (ase_test_utf8_funcs); - +TEST_INTEGRITY (unicode_tests); static void -ase_test_utf8_funcs() +unicode_tests() { Blob b = Blob::from_file ("/etc/mailcap"); const std::string str = b.string(); @@ -227,6 +284,10 @@ ase_test_utf8_funcs() for (size_t i = 0; i < codepoints.size(); ++i) TASSERT (tmp[i] == codepoints[i]); } + TCMP (false, ==, string_is_ncname ("0abc@def^foo")); + TCMP ("_0abcdeffoo", ==, string_to_ncname ("0abc@def^foo")); + TCMP ("abc_def_foo", ==, string_to_ncname ("abc@def^foo", '_')); + TCMP (true, ==, string_is_ncname ("_0abc_def_foo")); } } // Anon diff --git a/ase/unicode.hh b/ase/unicode.hh index 1fcf4ad2..40d58af9 100644 --- a/ase/unicode.hh +++ b/ase/unicode.hh @@ -8,6 +8,8 @@ namespace Ase { std::string string_from_unicode (const std::vector &codepoints); std::string string_from_unicode (const uint32_t *codepoints, size_t n_codepoints); +String string_to_ncname (const String &input, uint32_t substitute = 0); +bool string_is_ncname (const String &input); size_t utf8_to_unicode (const std::string &str, std::vector &codepoints); size_t utf8_to_unicode (const char *str, uint32_t *codepoints); size_t utf8len (const std::string &str); From 708e016b418b0ca9729c7c3331018441225fa170 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sat, 14 Oct 2023 21:51:00 +0200 Subject: [PATCH 08/50] ASE: unicode.cc: add missing docs Signed-off-by: Tim Janik --- ase/unicode.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ase/unicode.cc b/ase/unicode.cc index a8557d1c..bd273990 100644 --- a/ase/unicode.cc +++ b/ase/unicode.cc @@ -208,6 +208,9 @@ codepoint_is_ncname (uint32_t c) return ok; } +/** Check `input` to be a NCName, according to the QName EBNF. + * See https://en.wikipedia.org/wiki/QName + */ bool string_is_ncname (const String &input) { @@ -219,6 +222,9 @@ string_is_ncname (const String &input) return true; } +/** Convert `input` to a NCName, according to the QName EBNF. + * See https://en.wikipedia.org/wiki/QName + */ String string_to_ncname (const String &input, uint32_t substitute) { From 93580e4fe51ecc27e69f114e2a8fabaeada034b8 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Thu, 5 Oct 2023 03:35:26 +0200 Subject: [PATCH 09/50] ASE: strings: define and use ASE_STRING_SET_ASCII_ALNUM and _LOWER_ALNUM Signed-off-by: Tim Janik --- ase/strings.cc | 4 ++-- ase/strings.hh | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ase/strings.cc b/ase/strings.cc index be48da1a..442d8981 100644 --- a/ase/strings.cc +++ b/ase/strings.cc @@ -46,7 +46,7 @@ string_multiply (const String &s, String string_to_identifier (const String &input) { - static const String validset = string_set_a2z() + "_0123456789"; + static const String validset = ASE_STRING_SET_LOWER_ALNUM "_"; String ident = string_tolower (input); ident = string_canonify (ident, validset, "_"); if (!ident.empty() && ident[0] <= '9') @@ -110,7 +110,7 @@ string_set_A2Z () const String& string_set_ascii_alnum () { - static const String cached_alnum = "0123456789" + string_set_A2Z() + string_set_a2z(); + static const String cached_alnum = ASE_STRING_SET_ASCII_ALNUM; return cached_alnum; } diff --git a/ase/strings.hh b/ase/strings.hh index 422ba627..87b3bd88 100644 --- a/ase/strings.hh +++ b/ase/strings.hh @@ -9,6 +9,9 @@ namespace Ase { typedef std::string String; +#define ASE_STRING_SET_ASCII_ALNUM "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" +#define ASE_STRING_SET_LOWER_ALNUM "0123456789" "abcdefghijklmnopqrstuvwxyz" + // == C-String == bool cstring_to_bool (const char *string, bool fallback = false); const char* strrstr (const char *haystack, const char *needle); From f229158425551a6c574ff54130d47d24092a5801 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sun, 15 Oct 2023 17:13:55 +0200 Subject: [PATCH 10/50] ASE: strings: add kvpairs_search() Signed-off-by: Tim Janik --- ase/strings.cc | 16 +++++++++++++++- ase/strings.hh | 5 +++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/ase/strings.cc b/ase/strings.cc index 442d8981..1778590a 100644 --- a/ase/strings.cc +++ b/ase/strings.cc @@ -1227,7 +1227,7 @@ cstrings_to_vector (const char *s, ...) return sv; } -// == Generic Key-Value-Pairs == +// == Key=Value Pairs == String kvpair_key (const String &key_value_pair) { @@ -1242,6 +1242,20 @@ kvpair_value (const String &key_value_pair) return eq ? key_value_pair.substr (eq - key_value_pair.c_str() + 1) : ""; } +ssize_t +kvpairs_search (const StringS &kvs, const String &k, const bool casesensitive) +{ + const size_t l = k.size(); + for (size_t i = 0; i < kvs.size(); i++) + if (kvs[i].size() > l && kvs[i][l] == '=') { + if (casesensitive && strncmp (kvs[i].data(), k.data(), l) == 0) + return i; + if (casesensitive && strncasecmp (kvs[i].data(), k.data(), l) == 0) + return i; + } + return -1; +} + // === String Options === #define is_sep(c) (c == ';' || c == ':') #define is_spacesep(c) (isspace (c) || is_sep (c)) diff --git a/ase/strings.hh b/ase/strings.hh index 87b3bd88..22eb81bc 100644 --- a/ase/strings.hh +++ b/ase/strings.hh @@ -149,8 +149,9 @@ void string_options_split (const String &option_string, String string_option_find (const String &config, const String &feature, const String &fallback = "0"); // == Generic Key-Value-Pairs == -String kvpair_key (const String &key_value_pair); -String kvpair_value (const String &key_value_pair); +String kvpair_key (const String &key_value_pair); +String kvpair_value (const String &key_value_pair); +ssize_t kvpairs_search (const StringS &kvs, const String &k, bool casesensitive = true); // == Strings == /// Convenience Constructor for StringSeq or std::vector From 6af9e9643cec8ac8bbe4e19155d7d0ee98ff9c6f Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Mon, 2 Oct 2023 16:35:36 +0200 Subject: [PATCH 11/50] ASE: defs.hh: declare Preference Signed-off-by: Tim Janik --- ase/defs.hh | 1 + 1 file changed, 1 insertion(+) diff --git a/ase/defs.hh b/ase/defs.hh index 56fa493a..6bbcaff0 100644 --- a/ase/defs.hh +++ b/ase/defs.hh @@ -56,6 +56,7 @@ ASE_CLASS_DECLS (Monitor); ASE_CLASS_DECLS (NativeDevice); ASE_CLASS_DECLS (NativeDeviceImpl); ASE_CLASS_DECLS (Object); +ASE_CLASS_DECLS (Preference); ASE_CLASS_DECLS (Project); ASE_CLASS_DECLS (ProjectImpl); ASE_CLASS_DECLS (Property); From 9e8365da3d01f4e7fe06b8306805b3b363351383 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Tue, 3 Oct 2023 04:17:53 +0200 Subject: [PATCH 12/50] ASE: object.cc: allow NCName as event detail, to support preference identifiers Signed-off-by: Tim Janik --- ase/object.cc | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/ase/object.cc b/ase/object.cc index cbfabc2e..9ea73bee 100644 --- a/ase/object.cc +++ b/ase/object.cc @@ -2,6 +2,7 @@ #include "object.hh" #include "internal.hh" #include "utils.hh" +#include "unicode.hh" #include "main.hh" namespace Ase { @@ -189,22 +190,20 @@ EmittableImpl::on_event (const String &eventselector, const EventHandler &eventh void EmittableImpl::emit_event (const String &type, const String &detail, const ValueR fields) { - const char ident_chars[] = - "0123456789" - "abcdefghijklmnopqrstuvwxyz" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +#ifndef NDEBUG + const char eventtype_chars[] = ASE_STRING_SET_ASCII_ALNUM "_"; for (size_t i = 0; type[i]; i++) - if (!strchr (ident_chars, type[i])) - { - warning ("invalid characters in Event type: %s", type); - break; - } + if (!strchr (eventtype_chars, type[i])) { + warning ("invalid characters in Event type: %s", type); + break; + } for (size_t i = 0; detail[i]; i++) - if (!strchr (ident_chars, detail[i]) and detail[i] != '_') - { + if (!strchr (eventtype_chars, detail[i])) { + if (!string_is_ncname (detail)) warning ("invalid characters in Event detail: %s:%s", type, detail); - break; - } + break; + } +#endif // NDEBUG return_unless (ed_!= nullptr); Event ev { type, detail }; for (auto &&e : fields) From 9508dfb27ddbdb7ffdda777a5f0b7395b1357117 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Thu, 5 Oct 2023 16:00:15 +0200 Subject: [PATCH 13/50] ASE: value.hh: add Value::is_string() Signed-off-by: Tim Janik --- ase/value.hh | 1 + 1 file changed, 1 insertion(+) diff --git a/ase/value.hh b/ase/value.hh index c086ec95..288b0819 100644 --- a/ase/value.hh +++ b/ase/value.hh @@ -66,6 +66,7 @@ struct Value : ValueVariant { bool has (const String &key) const; void filter (const std::function &pred); bool is_numeric (bool boolisnumeric = true) const; + bool is_string () const { return index() == Type::STRING; } void operator= (bool v) { ValueVariant::operator= (v); } void operator= (int64 v) { ValueVariant::operator= (v); } void operator= (int32 v) { ValueVariant::operator= (int64 (v)); } From f92cbf15f83845485737a20fbd9fb6da56240760 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Fri, 22 Sep 2023 23:21:17 +0200 Subject: [PATCH 14/50] ASE: parameter: add Parameter, ParameterProperty and parameter_guess_nick() * Implement Parameter{} with Param{} as initializer * Add constrain, normalize and text conversions * Implement simple ParameterProperty abstract base class * Support callback function to query parameter choices * In set_value: constrain Value according to parameter range * Use variants for flexible initilizers * Treat choice parameters as text * Match choices via normalized Damerau-Levenshtein distance * Add parameter_guess_nick() (former on property_guess_nick). Signed-off-by: Tim Janik --- ase/parameter.cc | 489 +++++++++++++++++++++++++++++++++++++++++++++++ ase/parameter.hh | 116 +++++++++++ 2 files changed, 605 insertions(+) create mode 100644 ase/parameter.cc create mode 100644 ase/parameter.hh diff --git a/ase/parameter.cc b/ase/parameter.cc new file mode 100644 index 00000000..58595171 --- /dev/null +++ b/ase/parameter.cc @@ -0,0 +1,489 @@ +// This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 +#include "parameter.hh" +#include "levenshtein.hh" +#include "unicode.hh" +#include "regex.hh" +#include "internal.hh" + +namespace Ase { + +// == Param == +Param::ExtraVals::ExtraVals (const MinMaxStep &range) +{ + using Base = std::variant; + Base::operator= (range); +} +Param::ExtraVals::ExtraVals (const std::initializer_list &choices) +{ + *this = ChoiceS (choices); +} + +Param::ExtraVals::ExtraVals (const ChoiceS &choices) +{ + using Base = std::variant; + Base::operator= (choices); +} + +Param::ExtraVals::ExtraVals (const ChoicesFunc &choicesfunc) +{ + using Base = std::variant; + Base::operator= (choicesfunc); +} + +// == Parameter == +static String +construct_hints (const String &hints, const String &more, double pmin, double pmax) +{ + String h = hints; + if (h.empty()) + h = STANDARD; + if (h[0] != ':') + h = ":" + h; + if (h.back() != ':') + h = h + ":"; + if (pmax > 0 && pmax == -pmin && "" == string_option_find (h, "bidir", "")) + h += "bidir:"; + if (pmin != pmax && "" == string_option_find (h, "range", "")) + h += "range:"; + for (const auto &s : string_split (more, ":")) + if (!s.empty() && "" == string_option_find (h, s, "")) + h += s + ":"; + if (h.back() != ':') + h = h + ":"; + return h; +} + +String +Parameter::nick () const +{ + String nick = fetch ("nick"); + if (nick.empty()) + nick = parameter_guess_nick (label()); + return nick; +} + +bool +Parameter::has (const String &key) const +{ + return key == "ident" || kvpairs_search (details_, key) >= 0; +} + +String +Parameter::fetch (const String &key) const +{ + return_unless (key != "ident", cident); + const ssize_t i = kvpairs_search (details_, key); + return i >= 0 ? details_[i].data() + key.size() + 1 : ""; +} + +void +Parameter::store (const String &key, const String &value) +{ + assert_return (key.size() > 0); + if (key == "ident") { + cident = value; + return; + } + const ssize_t i = kvpairs_search (details_, key); + const std::string kv = key + '=' + value; + if (i >= 0) + details_[i] = kv; + else + details_.push_back (kv); +} + +MinMaxStep +Parameter::range () const +{ + if (const auto mms = std::get_if (&extras_)) + return *mms; + const ChoiceS cs = choices(); + if (cs.size()) + return { 0, cs.size() -1, 1 }; + return { NAN, NAN, NAN }; +} + +ChoiceS +Parameter::choices () const +{ + if (const auto cs = std::get_if (&extras_)) + return *cs; + if (const auto cf = std::get_if (&extras_)) + return (*cf) (cident); + return {}; +} + +bool +Parameter::is_numeric () const +{ + auto [i, a, s] = range(); + return i != a; +} + +void +Parameter::initialsync (const Value &v) +{ + initial_ = v; +} + +bool +Parameter::has_hint (const String &hint) const +{ + const String hints_ = hints(); + size_t pos = 0; + while ((pos = hints_.find (hint, pos)) != std::string::npos) { + if ((pos == 0 || hints_[pos-1] == ':') && (pos + hint.size() == hints_.size() || hints_[pos + hint.size()] == ':')) + return true; + pos += hint.size(); + } + return false; +} + +double +Parameter::normalize (double val) const +{ + const auto [fmin, fmax, step] = range(); + if (std::abs (fmax - fmin) < F32EPS) + return 0; + const double normalized = (val - fmin) / (fmax - fmin); + assert_return (normalized >= 0.0 && normalized <= 1.0, normalized); + return normalized; +} + +double +Parameter::rescale (double t) const +{ + const auto [fmin, fmax, step] = range(); + const double value = fmin + t * (fmax - fmin); + assert_return (t >= 0.0 && t <= 1.0, value); + return value; +} + +Value +Parameter::constrain (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 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; + } + } + } + return choices.size() ? choices[selected].ident : initial_; + } + // text + if (is_text()) + return value.as_string(); + // 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); + val = fmin + t * step; + val = std::min (val, fmax); + } + return val; +} + +static MinMaxStep +minmaxstep_from_initialval (const Param::InitialVal &iv) +{ + MinMaxStep range; + std::visit ([&range] (auto &&arg) + { + using T = std::decay_t; + if constexpr (std::is_same_v) + range = { 0, 1, 1 }; + else if constexpr (std::is_same_v) + range = { -128, 127, 1 }; + else if constexpr (std::is_same_v) + range = { 0, 255, 1 }; + else if constexpr (std::is_same_v) + range = { -32768, 32767, 1 }; + else if constexpr (std::is_same_v) + range = { 0, 65536, 1 }; + else if constexpr (std::is_same_v) + range = { -2147483648, 2147483647, 1 }; + else if constexpr (std::is_same_v) + range = { 0, 4294967295, 1 }; + else if constexpr (std::is_same_v) + range = { -9223372036854775807-1, 9223372036854775807, 1 }; + else if constexpr (std::is_same_v) + range = { 0, 18446744073709551615ull, 1 }; + else if constexpr (std::is_same_v) + range = { -F32MAX, F32MAX, 0 }; + else if constexpr (std::is_same_v) + range = { -D64MAX, D64MAX, 0 }; + else if constexpr (std::is_same_v) + range = { 0, 0, 0 }; // strings have no numeric range + else if constexpr (std::is_same_v) + range = { 0, 0, 0 }; // strings have no numeric range + else + static_assert (sizeof (T) < 0, "unimplemented InitialVal type"); + }, iv); + return range; +} + +static Value +value_from_initialval (const Param::InitialVal &iv) +{ + Value value; + std::visit ([&value] (auto &&arg) + { + using T = std::decay_t; + if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v) + value = int64_t (arg); + else if constexpr (std::is_same_v) + value = int64_t (arg); + else if constexpr (std::is_same_v || + std::is_same_v) + value = arg; + else if constexpr (std::is_same_v) + value = arg; + else if constexpr (std::is_same_v) + value = arg; + else + static_assert (sizeof (T) < 0, "unimplemented InitialVal type"); + }, iv); + return value; +} + +Parameter::Parameter (const Param &initparam) +{ + const Param &p = initparam; + cident = !p.ident.empty() ? string_to_ncname (p.ident) : string_to_ncname (p.label, '_'); + details_ = p.details; + const auto choicesfuncp = std::get_if (&p.extras); + MinMaxStep range; + if (const auto rangep = std::get_if (&p.extras)) + range = *rangep; + const auto [fmin, fmax, step] = range; + if (!p.label.empty()) + store ("label", p.label); + if (!p.nick.empty()) + store ("nick", p.nick); + if (!p.unit.empty()) + store ("unit", p.unit); + if (!p.blurb.empty()) + store ("blurb", p.blurb); + if (!p.descr.empty()) + store ("descr", p.descr); + if (!p.group.empty()) + store ("group", p.group); + const auto choicesp = std::get_if (&p.extras); + if (choicesfuncp) + extras_ = *choicesfuncp; + else if (choicesp) + extras_ = *choicesp; + else if (fmin != fmax) + extras_ = range; + else + extras_ = minmaxstep_from_initialval (p.initial); + 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 +Parameter::value_to_text (const Value &value) const +{ + if (is_choice()) + return constrain (value).as_string(); + const Value::Type type = value.index(); + if (type != Value::BOOL && type != Value::INT64 && type != Value::DOUBLE) + return value.as_string(); + double val = value.as_double(); + String unit = this->unit(); + if (unit == "Hz" && fabs (val) >= 1000) + { + unit = "kHz"; + val /= 1000; + } + int fdigits = 2; // fractional digits + if (fabs (val) < 10) + fdigits = 2; + else if (fabs (val) < 100) + fdigits = 1; + else if (fabs (val) < 1000) + fdigits = 0; + else + fdigits = 0; + const auto [fmin, fmax, step] = range(); + const bool need_sign = fmin < 0; + String s = need_sign ? string_format ("%+.*f", fdigits, val) : string_format ("%.*f", fdigits, val); + if (fdigits == 0 && fabs (val) == 100 && unit == "%") + s += "."; // use '100. %' for fixed with of percent numbers + if (!unit.empty()) + s += " " + unit; + return s; +} + +Value +Parameter::value_from_text (const String &text) const +{ + if (is_choice() || is_text()) + return constrain (text).as_string(); + else + return constrain (string_to_double (text)); +} + +// == guess_nick == +using String3 = std::tuple; + +// Fast version of Re::search (R"(\d)") +static ssize_t +search_first_digit (const String &s) +{ + for (size_t i = 0; i < s.size(); ++i) + if (isdigit (s[i])) + return i; + return -1; +} + +// Fast version of Re::search (R"(\d\d?\b)") +static ssize_t +search_last_digits (const String &s) +{ + for (size_t i = 0; i < s.size(); ++i) + if (isdigit (s[i])) { + if (isdigit (s[i+1]) && !isalnum (s[i+2])) + return i; + else if (!isalnum (s[i+1])) + return i; + } + return -1; +} + +// Exract up to 3 useful letters or words from label +static String3 +make_nick3 (const String &label) +{ + // split words + const StringS words = Re::findall (R"(\b\w+)", label); // TODO: allow Re caching + + // single word nick, give precedence to digits + if (words.size() == 1) { + const ssize_t d = search_first_digit (words[0]); + if (d > 0 && isdigit (words[0][d + 1])) // A11 + return { words[0].substr (0, 1), words[0].substr (d, 2), "" }; + if (d > 0) // Aa1 + return { words[0].substr (0, 2), words[0].substr (d, 1), "" }; + else // Aaa + return { words[0].substr (0, 3), "", "" }; + } + + // two word nick, give precedence to second word digits + if (words.size() == 2) { + const ssize_t e = search_last_digits (words[1]); + if (e >= 0 && isdigit (words[1][e+1])) // A22 + return { words[0].substr (0, 1), words[1].substr (e, 2), "" }; + if (e > 0) // AB2 + return { words[0].substr (0, 1), words[1].substr (0, 1), words[1].substr (e, 1) }; + if (e == 0) // Aa2 + return { words[0].substr (0, 2), words[1].substr (e, 1), "" }; + const ssize_t d = search_first_digit (words[0]); + if (d > 0) // A1B + return { words[0].substr (0, 1), words[0].substr (d, 1), words[1].substr (0, 1) }; + if (words[1].size() > 1) // ABb + return { words[0].substr (0, 1), words[1].substr (0, 2), "" }; + else // AaB + return { words[0].substr (0, 2), words[1].substr (0, 1), "" }; + } + + // 3+ word nick + if (words.size() >= 3) { + ssize_t i, e = -1; // digit pos in last possible word + for (i = words.size() - 1; i > 1; i--) { + e = search_last_digits (words[i]); + if (e >= 0) + break; + } + if (e >= 0 && isdigit (words[i][e + 1])) // A77 + return { words[0].substr (0, 1), words[i].substr (e, 2), "" }; + if (e >= 0 && i + 1 < words.size()) // A7G + return { words[0].substr (0, 1), words[i].substr (e, 1), words[i+1].substr (0, 1) }; + if (e > 0) // AF7 + return { words[0].substr (0, 1), words[i].substr (0, 1), words[i].substr (e, 1) }; + if (e == 0 && i >= 3) // AE7 + return { words[0].substr (0, 1), words[i-1].substr (0, 1), words[i].substr (e, 1) }; + if (e == 0 && i >= 2) // AB7 + return { words[0].substr (0, 1), words[1].substr (0, 1), words[i].substr (e, 1) }; + if (e == 0) // Aa7 + return { words[0].substr (0, 2), words[i].substr (e, 1), "" }; + if (words.back().size() >= 2) // AFf + return { words[0].substr (0, 1), words.back().substr (0, 2), "" }; + else // AEF + return { words[0].substr (0, 1), words[words.size()-1].substr (0, 1), words.back().substr (0, 1) }; + } + + // pathological name + return { words[0].substr (0, 3), "", "" }; +} + +// Re::sub (R"(([a-zA-Z])([0-9]))", "$1 $2", s) +static String +spaced_nums (String s) +{ + for (ssize_t i = s.size() - 1; i > 0; i--) + if (isdigit (s[i]) && !isdigit (s[i-1]) && !isspace (s[i-1])) + s.insert (s.begin() + i, ' '); + return s; +} + +/// Create a few letter nick name from a multi word parameter label. +String +parameter_guess_nick (const String ¶meter_label) +{ + // seperate numbers from words, increases word count + String string = spaced_nums (parameter_label); + + // use various letter extractions to construct nick portions + const auto& [a, b, c] = make_nick3 (string); + + // combine from right to left to increase word variance + String nick; + if (c.size() > 0) + nick = a.substr (0, 1) + b.substr (0, 1) + c.substr (0, 1); + else if (b.size() > 0) + nick = a.substr (0, 1) + b.substr (0, 2); + else + nick = a.substr (0, 3); + return nick; +} + +} // Ase diff --git a/ase/parameter.hh b/ase/parameter.hh new file mode 100644 index 00000000..e19201e9 --- /dev/null +++ b/ase/parameter.hh @@ -0,0 +1,116 @@ +// This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 +#ifndef __ASE_PARAMETER_HH__ +#define __ASE_PARAMETER_HH__ + +#include +#include // EmittableImpl +#include + +namespace Ase { + +/// Min, max, stepping for double ranges. +using MinMaxStep = std::tuple; +using ChoicesFunc = std::function; + +/// Structured initializer for Parameter +struct Param { + using InitialVal = std::variant; + struct ExtraVals : std::variant { + ExtraVals () = default; + ExtraVals (const MinMaxStep&); + ExtraVals (const std::initializer_list&); + ExtraVals (const ChoiceS&); + ExtraVals (const ChoicesFunc&); + }; + String label; ///< Preferred user interface name. + String nick; ///< Abbreviated user interface name, usually not more than 6 characters. + InitialVal initial = 0; ///< Initial value for float, int, choice types. + String unit; ///< Units of the values within range. + ExtraVals extras; ///< Min, max, stepping for double ranges or array of choices to select from. + String hints; ///< Hints for parameter handling. + String blurb; ///< Short description for user interface tooltips. + String descr; ///< Elaborate description for help dialogs. + String group; ///< Group for parameters of similar function. + String ident; ///< Identifier used for serialization (can be derived from untranslated label). + StringS details; ///< Array of "key=value" pairs. + static inline const String STORAGE = ":r:w:S:"; + static inline const String STANDARD = ":r:w:S:G:"; +}; + +/// Structure to provide information about properties or preferences. +struct Parameter { + CString cident; + bool has (const String &key) const; + String fetch (const String &key) const; + void store (const String &key, const String &value); + String ident () const { return cident; } + String label () const { return fetch ("label"); } + String nick () const; + String unit () const { return fetch ("unit"); } + String hints () const { return fetch ("hints"); } + String blurb () const { return fetch ("blurb"); } + String descr () const { return fetch ("descr"); } + String group () const { return fetch ("group"); } + Value initial () const { return initial_; } + bool has_hint (const String &hint) const; + ChoiceS choices () const; + MinMaxStep range () const; ///< Min, max, stepping for double ranges. + bool is_numeric () const; + bool is_choice () const { return has_hint ("choice"); } + bool is_text () const { return has_hint ("text"); } + double normalize (double val) const; + double rescale (double t) const; + Value constrain (const Value &value) const; + void initialsync (const Value &v); + /*ctor*/ Parameter () = default; + /*ctor*/ Parameter (const Param&); + /*copy*/ Parameter (const Parameter&) = default; + Parameter& operator= (const Parameter&) = default; + // helpers + String value_to_text (const Value &value) const; + Value value_from_text (const String &text) const; +private: + using ExtrasV = std::variant; + StringS details_; + ExtrasV extras_; + Value initial_ = 0; +}; +using ParameterC = std::shared_ptr; + +/// Abstract base type for Property implementations with Parameter meta data. +class ParameterProperty : public EmittableImpl, public virtual Property { +protected: + ParameterC parameter_; +public: + String identifier () override { return parameter_->cident; } + String label () override { return parameter_->label(); } + String nick () override { return parameter_->nick(); } + String unit () override { return parameter_->unit(); } + String hints () override { return parameter_->hints(); } + String blurb () override { return parameter_->blurb(); } + String descr () override { return parameter_->descr(); } + String group () override { return parameter_->group(); } + double get_min () override { return std::get<0> (parameter_->range()); } + double get_max () override { return std::get<1> (parameter_->range()); } + double get_step () override { return std::get<2> (parameter_->range()); } + bool is_numeric () override { return parameter_->is_numeric(); } + ChoiceS choices () override { return parameter_->choices(); } + void reset () override { set_value (parameter_->initial()); } + double get_normalized () override { return !is_numeric() ? 0 : parameter_->normalize (get_double()); } + bool set_normalized (double v) override { return is_numeric() && set_value (parameter_->rescale (v)); } + String get_text () override { return parameter_->value_to_text (get_value()); } + bool set_text (String txt) override { set_value (parameter_->value_from_text (txt)); return !txt.empty(); } + Value get_value () override = 0; + bool set_value (const Value &v) override = 0; + double get_double () { return !is_numeric() ? 0 : get_value().as_double(); } + ParameterC parameter () const { return parameter_; } + Value initial () const { return parameter_->initial(); } + MinMaxStep range () const { return parameter_->range(); } +}; + +/// Find a suitable 3-letter abbreviation for a Parameter without nick. +String parameter_guess_nick (const String ¶meter_label); + +} // Ase + +#endif // __ASE_PARAMETER_HH__ From 4c17216eeb5b9da6c60a10d1aa45ca5fb98e84b5 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sat, 23 Sep 2023 02:29:52 +0200 Subject: [PATCH 15/50] ASE: processor: use Parameter instead of the old ParamInfo Signed-off-by: Tim Janik --- ase/defs.hh | 2 +- ase/nativedevice.cc | 6 +- ase/processor.cc | 438 ++++++++++++-------------------------------- ase/processor.hh | 81 ++------ 4 files changed, 135 insertions(+), 392 deletions(-) diff --git a/ase/defs.hh b/ase/defs.hh index 6bbcaff0..9cffbfb1 100644 --- a/ase/defs.hh +++ b/ase/defs.hh @@ -30,7 +30,7 @@ ASE_STRUCT_DECLS (ClapParamUpdate); ASE_STRUCT_DECLS (ClipNote); ASE_STRUCT_DECLS (DeviceInfo); ASE_STRUCT_DECLS (DriverEntry); -ASE_STRUCT_DECLS (ParamInfo); +ASE_STRUCT_DECLS (Parameter); ASE_STRUCT_DECLS (Resource); ASE_STRUCT_DECLS (TelemetryField); ASE_STRUCT_DECLS (TelemetrySegment); diff --git a/ase/nativedevice.cc b/ase/nativedevice.cc index b01b298b..ae78eb25 100644 --- a/ase/nativedevice.cc +++ b/ase/nativedevice.cc @@ -49,10 +49,10 @@ PropertyS NativeDeviceImpl::access_properties () { std::vector pparams; - pparams.reserve (proc_->params_.size()); - for (const AudioProcessor::PParam &p : proc_->params_) + pparams.reserve (proc_->pparams_.size()); + for (const AudioProcessor::PParam &p : proc_->pparams_) pparams.push_back (&p); - std::sort (pparams.begin(), pparams.end(), [] (auto a, auto b) { return a->info->order < b->info->order; }); + std::sort (pparams.begin(), pparams.end(), [] (auto a, auto b) { return a->order() < b->order(); }); PropertyS pseq; pseq.reserve (pparams.size()); for (const AudioProcessor::PParam *p : pparams) diff --git a/ase/processor.cc b/ase/processor.cc index bb927380..1ef66043 100644 --- a/ase/processor.cc +++ b/ase/processor.cc @@ -12,168 +12,6 @@ namespace Ase { -// == ParamInfo == -static constexpr uint PTAG_FLOATS = 1; -static constexpr uint PTAG_CENTRIES = 2; - -ParamInfo::ParamInfo (ParamId pid, uint porder) : - order (porder), union_tag (0) -{ - memset (&u, 0, sizeof (u)); -} - -ParamInfo::~ParamInfo() -{ - release(); -} - -void -ParamInfo::copy_fields (const ParamInfo &src) -{ - ident = src.ident; - label = src.label; - nick = src.nick; - unit = src.unit; - hints = src.hints; - group = src.group; - blurb = src.blurb; - description = src.description; - switch (src.union_tag) - { - case PTAG_FLOATS: - set_range (src.u.fmin, src.u.fmax, src.u.fstep); - break; - case PTAG_CENTRIES: - set_choices (*src.u.centries()); - break; - } -} - -void -ParamInfo::release() -{ - const bool destroy = union_tag == PTAG_CENTRIES; - union_tag = 0; - if (destroy) - u.centries()->~ChoiceS(); -} - -/// Clear all ParamInfo fields. -void -ParamInfo::clear () -{ - ident = ""; - label = ""; - nick = ""; - unit = ""; - hints = ""; - group = ""; - blurb = ""; - description = ""; - release(); -} - -/// Get parameter stepping or 0 if not quantized. -double -ParamInfo::get_stepping () const -{ - switch (union_tag) - { - case PTAG_FLOATS: - return u.fstep; - case PTAG_CENTRIES: - return 1; - default: - return 0; - } -} - -/// Get initial parameter value. -double -ParamInfo::get_initial () const -{ - return initial_; -} - -/// Get parameter range minimum and maximum. -ParamInfo::MinMax -ParamInfo::get_minmax () const -{ - switch (union_tag) - { - case PTAG_FLOATS: - return { u.fmin, u.fmax }; - case PTAG_CENTRIES: - return { 0, u.centries()->size() - 1 }; - default: - return { NAN, NAN }; - } -} - -/// Get parameter range properties. -void -ParamInfo::get_range (double &fmin, double &fmax, double &fstep) const -{ - switch (union_tag) - { - case PTAG_FLOATS: - fmin = u.fmin; - fmax = u.fmax; - fstep = u.fstep; - break; - case PTAG_CENTRIES: - { - auto mm = get_minmax (); - fmin = mm.first; - fmax = mm.second; - } - fstep = 1; - break; - default: - fmin = NAN; - fmax = NAN; - fstep = NAN; - break; - } -} - -/// Assign range properties to parameter. -void -ParamInfo::set_range (double fmin, double fmax, double fstep) -{ - release(); - union_tag = PTAG_FLOATS; - u.fmin = fmin; - u.fmax = fmax; - u.fstep = fstep; -} - -/// Get parameter choice list. -const ChoiceS& -ParamInfo::get_choices () const -{ - if (union_tag == PTAG_CENTRIES) - return *u.centries(); - static const ChoiceS empty; - return empty; -} - -/// Assign choice list to parameter via vector move. -void -ParamInfo::set_choices (ChoiceS &¢ries) -{ - release(); - union_tag = PTAG_CENTRIES; - new (u.centries()) ChoiceS (std::move (centries)); -} - -/// Assign choice list to parameter via deep copy. -void -ParamInfo::set_choices (const ChoiceS ¢ries) -{ - set_choices (ChoiceS (centries)); -} - // == PBus == AudioProcessor::PBus::PBus (const String &ident, const String &uilabel, SpeakerArrangement sa) : ibus (ident, uilabel, sa) @@ -339,36 +177,37 @@ construct_hints (String hints, double pmin, double pmax, const String &more = "" ParamId AudioProcessor::nextid () const { - const uint pmax = params_.size(); - const uint last = params_.empty() ? 0 : uint (params_.back().id); + const uint pmax = pparams_.size(); + const uint last = pparams_.empty() ? 0 : uint (pparams_.back().id); return ParamId (MAX (pmax, last) + 1); } /// Add a new parameter with unique `ParamInfo.identifier`. /// The returned `ParamId` is forced to match `id` (and must be unique). ParamId -AudioProcessor::add_param (Id32 id, const ParamInfo &infotmpl, double value) +AudioProcessor::add_param (Id32 id, const Param &initparam, double value) { assert_return (uint (id) > 0, ParamId (0)); assert_return (!is_initialized(), {}); - assert_return (infotmpl.label != "", {}); - if (params_.size()) - assert_return (infotmpl.label != params_.back().info->label, {}); // easy CnP error - PParam param { ParamId (id.id), uint (1 + params_.size()), infotmpl }; - if (param.info->ident == "") - param.info->ident = string_to_identifier (param.info->label); - if (params_.size()) - assert_return (param.info->ident != params_.back().info->ident, {}); // easy CnP error - if (param.info->group.empty()) - param.info->group = tls_param_group; - using P = decltype (params_); + assert_return (initparam.label != "", {}); + if (pparams_.size()) + assert_return (initparam.label != pparams_.back().parameter->label(), {}); // easy CnP error + Param iparam = initparam; + if (iparam.group.empty()) + iparam.group = tls_param_group; + ParameterP parameterp = std::make_shared (iparam); + ParameterC parameterc = parameterp; + if (pparams_.size()) + assert_return (parameterc->ident() != pparams_.back().parameter->ident(), {}); // easy CnP error + PParam pparam { ParamId (id.id), uint (1 + pparams_.size()), parameterc }; + using P = decltype (pparams_); std::pair existing_parameter_position = - Aux::binary_lookup_insertion_pos (params_.begin(), params_.end(), PParam::cmp, param); + Aux::binary_lookup_insertion_pos (pparams_.begin(), pparams_.end(), PParam::cmp, pparam); assert_return (existing_parameter_position.second == false, {}); - params_.insert (existing_parameter_position.first, std::move (param)); - set_param (param.id, value); // forces dirty - param.info->initial_ = peek_param_mt (param.id); - return param.id; + pparams_.insert (existing_parameter_position.first, std::move (pparam)); + set_param (pparam.id, value); // forces dirty + parameterp->initialsync (peek_param_mt (pparam.id)); + return pparam.id; } /// Add new range parameter with most `ParamInfo` fields as inlined arguments. @@ -382,16 +221,16 @@ AudioProcessor::add_param (Id32 id, const String &clabel, const String &nickname const String &blurb, const String &description) { assert_return (uint (id) > 0, ParamId (0)); - ParamInfo info; - info.ident = string_to_identifier (clabel); - info.label = clabel; - info.nick = nickname; - info.hints = construct_hints (hints, pmin, pmax); - info.unit = unit; - info.blurb = blurb; - info.description = description; - info.set_range (pmin, pmax); - return add_param (id, info, value); + return add_param (id, Param { + .label = clabel, + .nick = nickname, + .initial = value, + .unit = unit, + .extras = MinMaxStep { pmin, pmax, 0 }, + .hints = construct_hints (hints, pmin, pmax), + .blurb = blurb, + .descr = description, + }, value); } /// Variant of AudioProcessor::add_param() with sequential `id` generation. @@ -414,16 +253,16 @@ AudioProcessor::add_param (Id32 id, const String &clabel, const String &nickname const String &blurb, const String &description) { assert_return (uint (id) > 0, ParamId (0)); - ParamInfo info; - info.ident = string_to_identifier (clabel); - info.label = clabel; - info.nick = nickname; - info.blurb = blurb; - info.description = description; const double pmax = centries.size(); - info.set_choices (std::move (centries)); - info.hints = construct_hints (hints, 0, pmax, "choice"); - return add_param (id, info, value); + return add_param (id, Param { + .label = clabel, + .nick = nickname, + .initial = value, + .extras = centries, + .hints = construct_hints (hints, 0, pmax, "choice"), + .blurb = blurb, + .descr = description, + }, value); } /// Variant of AudioProcessor::add_param() with sequential `id` generation. @@ -445,21 +284,18 @@ AudioProcessor::add_param (Id32 id, const String &clabel, const String &nickname const String &blurb, const String &description) { assert_return (uint (id) > 0, ParamId (0)); - ParamInfo info; - info.ident = string_to_identifier (clabel); - info.label = clabel; - info.nick = nickname; - info.blurb = blurb; - info.description = description; using namespace MakeIcon; static const ChoiceS centries { { "on"_icon, "Off" }, { "off"_icon, "On" } }; - info.set_choices (centries); - info.hints = construct_hints (hints, false, true, "toggle"); - const auto rid = add_param (id, info, boolvalue); - assert_return (uint (rid) == id.id, rid); - const PParam *param = find_pparam (rid); - assert_return (param && param->peek() == (boolvalue ? 1.0 : 0.0), rid); - return rid; + 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"), + .blurb = blurb, + .descr = description, + }, value); } /// Variant of AudioProcessor::add_param() with sequential `id` generation. @@ -477,9 +313,9 @@ AudioProcessor::find_param (const String &identifier) const -> MaybeParamId { auto ident = CString::lookup (identifier); if (!ident.empty()) - for (const PParam &p : params_) - if (p.info->ident == ident) - return std::make_pair (p.id, true); + for (const PParam &pp : pparams_) + if (pp.parameter->cident == ident) + return std::make_pair (pp.id, true); return std::make_pair (ParamId (0), false); } @@ -489,8 +325,8 @@ AudioProcessor::find_pparam_ (ParamId paramid) const { // lookup id with gaps const PParam key { paramid }; - auto iter = Aux::binary_lookup (params_.begin(), params_.end(), PParam::cmp, key); - const bool found_paramid = iter != params_.end(); + auto iter = Aux::binary_lookup (pparams_.begin(), pparams_.end(), PParam::cmp, key); + const bool found_paramid = iter != pparams_.end(); if (ISLIKELY (found_paramid)) return &*iter; assert_return (found_paramid == true, nullptr); @@ -503,35 +339,34 @@ AudioProcessor::set_param (Id32 paramid, const double value, bool sendnotify) { const PParam *pparam = find_pparam (ParamId (paramid.id)); return_unless (pparam, false); - const ParamInfo *info = pparam->info.get(); + ParameterC parameter = pparam->parameter; double v = value; - if (info) + if (parameter) { - const auto mm = info->get_minmax(); - v = CLAMP (v, mm.first, mm.second); - const double stepping = info->get_stepping(); + 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 - mm.first) / stepping + nearintoffset); - v = CLAMP (mm.first + v, mm.first, mm.second); + v = stepping * uint64_t ((v - fmin) / stepping + nearintoffset); + v = CLAMP (fmin + v, fmin, fmax); } } const bool need_notify = const_cast (pparam)->assign (v); - if (need_notify && sendnotify && !pparam->info->aprop_.expired()) + if (need_notify && sendnotify && !pparam->aprop_.expired()) enotify_enqueue_mt (PARAMCHANGE); return need_notify; } /// Retrieve supplemental information for parameters, usually to enhance the user interface. -ParamInfoP -AudioProcessor::param_info (Id32 paramid) const +ParameterC +AudioProcessor::parameter (Id32 paramid) const { - const PParam *param = this->find_pparam (ParamId (paramid.id)); - return ASE_ISLIKELY (param) ? param->info : nullptr; + const PParam *pparam = this->find_pparam (ParamId (paramid.id)); + return ASE_ISLIKELY (pparam) ? pparam->parameter : nullptr; } /// Fetch the current parameter value of a AudioProcessor. @@ -557,10 +392,10 @@ AudioProcessor::param_peek_mt (const AudioProcessorP proc, Id32 paramid) double AudioProcessor::value_to_normalized (Id32 paramid, double value) const { - const PParam *const param = find_pparam (paramid); - assert_return (param != nullptr, 0); - const auto mm = param->info->get_minmax(); - const double normalized = (value - mm.first) / (mm.second - mm.first); + const PParam *const pparam = find_pparam (paramid); + assert_return (pparam != nullptr, 0); + const auto [fmin, fmax, step] = pparam->parameter->range(); + const double normalized = (value - fmin) / (fmax - fmin); assert_return (normalized >= 0.0 && normalized <= 1.0, CLAMP (normalized, 0.0, 1.0)); return normalized; } @@ -568,10 +403,10 @@ AudioProcessor::value_to_normalized (Id32 paramid, double value) const double AudioProcessor::value_from_normalized (Id32 paramid, double normalized) const { - const PParam *const param = find_pparam (paramid); - assert_return (param != nullptr, 0); - const auto mm = param->info->get_minmax(); - const double value = mm.first + normalized * (mm.second - mm.first); + const PParam *const pparam = find_pparam (paramid); + assert_return (pparam != nullptr, 0); + const auto [fmin, fmax, step] = pparam->parameter->range(); + const double value = fmin + normalized * (fmax - fmin); assert_return (normalized >= 0.0 && normalized <= 1.0, value); return value; } @@ -603,46 +438,14 @@ String AudioProcessor::param_value_to_text (Id32 paramid, double value) const { const PParam *pparam = find_pparam (ParamId (paramid.id)); - if (!pparam || !pparam->info) - return ""; - const ParamInfo &info = *pparam->info; - const bool ischoice = strstr (info.hints.c_str(), ":choice:") != nullptr; - if (ischoice) - { - const ChoiceS &choices = info.get_choices(); - const size_t idx = value; - if (idx < choices.size()) - return choices[idx].ident; - } - String unit = pparam->info->unit; - if (unit == "Hz" && fabs (value) >= 1000) - { - unit = "kHz"; - value /= 1000; - } - int fdigits = 2; - if (fabs (value) < 10) - fdigits = 2; - else if (fabs (value) < 100) - fdigits = 1; - else if (fabs (value) < 1000) - fdigits = 0; - else - fdigits = 0; - const bool need_sign = info.get_minmax().first < 0; - String s = need_sign ? string_format ("%+.*f", fdigits, value) : string_format ("%.*f", fdigits, value); - if (fdigits == 0 && fabs (value) == 100 && unit == "%") - s += "."; // use '100. %' for fixed with - if (!unit.empty()) - s += " " + unit; - return s; + return pparam && pparam->parameter ? pparam->parameter->value_to_text (value) : ""; } /** Extract a parameter `paramid` value from a text string. * The string might contain unit information or consist only of * number characters. Non-recognized characters should be ignored, * so a best effort conversion is always undertaken. - * Currently, this function may be called from any thread, + * This function may be called from any thread, * so `this` must be treated as `const` (it might be used * concurrently by a different thread). */ @@ -650,18 +453,7 @@ double AudioProcessor::param_value_from_text (Id32 paramid, const String &text) const { const PParam *pparam = find_pparam (ParamId (paramid.id)); - if (!pparam || !pparam->info) - return 0.0; - const ParamInfo &info = *pparam->info; - const bool ischoice = strstr (info.hints.c_str(), ":choice:") != nullptr; - if (ischoice) - { - ChoiceS choices = info.get_choices(); - for (size_t i = 0; i < choices.size(); i++) - if (text == choices[i].ident) - return i; - } - return string_to_double (text); + return pparam && pparam->parameter ? pparam->parameter->value_from_text (text).as_double() : 0.0; } /// Prepare `count` bits for atomic notifications. @@ -702,8 +494,10 @@ AudioProcessor::is_initialized () const AudioProcessor::MinMax AudioProcessor::param_range (Id32 paramid) const { - ParamInfo *info = param_info (paramid).get(); - return info ? info->get_minmax() : MinMax { FP_NAN, FP_NAN }; + ParameterC parameterc = parameter (paramid); + return_unless (parameterc, MinMax { FP_NAN, FP_NAN }); + const auto [fmin, fmax, step] = parameterc->range(); + return MinMax { fmin, fmax }; } String @@ -1185,10 +979,10 @@ AudioProcessor::registry_foreach (const std::function (_id, order)) +AudioProcessor::PParam::PParam (ParamId _id, uint order, ParameterC ¶meterc) : + id (_id), order_ (order), parameter (parameterc) { - info->copy_fields (pinfo); + assert_return (parameterc != nullptr); } AudioProcessor::PParam::PParam (ParamId _id) : @@ -1205,9 +999,11 @@ AudioProcessor::PParam& AudioProcessor::PParam::operator= (const PParam &src) { id = src.id; + order_ = src.order_; flags_ = src.flags_.load(); value_ = src.value_.load(); - info = src.info; + aprop_ = src.aprop_; + parameter = src.parameter; return *this; } @@ -1233,25 +1029,25 @@ AudioProcessor::FloatBuffer::check () // == AudioPropertyImpl == class AudioPropertyImpl : public Property, public virtual EmittableImpl { DeviceP device_; - ParamInfoP info_; + ParameterC parameter_; const ParamId id_; double inflight_value_ = 0; std::atomic inflight_counter_ = 0; public: - String identifier () override { return info_->ident; } - String label () override { return info_->label; } - String nick () override { return info_->nick; } - String unit () override { return info_->unit; } - String hints () override { return info_->hints; } - String group () override { return info_->group; } - String blurb () override { return info_->blurb; } - String description () override { return info_->description; } - double get_min () override { double mi, ma, st; info_->get_range (mi, ma, st); return mi; } - double get_max () override { double mi, ma, st; info_->get_range (mi, ma, st); return ma; } - double get_step () override { double mi, ma, st; info_->get_range (mi, ma, st); return st; } + String identifier () override { return parameter_->ident(); } + String label () override { return parameter_->label(); } + String nick () override { return parameter_->nick(); } + String unit () override { return parameter_->unit(); } + String hints () override { return parameter_->hints(); } + String group () override { return parameter_->group(); } + String blurb () override { return parameter_->blurb(); } + String description () override { return parameter_->descr(); } + double get_min () override { const auto [fmin, fmax, step] = parameter_->range(); return fmin; } + double get_max () override { const auto [fmin, fmax, step] = parameter_->range(); return fmax; } + double get_step () override { const auto [fmin, fmax, step] = parameter_->range(); return step; } explicit - AudioPropertyImpl (DeviceP devp, ParamId id, ParamInfoP param_) : - device_ (devp), info_ (param_), id_ (id) + AudioPropertyImpl (DeviceP devp, ParamId id, ParameterC parameter) : + device_ (devp), parameter_ (parameter), id_ (id) {} void proc_paramchange() @@ -1260,19 +1056,19 @@ class AudioPropertyImpl : public Property, public virtual EmittableImpl { const double value = inflight_counter_ ? inflight_value_ : AudioProcessor::param_peek_mt (proc, id_); ValueR vfields; vfields["value"] = value; - emit_event ("notify", info_->ident, vfields); + emit_event ("notify", parameter_->ident(), vfields); } void reset () override { - set_value (info_->get_initial()); + set_value (parameter_->initial()); } Value get_value () override { const AudioProcessorP proc = device_->_audio_processor(); const double value = inflight_counter_ ? inflight_value_ : AudioProcessor::param_peek_mt (proc, id_); - const bool ischoice = strstr (info_->hints.c_str(), ":choice:") != nullptr; + const bool ischoice = strstr (parameter_->hints().c_str(), ":choice:") != nullptr; if (ischoice) return proc->param_value_to_text (id_, value); else @@ -1283,7 +1079,7 @@ class AudioPropertyImpl : public Property, public virtual EmittableImpl { { PropertyP thisp = shared_ptr_cast (this); // thisp keeps this alive during lambda const AudioProcessorP proc = device_->_audio_processor(); - const bool ischoice = strstr (info_->hints.c_str(), ":choice:") != nullptr; + const bool ischoice = strstr (parameter_->hints().c_str(), ":choice:") != nullptr; double v; if (ischoice && value.index() == Value::STRING) v = proc->param_value_from_text (id_, value.as_string()); @@ -1297,7 +1093,7 @@ class AudioPropertyImpl : public Property, public virtual EmittableImpl { inflight_counter_--; }; proc->engine().async_jobs += lambda; - emit_notify (info_->ident); + emit_notify (parameter_->ident()); return true; } double @@ -1312,8 +1108,8 @@ class AudioPropertyImpl : public Property, public virtual EmittableImpl { set_normalized (double normalized) override { const AudioProcessorP proc = device_->_audio_processor(); - const auto mm = info_->get_minmax(); - const double value = mm.first + CLAMP (normalized, 0, 1) * (mm.second - mm.first); + const auto [fmin, fmax, step] = parameter_->range(); + const double value = fmin + CLAMP (normalized, 0, 1) * (fmax - fmin); return set_value (value); } String @@ -1333,7 +1129,7 @@ class AudioPropertyImpl : public Property, public virtual EmittableImpl { proc->set_param (pid, v, false); }; proc->engine().async_jobs += lambda; - emit_notify (info_->ident); + emit_notify (parameter_->ident()); return true; } bool @@ -1345,7 +1141,7 @@ class AudioPropertyImpl : public Property, public virtual EmittableImpl { ChoiceS choices () override { - return info_->get_choices(); + return parameter_->choices(); } }; @@ -1356,17 +1152,17 @@ PropertyP AudioProcessor::access_property (ParamId id) const { assert_return (is_initialized(), {}); - const PParam *param = find_pparam (id); - assert_return (param, {}); + const PParam *pparam = find_pparam (id); + assert_return (pparam, {}); DeviceP devp = get_device(); assert_return (devp, {}); PropertyP newptr; - PropertyP prop = weak_ptr_fetch_or_create (param->info->aprop_, [&] () { - newptr = std::make_shared (devp, param->id, param->info); + PropertyP prop = weak_ptr_fetch_or_create (const_cast (pparam)->aprop_, [&] () { + newptr = std::make_shared (devp, pparam->id, pparam->parameter); return newptr; }); if (newptr.get() == prop.get()) - const_cast (param)->changed (false); // skip initial change notification + const_cast (pparam)->changed (false); // skip initial change notification return prop; } @@ -1422,10 +1218,10 @@ AudioProcessor::enotify_dispatch () if (nflags & REMOVAL) devicep->emit_event ("sub", "remove"); if (nflags & PARAMCHANGE) - for (const PParam &p : current->params_) - if (ASE_UNLIKELY (p.changed()) && const_cast (p).changed (false)) + for (PParam &pparam : current->pparams_) + if (ASE_UNLIKELY (pparam.changed()) && pparam.changed (false)) { - PropertyP propi = p.info->aprop_.lock(); + PropertyP propi = pparam.aprop_.lock(); AudioPropertyImpl *aprop = dynamic_cast (propi.get()); if (aprop) aprop->proc_paramchange(); diff --git a/ase/processor.hh b/ase/processor.hh index aa51c00f..73647496 100644 --- a/ase/processor.hh +++ b/ase/processor.hh @@ -3,6 +3,7 @@ #define __ASE_PROCESSOR_HH__ #include +#include #include #include #include @@ -39,45 +40,6 @@ struct AudioProcessorInfo { /// Add an AudioProcessor derived type to the audio processor registry. template CString register_audio_processor (const char *aseid = nullptr); -/// Detailed information and common properties of parameters. -struct ParamInfo { - CString ident; ///< Identifier used for serialization. - CString label; ///< Preferred user interface name. - CString nick; ///< Abbreviated user interface name, usually not more than 6 characters. - CString unit; ///< Units of the values within range. - CString hints; ///< Hints for parameter handling. - CString group; ///< Group for parameters of similar function. - CString blurb; ///< Short description for user interface tooltips. - CString description; ///< Elaborate description for help dialogs. - using MinMax = std::pair; - void clear (); - MinMax get_minmax () const; - double get_stepping() const; - double get_initial () const; - void get_range (double &fmin, double &fmax, double &fstep) const; - void set_range (double fmin, double fmax, double fstep = 0); - void set_choices (const ChoiceS ¢ries); - void set_choices (ChoiceS &¢ries); - const - ChoiceS& get_choices () const; - void copy_fields (const ParamInfo &src); - /*ctor*/ ParamInfo (ParamId pid = ParamId (0), uint porder = 0); - virtual ~ParamInfo (); - const uint order; -private: - uint union_tag = 0; - union { - struct { double fmin, fmax, fstep; }; - uint64_t mem[sizeof (ChoiceS) / sizeof (uint64_t)]; - ChoiceS* centries() const { return (ChoiceS*) mem; } - } u; - double initial_ = 0; - /*copy*/ ParamInfo (const ParamInfo&) = delete; - void release (); - std::weak_ptr aprop_; - friend class AudioProcessor; -}; - /// Structure providing supplementary information about input/output buses. struct BusInfo { CString ident; ///< Identifier used for serialization. @@ -126,7 +88,7 @@ private: uint32 output_offset_ = 0; FloatBuffer *fbuffers_ = nullptr; std::vector iobuses_; - std::vector params_; // const once is_initialized() + std::vector pparams_; // const once is_initialized() std::vector outputs_; EventStreams *estreams_ = nullptr; AtomicBits *atomic_bits_ = nullptr; @@ -159,7 +121,7 @@ protected: // Parameters virtual void adjust_param (Id32 tag) {} ParamId nextid () const; - ParamId add_param (Id32 id, const ParamInfo &infotmpl, double value); + ParamId add_param (Id32 id, const Param &initparam, double value); ParamId add_param (Id32 id, const String &clabel, const String &nickname, double pmin, double pmax, double value, const String &unit = "", String hints = "", @@ -224,7 +186,7 @@ public: // Parameters double get_param (Id32 paramid); bool set_param (Id32 paramid, double value, bool sendnotify = true); - ParamInfoP param_info (Id32 paramid) const; + ParameterC parameter (Id32 paramid) const; MaybeParamId find_param (const String &identifier) const; MinMax param_range (Id32 paramid) const; bool check_dirty (Id32 paramid) const; @@ -339,9 +301,9 @@ struct AudioProcessor::EventStreams { // AudioProcessor internal parameter book keeping struct AudioProcessor::PParam { explicit PParam (ParamId id); - explicit PParam (ParamId id, uint order, const ParamInfo &pinfo); + explicit PParam (ParamId id, uint order, ParameterC ¶meter); /*copy*/ PParam (const PParam &); - PParam& operator= (const PParam &); + PParam& operator= (const PParam &); double fetch_and_clean () { dirty (false); return value_; } double peek () const { return value_; } bool dirty () const { return flags_ & DIRTY; } @@ -360,8 +322,12 @@ private: enum { DIRTY = 1, CHANGED = 2, }; std::atomic flags_ = 1; std::atomic value_ = NAN; + std::weak_ptr aprop_; + friend class AudioProcessor; + uint order_ = 0; public: - ParamInfoP info; + ParameterC parameter; + uint order() const { return order_; } bool assign (double f) { @@ -480,7 +446,7 @@ AudioProcessor::n_ochannels (OBusId busid) const inline void AudioProcessor::adjust_params (bool include_nondirty) { - for (const PParam &p : params_) + for (const PParam &p : pparams_) if (include_nondirty || p.dirty()) adjust_param (p.id); } @@ -491,8 +457,8 @@ AudioProcessor::find_pparam (Id32 paramid) const { // fast path via sequential ids const size_t idx = paramid.id - 1; - if (ASE_ISLIKELY (idx < params_.size()) && ASE_ISLIKELY (params_[idx].id == ParamId (paramid.id))) - return ¶ms_[idx]; + if (ASE_ISLIKELY (idx < pparams_.size()) && ASE_ISLIKELY (pparams_[idx].id == ParamId (paramid.id))) + return &pparams_[idx]; return find_pparam_ (ParamId (paramid.id)); } @@ -579,23 +545,4 @@ AudioProcessor::create_processor (AudioEngine &engine, const Args &...args) } // Ase -namespace std { -template<> -struct hash<::Ase::ParamInfo> { - /// Hash value for Ase::ParamInfo. - size_t - operator() (const ::Ase::ParamInfo &pi) const - { - size_t h = ::std::hash<::Ase::CString>() (pi.ident); - // h ^= ::std::hash (pi.label); - // h ^= ::std::hash (pi.nick); - // h ^= ::std::hash (pi.description); - h ^= ::std::hash<::Ase::CString>() (pi.unit); - h ^= ::std::hash<::Ase::CString>() (pi.hints); - // min, max, step - return h; - } -}; -} // std - #endif // __ASE_PROCESSOR_HH__ From 245b01db8b0e0c8330c0ac6cdc2855794e432772 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Thu, 5 Oct 2023 01:28:30 +0200 Subject: [PATCH 16/50] ASE: main.cc: save/load preferences unless --norc is given Signed-off-by: Tim Janik --- ase/main.cc | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/ase/main.cc b/ase/main.cc index 0ef18429..66563156 100644 --- a/ase/main.cc +++ b/ase/main.cc @@ -178,6 +178,7 @@ parse_args (int *argcp, char **argv) } config.fatal_warnings = feature_check ("fatal-warnings"); + bool norc = false; bool sep = false; // -- separator const uint argc = *argcp; for (uint i = 1; i < argc; i++) @@ -188,6 +189,8 @@ parse_args (int *argcp, char **argv) config.fatal_warnings = true; else if (strcmp ("--disable-randomization", argv[i]) == 0) config.allow_randomization = false; + else if (strcmp ("--norc", argv[i]) == 0) + norc = true; else if (strcmp ("--rand64", argv[i]) == 0) { FastRng prng; @@ -276,6 +279,9 @@ parse_args (int *argcp, char **argv) } *argcp = e; } + // load preferences unless --norc was given + if (!norc) + Preference::load_preferences (true); return config; } @@ -426,6 +432,8 @@ main (int argc, char *argv[]) using namespace Ase; using namespace AnsiColors; + // setup thread identifier + TaskRegistry::setup_ase ("AnklangMainProc"); // use malloc to serve allocations via sbrk only (avoid mmap) mallopt (M_MMAP_MAX, 0); // avoid releasing sbrk memory back to the system (reduce page faults) @@ -435,8 +443,16 @@ main (int argc, char *argv[]) // preallocate memory for lock-free allocator preallocate_loft (64 * 1024 * 1024); - // setup thread and handle args and config - TaskRegistry::setup_ase ("AnklangMainProc"); + // SIGPIPE init: needs to be done before any child thread is created + init_sigpipe(); + // prepare main event loop (needed before parse_args) + main_loop = MainLoop::create(); + // handle loft preallocation needs + main_loop->exec_dispatcher (dispatch_loft_lowmem, EventLoop::PRIORITY_CEILING); + // handle automatic shutdown + main_loop->exec_dispatcher (handle_autostop); + + // setup thread and handle args and config (needs main_loop) main_config_ = parse_args (&argc, argv); const MainConfig &config = main_config_; @@ -455,16 +471,6 @@ main (int argc, char *argv[]) return 0; } - // SIGPIPE init: needs to be done before any child thread is created - init_sigpipe(); - - // prepare main event loop - main_loop = MainLoop::create(); - // handle loft preallocation needs - main_loop->exec_dispatcher (dispatch_loft_lowmem, EventLoop::PRIORITY_CEILING); - // handle automatic shutdown - main_loop->exec_dispatcher (handle_autostop); - // load drivers and dump device list load_registered_drivers(); if (config.list_drivers) From 3c58525d30363c070760ccdc970208b061a20b3b Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Thu, 5 Oct 2023 15:58:06 +0200 Subject: [PATCH 17/50] ASE: api.hh: add constexpr GUIONLY, STORAGE, STANDARD to avoid C++ SIOF Signed-off-by: Tim Janik --- ase/api.hh | 5 +++-- ase/properties.cc | 3 --- ase/server.cc | 10 +++++----- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/ase/api.hh b/ase/api.hh index bdf810b9..519fd7e3 100644 --- a/ase/api.hh +++ b/ase/api.hh @@ -8,8 +8,9 @@ namespace Ase { // == Property hint constants == -extern const String STORAGE; // ":r:w:S:"; -extern const String STANDARD; // ":r:w:S:G:"; +constexpr const char GUIONLY[] = ":G:r:w:"; ///< GUI READABLE WRITABLE +constexpr const char STORAGE[] = ":S:r:w:"; ///< STORAGE READABLE WRITABLE +constexpr const char STANDARD[] = ":S:G:r:w:"; ///< STORAGE GUI READABLE WRITABLE /// Common base type for polymorphic classes managed by `std::shared_ptr<>`. class SharedBase : public virtual VirtualBase, diff --git a/ase/properties.cc b/ase/properties.cc index 27bd0a82..aa747689 100644 --- a/ase/properties.cc +++ b/ase/properties.cc @@ -8,9 +8,6 @@ namespace Ase { -const String STORAGE = ":r:w:S:"; -const String STANDARD = ":r:w:S:G:"; - static String canonify_identifier (const std::string &input) { diff --git a/ase/server.cc b/ase/server.cc index a2059625..35938f06 100644 --- a/ase/server.cc +++ b/ase/server.cc @@ -68,7 +68,7 @@ Preferences::access_properties (const EventHandler &eventhandler) return_unless (bag.props.empty(), bag.props); bag.group = _("Synthesis Settings"); bag += Text (&pcm_driver, _("PCM Driver"), "", pcm_driver_choices, STANDARD, _("Driver and device to be used for PCM input and output")); - bag += Range (&synth_latency, _("Latency"), "", 0, 3000, 5, "ms", STANDARD + "step=5", + bag += Range (&synth_latency, _("Latency"), "", 0, 3000, 5, "ms", STANDARD + String ("step=5"), _("Processing duration between input and output of a single sample, smaller values increase CPU load")); bag += Range (&synth_mixing_freq, _("Synth Mixing Frequency"), "", 48000, 48000, 48000, "Hz", STANDARD, _("Unused, synthesis mixing frequency is always 48000 Hz")); @@ -85,13 +85,13 @@ Preferences::access_properties (const EventHandler &eventhandler) bag += Text (&author_default, _("Default Author"), "", STANDARD, _("Default value for 'Author' fields")); bag += Text (&license_default, _("Default License"), "", STANDARD, _("Default value for 'License' fields")); bag.group = _("Search Paths"); - bag += Text (&sample_path, _("Sample Path"), "", STANDARD + "searchpath", + bag += Text (&sample_path, _("Sample Path"), "", STANDARD + String ("searchpath"), _("Search path of directories, seperated by \";\", used to find audio samples.")); - bag += Text (&effect_path, _("Effect Path"), "", STANDARD + "searchpath", + bag += Text (&effect_path, _("Effect Path"), "", STANDARD + String ("searchpath"), _("Search path of directories, seperated by \";\", used to find effect files.")); - bag += Text (&instrument_path, _("Instrument Path"), "", STANDARD + "searchpath", + bag += Text (&instrument_path, _("Instrument Path"), "", STANDARD + String ("searchpath"), _("Search path of directories, seperated by \";\", used to find instrument files.")); - bag += Text (&plugin_path, _("Plugin Path"), "", STANDARD + "searchpath", + bag += Text (&plugin_path, _("Plugin Path"), "", STANDARD + String ("searchpath"), _("Search path of directories, seperated by \";\", used to find plugins. This path " "is searched for in addition to the standard plugin location on this system.")); bag.on_events ("notify", eventhandler); From 36070cd914be687c7ef67675fa155c7e47e27a03 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Fri, 13 Oct 2023 12:15:33 +0200 Subject: [PATCH 18/50] ASE: api.hh, server: provide access_preference() instead of access_prefs() Signed-off-by: Tim Janik --- ase/api.hh | 5 ++--- ase/server.cc | 6 +++--- ase/server.hh | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/ase/api.hh b/ase/api.hh index 519fd7e3..53614c28 100644 --- a/ase/api.hh +++ b/ase/api.hh @@ -395,9 +395,8 @@ public: virtual uint64 user_note (const String &text, const String &channel = "misc", UserNote::Flags flags = UserNote::TRANSIENT, const String &rest = "") = 0; virtual bool user_reply (uint64 noteid, uint r) = 0; virtual bool broadcast_telemetry (const TelemetrySegmentS &segments, - int32 interval_ms) = 0; ///< Broadcast telemetry memory segments to the current Jsonipc connection. - // preferences - virtual PropertyS access_prefs () = 0; ///< Retrieve property handles for Preferences fields. + int32 interval_ms) = 0; ///< Broadcast telemetry memory segments to the current Jsonipc connection. + virtual PropertyP access_preference (const String &ident) = 0; ///< Retrieve property handle for a Preference identifier. // projects virtual ProjectP last_project () = 0; ///< Retrieve the last created project. virtual ProjectP create_project (String projectname) = 0; ///< Create a new project (name is modified to be unique if necessary. diff --git a/ase/server.cc b/ase/server.cc index 35938f06..54434ec5 100644 --- a/ase/server.cc +++ b/ase/server.cc @@ -235,10 +235,10 @@ ServerImpl::create_project (String projectname) return ProjectImpl::create (projectname); } -PropertyS -ServerImpl::access_prefs() +PropertyP +ServerImpl::access_preference (const String &ident) { - return prefs_properties_; + return Preference::find (ident); } ServerImplP diff --git a/ase/server.hh b/ase/server.hh index 90f234c1..2ad84830 100644 --- a/ase/server.hh +++ b/ase/server.hh @@ -32,7 +32,7 @@ public: void shutdown () override; ProjectP last_project () override; ProjectP create_project (String projectname) override; - PropertyS access_prefs () override; + PropertyP access_preference (const String &ident) override; const Preferences& preferences () const { return prefs_; } using Block = FastMemory::Block; Block telemem_allocate (uint32 length) const; From ce6456df72b86aea4c41c5baf089384abb7a38ff Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Fri, 13 Oct 2023 12:21:10 +0200 Subject: [PATCH 19/50] ASE: api.hh: rename Property.descr() (abbreviate) Signed-off-by: Tim Janik --- ase/api.hh | 2 +- ase/clapdevice.cc | 2 +- ase/processor.cc | 2 +- ase/properties.cc | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ase/api.hh b/ase/api.hh index 53614c28..200764c2 100644 --- a/ase/api.hh +++ b/ase/api.hh @@ -141,7 +141,7 @@ public: virtual String hints () = 0; ///< Hints for parameter handling. virtual String group () = 0; ///< Group name for parameters of similar function. virtual String blurb () = 0; ///< Short description for user interface tooltips. - virtual String description () = 0; ///< Elaborate description for help dialogs. + virtual String descr () = 0; ///< Elaborate description for help dialogs. virtual double get_min () = 0; ///< Get the minimum property value, converted to double. virtual double get_max () = 0; ///< Get the maximum property value, converted to double. virtual double get_step () = 0; ///< Get the property value stepping, converted to double. diff --git a/ase/clapdevice.cc b/ase/clapdevice.cc index 1ac72b22..617d2bd1 100644 --- a/ase/clapdevice.cc +++ b/ase/clapdevice.cc @@ -34,7 +34,7 @@ struct ClapPropertyImpl : public Property, public virtual EmittableImpl { String hints () override { return ClapParamInfo::hints_from_param_info_flags (flags); } String group () override { return module_; } String blurb () override { return ""; } - String description () override { return ""; } + String descr () override { return ""; } double get_min () override { return min_value; } double get_max () override { return max_value; } double get_step () override { return is_stepped() ? 1 : 0; } diff --git a/ase/processor.cc b/ase/processor.cc index 1ef66043..6ad01774 100644 --- a/ase/processor.cc +++ b/ase/processor.cc @@ -1041,7 +1041,7 @@ class AudioPropertyImpl : public Property, public virtual EmittableImpl { String hints () override { return parameter_->hints(); } String group () override { return parameter_->group(); } String blurb () override { return parameter_->blurb(); } - String description () override { return parameter_->descr(); } + String descr () override { return parameter_->descr(); } double get_min () override { const auto [fmin, fmax, step] = parameter_->range(); return fmin; } double get_max () override { const auto [fmin, fmax, step] = parameter_->range(); return fmax; } double get_step () override { const auto [fmin, fmax, step] = parameter_->range(); return step; } diff --git a/ase/properties.cc b/ase/properties.cc index aa747689..b15fbc12 100644 --- a/ase/properties.cc +++ b/ase/properties.cc @@ -69,7 +69,7 @@ struct LambdaPropertyImpl : public virtual PropertyImpl { String hints () override { return d.hints; } String group () override { return d.groupname; } String blurb () override { return d.blurb; } - String description () override { return d.description; } + String descr () override { return d.description; } ChoiceS choices () override { return lister_ ? lister_ (*static_cast (this)) : ChoiceS{}; } double get_min () override { return d.pmin; } double get_max () override { return d.pmax; } From e8ecf0c0ee7a83bdb8fc5193ca73f7a8b8fb2e70 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sat, 14 Oct 2023 21:57:39 +0200 Subject: [PATCH 20/50] ASE: defs.hh: add PropertyImpl Signed-off-by: Tim Janik --- ase/defs.hh | 1 + 1 file changed, 1 insertion(+) diff --git a/ase/defs.hh b/ase/defs.hh index 9cffbfb1..4ecb385e 100644 --- a/ase/defs.hh +++ b/ase/defs.hh @@ -60,6 +60,7 @@ ASE_CLASS_DECLS (Preference); ASE_CLASS_DECLS (Project); ASE_CLASS_DECLS (ProjectImpl); ASE_CLASS_DECLS (Property); +ASE_CLASS_DECLS (PropertyImpl); ASE_CLASS_DECLS (ResourceCrawler); ASE_CLASS_DECLS (Server); ASE_CLASS_DECLS (ServerImpl); From 317dafea13e2c81fb9b451bccb04a172f333508e Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sat, 14 Oct 2023 21:54:49 +0200 Subject: [PATCH 21/50] ASE: properties: add Preference classes and majorly simplify Property impls * Remove obsolete structures and aux constructors * Add getter/setter lister for enumeration types * Add PropertyBag - a helper to simplify property registrations * Add small PropertyImpl based on ParameterProperty * Add generic Preference class, useful for static initialization * Remove property nick guessing * Add global Preference setting list based on Parameter * Implement saving/loading preferences from anklangrc * Auto save preference changes after maybe half a second * Ensure that Preference names follow NCName syntax Signed-off-by: Tim Janik --- ase/properties.cc | 515 ++++++++++++++++------------------------------ ase/properties.hh | 230 +++++++++------------ 2 files changed, 271 insertions(+), 474 deletions(-) diff --git a/ase/properties.cc b/ase/properties.cc index b15fbc12..fa0e0c1c 100644 --- a/ase/properties.cc +++ b/ase/properties.cc @@ -3,399 +3,234 @@ #include "jsonipc/jsonipc.hh" #include "strings.hh" #include "utils.hh" -#include "regex.hh" +#include "main.hh" +#include "path.hh" +#include "serialize.hh" #include "internal.hh" -namespace Ase { +#define PDEBUG(...) Ase::debug ("prefs", __VA_ARGS__) -static String -canonify_identifier (const std::string &input) -{ - static const String validset = string_set_a2z() + "0123456789" + "_"; - const String lowered = string_tolower (input); - String str = string_canonify (lowered, validset, "_"); - if (str.size() && str[0] >= '0' && str[0] <= '9') - str = '_' + str; - return str; -} +namespace Ase { +// == Property == Property::~Property() {} -namespace Properties { - -String -construct_hints (const String &hints, const String &more, double pmin, double pmax) +// == PropertyImpl == +PropertyImpl::PropertyImpl (CString ident, const Param ¶m, const PropertyGetter &getter, + const PropertySetter &setter, const PropertyLister &lister) : + getter_ (getter), setter_ (setter), lister_ (lister) { - String h = hints; - if (h.empty()) - h = STANDARD; - if (h.back() != ':') - h = h + ":"; - for (const auto &s : string_split (more)) - if (!s.empty() && "" == string_option_find (h, s, "")) - h += s + ":"; - if (h[0] != ':') - h = ":" + h; - if (pmax > 0 && pmax == -pmin) - h += "bidir:"; - return h; + Param iparam = param; + iparam.ident = ident; + parameter_ = std::make_shared (iparam); } -PropertyImpl::~PropertyImpl() -{} - -static Value -call_getter (const ValueGetter &g) +// == PropertyBag == +void +PropertyBag::operator+= (const Prop &prop) const { - Value v; - g(v); - return v; + add_ (prop, group); } -// == LambdaPropertyImpl == -struct LambdaPropertyImpl : public virtual PropertyImpl { - virtual ~LambdaPropertyImpl () {} - Initializer d; - const ValueGetter getter_; - const ValueSetter setter_; - const ValueLister lister_; - const Value vdefault_; - void notify (); - String identifier () override { return d.ident; } - String label () override { return d.label; } - String nick () override { return d.nickname; } - String unit () override { return d.unit; } - String hints () override { return d.hints; } - String group () override { return d.groupname; } - String blurb () override { return d.blurb; } - String descr () override { return d.description; } - ChoiceS choices () override { return lister_ ? lister_ (*static_cast (this)) : ChoiceS{}; } - double get_min () override { return d.pmin; } - double get_max () override { return d.pmax; } - double get_step () override { return 0.0; } - bool is_numeric () override; - void reset () override; - Value get_value () override; - bool set_value (const Value &v) override; - double get_normalized () override; - bool set_normalized (double v) override; - String get_text () override; - bool set_text (String v) override; - LambdaPropertyImpl (const Properties::Initializer &initializer, const ValueGetter &g, const ValueSetter &s, const ValueLister &l) : - d (initializer), getter_ (g), setter_ (s), lister_ (l), vdefault_ (call_getter (g)) - { - d.ident = canonify_identifier (d.ident.empty() ? d.label : d.ident); - assert_return (initializer.ident.size()); - } +// == Preference == +using PrefsValueCallbackList = CallbackList; +struct PrefsValue { + ParameterC parameter; + Value value; + std::shared_ptr callbacks; }; -using LambdaPropertyImplP = std::shared_ptr; -JSONIPC_INHERIT (LambdaPropertyImpl, Property); +using PrefsMap = std::unordered_map; -void -LambdaPropertyImpl::notify() -{ - emit_notify (identifier()); -} +static std::shared_ptr> prefs_callbacks = CallbackList::make_shared(); +static CStringS notify_preference_queue; -Value -LambdaPropertyImpl::get_value () +static PrefsMap& +prefs_map() { - Value val; - getter_ (val); - return val; + static PrefsMap *const prefsmap = []() { return new PrefsMap(); } (); + return *prefsmap; } -bool -LambdaPropertyImpl::set_value (const Value &val) -{ - const bool changed = setter_ (val); - if (changed) - notify(); - return changed; -} +static bool preferences_autosave = false; +static uint timerid_maybe_save_preferences = 0; -double -LambdaPropertyImpl::get_normalized () // TODO: implement +static void +maybe_save_preferences() { - Value val; - getter_ (val); - return val.as_double(); + main_loop->clear_source (&timerid_maybe_save_preferences); + if (preferences_autosave && notify_preference_queue.empty()) + Preference::save_preferences(); } -bool -LambdaPropertyImpl::set_normalized (double v) // TODO: implement +static void +notify_preference_listeners () { - Value val { v }; - const bool changed = setter_ (val); - if (changed) - notify(); - return changed; + CStringS changed_prefs (notify_preference_queue.begin(), notify_preference_queue.end()); // converts CStringS to StringS + notify_preference_queue.clear(); // clear, queue taken over + return_unless (!changed_prefs.empty()); + std::sort (changed_prefs.begin(), changed_prefs.end()); // sort and dedup + changed_prefs.erase (std::unique (changed_prefs.begin(), changed_prefs.end()), changed_prefs.end()); + // emit "notify" on individual preferences + PrefsMap &prefsmap = prefs_map(); + for (auto cident : changed_prefs) { + PrefsValue &pv = prefsmap[cident]; + (*pv.callbacks) (cident, pv.value); + } + // notify preference list listeners + const auto callbacklist = prefs_callbacks; // keep reference around invocation + (*callbacklist) (changed_prefs); + if (preferences_autosave) + main_loop->exec_once (577, &timerid_maybe_save_preferences, maybe_save_preferences); + else + main_loop->clear_source (&timerid_maybe_save_preferences); +} + +static void +queue_notify_preference_listeners (const CString &cident) +{ + return_unless (main_loop != nullptr); + // enqueue idle handler for accumulated preference change notifications + const bool need_enqueue = notify_preference_queue.empty(); + notify_preference_queue.push_back (cident); + if (need_enqueue) + main_loop->exec_now (notify_preference_listeners); +} + +Preference::Preference (ParameterC parameter) +{ + parameter_ = parameter; + PrefsMap &prefsmap = prefs_map(); // Preference must already be registered + auto it = prefsmap.find (parameter_->cident); + assert_return (it != prefsmap.end()); + PrefsValue &pv = it->second; + sigh_ = pv.callbacks->add_delcb ([this] (const String &ident, const Value &value) { emit_event ("notify", ident); }); +} + +Preference::Preference (const CString &ident, const Param ¶m, const StringValueF &cb) +{ + Param iparam = param; + iparam.ident = ident; + parameter_ = std::make_shared (iparam); + PrefsMap &prefsmap = prefs_map(); + PrefsValue &pv = prefsmap[parameter_->cident]; + assert_return (pv.parameter == nullptr); // catch duplicate registration + pv.parameter = parameter_; + pv.value = pv.parameter->initial(); + pv.callbacks = PrefsValueCallbackList::make_shared(); + sigh_ = pv.callbacks->add_delcb ([this] (const String &ident, const Value &value) { emit_event ("notify", ident); }); + queue_notify_preference_listeners (parameter_->cident); + if (cb) { + Connection connection = on_event ("notify", [this,cb] (const Event &event) { cb (this->parameter_->cident, this->get_value()); }); + connection_ = new Connection (connection); + } } -String -LambdaPropertyImpl::get_text () +Preference::~Preference() { - Value val; - getter_ (val); - return val.as_string(); + if (connection_) { + connection_->disconnect(); + delete connection_; + connection_ = nullptr; + } + if (sigh_) + sigh_(); // delete signal handler callback } -bool -LambdaPropertyImpl::set_text (String v) +Value +Preference::get_value () { - Value val { v }; - const bool changed = setter_ (val); - if (changed) - notify(); - return changed; + PrefsMap &prefsmap = prefs_map(); + PrefsValue &pv = prefsmap[parameter_->cident]; + return pv.value; } bool -LambdaPropertyImpl::is_numeric () -{ - Value val; - getter_ (val); - switch (val.index()) - { - case Value::BOOL: return true; - case Value::INT64: return true; - case Value::DOUBLE: return true; - case Value::ARRAY: - case Value::RECORD: - case Value::STRING: - case Value::INSTANCE: - case Value::NONE: ; // std::monostate - } - return false; -} - -void -LambdaPropertyImpl::reset () -{ - setter_ (vdefault_); - notify(); -} - -PropertyImplP -mkprop (const Initializer &initializer, const ValueGetter &getter, const ValueSetter &setter, const ValueLister &lister) -{ - return std::make_shared (initializer, getter, setter, lister); -} - -// == Bag == -Bag& -Bag::operator+= (PropertyP p) -{ - if (!group.empty() && p->group().empty()) - { - LambdaPropertyImpl *simple = dynamic_cast (p.get()); - if (simple) - simple->d.groupname = group; - } - props.push_back (p); - return *this; -} - -void -Bag::on_events (const String &eventselector, const EventHandler &eventhandler) -{ - for (auto p : props) - connections.push_back (p->on_event (eventselector, eventhandler)); -} - -} // Properties - -template static PropertyP -ptrprop (const Properties::Initializer &initializer, V *p, const Properties::ValueLister &lister = {}) -{ - using namespace Properties; - assert_return (p, nullptr); - return mkprop (initializer, Getter (p), Setter (p), lister); -} - -// == Property constructors == -PropertyP -Properties::Bool (const String &ident, bool *v, const String &label, const String &nickname, bool dflt, const String &hints, const String &blurb, const String &description) -{ - return ptrprop ({ .ident = ident, .label = label, .nickname = nickname, .blurb = blurb, .description = description, - .hints = construct_hints (hints, "bool"), .pdef = double (dflt) }, v); -} - -PropertyP -Properties::Range (const String &ident, int32 *v, const String &label, const String &nickname, int32 pmin, int32 pmax, int32 dflt, - const String &unit, const String &hints, const String &blurb, const String &description) -{ - return ptrprop ({ .ident = ident, .label = label, .nickname = nickname, .blurb = blurb, .description = description, - .hints = construct_hints (hints, "range"), - .pmin = double (pmin), .pmax = double (pmax), .pdef = double (dflt) }, v); -} - -PropertyP -Properties::Range (const String &ident, const ValueGetter &getter, const ValueSetter &setter, const String &label, const String &nickname, double pmin, double pmax, double dflt, - const String &unit, const String &hints, const String &blurb, const String &description) -{ - const Initializer initializer = { - .ident = ident, .label = label, .nickname = nickname, .blurb = blurb, .description = description, - .hints = construct_hints (hints, "range"), .pmin = pmin, .pmax = pmax, .pdef = dflt, - }; - return mkprop (initializer, getter, setter, {}); -} - -PropertyP -Properties::Range (const String &ident, float *v, const String &label, const String &nickname, double pmin, double pmax, double dflt, - const String &unit, const String &hints, const String &blurb, const String &description) -{ - return ptrprop ({ .ident = ident, .label = label, .nickname = nickname, .blurb = blurb, .description = description, - .hints = construct_hints (hints, "range"), .pmin = pmin, .pmax = pmax, .pdef = dflt }, v); +Preference::set_value (const Value &v) +{ + PrefsMap &prefsmap = prefs_map(); + PrefsValue &pv = prefsmap[parameter_->cident]; + Value next = parameter_->constrain (v); + const bool changed = next == pv.value; + pv.value = std::move (next); + queue_notify_preference_listeners (parameter_->cident); // delayed + return changed; } -PropertyP -Properties::Range (const String &ident, double *v, const String &label, const String &nickname, double pmin, double pmax, double dflt, - const String &unit, const String &hints, const String &blurb, const String &description) +Value +Preference::get (const String &ident) { - return ptrprop ({ .ident = ident, .label = label, .nickname = nickname, .blurb = blurb, .description = description, - .hints = construct_hints (hints, "range"), .pmin = pmin, .pmax = pmax, .pdef = dflt }, v); + const CString cident = CString::lookup (ident); + return_unless (!cident.empty(), {}); + PrefsMap &prefsmap = prefs_map(); + auto it = prefsmap.find (cident); + return_unless (it != prefsmap.end(), {}); + PrefsValue &pv = it->second; + return pv.value; } -PropertyP -Properties::Text (const String &ident, String *v, const String &label, const String &nickname, const String &hints, const String &blurb, const String &description) +PreferenceP +Preference::find (const String &ident) { - return ptrprop ({ .ident = ident, .label = label, .nickname = nickname, .blurb = blurb, .description = description, - .hints = construct_hints (hints, "text") }, v); + const CString cident = CString::lookup (ident); + return_unless (!cident.empty(), {}); + PrefsMap &prefsmap = prefs_map(); + auto it = prefsmap.find (cident); + return_unless (it != prefsmap.end(), {}); + PrefsValue &pv = it->second; + return Preference::make_shared (pv.parameter); } -PropertyP -Properties::Text (const String &ident, String *v, const String &label, const String &nickname, const ValueLister &vl, const String &hints, const String &blurb, const String &description) +CStringS +Preference::list () { - PropertyP propp; - propp = ptrprop ({ .ident = ident, .label = label, .nickname = nickname, .blurb = blurb, .description = description, - .hints = construct_hints (hints, "text:choice") }, v, vl); - return propp; + CStringS strings; + for (const auto &e : prefs_map()) + strings.push_back (e.first); + std::sort (strings.begin(), strings.end()); + return strings; } -// == guess_nick == -using String3 = std::tuple; - -// Fast version of Re::search (R"(\d)") -static ssize_t -search_first_digit (const String &s) +Preference::DelCb +Preference::listen (const std::function &func) { - for (size_t i = 0; i < s.size(); ++i) - if (isdigit (s[i])) - return i; - return -1; + return prefs_callbacks->add_delcb (func); } -// Fast version of Re::search (R"(\d\d?\b)") -static ssize_t -search_last_digits (const String &s) +static String +pathname_anklangrc() { - for (size_t i = 0; i < s.size(); ++i) - if (isdigit (s[i])) { - if (isdigit (s[i+1]) && !isalnum (s[i+2])) - return i; - else if (!isalnum (s[i+1])) - return i; - } - return -1; + static const String anklangrc = Path::join (Path::config_home(), "anklang", "anklangrc.json"); + return anklangrc; } -// Exract up to 3 useful letters or words from label -static String3 -make_nick3 (const String &label) -{ - // split words - const StringS words = Re::findall (R"(\b\w+)", label); // TODO: allow Re caching - - // single word nick, give precedence to digits - if (words.size() == 1) { - const ssize_t d = search_first_digit (words[0]); - if (d > 0 && isdigit (words[0][d + 1])) // A11 - return { words[0].substr (0, 1), words[0].substr (d, 2), "" }; - if (d > 0) // Aa1 - return { words[0].substr (0, 2), words[0].substr (d, 1), "" }; - else // Aaa - return { words[0].substr (0, 3), "", "" }; - } - - // two word nick, give precedence to second word digits - if (words.size() == 2) { - const ssize_t e = search_last_digits (words[1]); - if (e >= 0 && isdigit (words[1][e+1])) // A22 - return { words[0].substr (0, 1), words[1].substr (e, 2), "" }; - if (e > 0) // AB2 - return { words[0].substr (0, 1), words[1].substr (0, 1), words[1].substr (e, 1) }; - if (e == 0) // Aa2 - return { words[0].substr (0, 2), words[1].substr (e, 1), "" }; - const ssize_t d = search_first_digit (words[0]); - if (d > 0) // A1B - return { words[0].substr (0, 1), words[0].substr (d, 1), words[1].substr (0, 1) }; - if (words[1].size() > 1) // ABb - return { words[0].substr (0, 1), words[1].substr (0, 2), "" }; - else // AaB - return { words[0].substr (0, 2), words[1].substr (0, 1), "" }; - } - - // 3+ word nick - if (words.size() >= 3) { - ssize_t i, e = -1; // digit pos in last possible word - for (i = words.size() - 1; i > 1; i--) { - e = search_last_digits (words[i]); - if (e >= 0) - break; - } - if (e >= 0 && isdigit (words[i][e + 1])) // A66 - return { words[0].substr (0, 1), words[i].substr (e, 2), "" }; - if (e >= 0 && i + 1 < words.size()) // A6G - return { words[0].substr (0, 1), words[i].substr (e, 1), words[i+1].substr (0, 1) }; - if (e > 0) // AF6 - return { words[0].substr (0, 1), words[i].substr (0, 1), words[i].substr (e, 1) }; - if (e == 0 && i >= 3) // AE6 - return { words[0].substr (0, 1), words[i-1].substr (0, 1), words[i].substr (e, 1) }; - if (e == 0 && i >= 2) // AB6 - return { words[0].substr (0, 1), words[1].substr (0, 1), words[i].substr (e, 1) }; - if (e == 0) // Aa6 - return { words[0].substr (0, 2), words[i].substr (e, 1), "" }; - if (words.back().size() >= 2) // AFf - return { words[0].substr (0, 1), words.back().substr (0, 2), "" }; - else // AEF - return { words[0].substr (0, 1), words[words.size()-1].substr (0, 1), words.back().substr (0, 1) }; +void +Preference::load_preferences (bool autosave) +{ + const String jsontext = Path::stringread (pathname_anklangrc()); + ValueR precord; + json_parse (jsontext, precord); + for (ValueField vf : precord) { + PreferenceP pref = find (vf.name); + PDEBUG ("%s: %s %s=%s\n", __func__, pref ? "loading" : "ignoring", vf.name, vf.value->repr()); + if (pref) + pref->set_value (*vf.value); } - - // pathological name - return { words[0].substr (0, 3), "", "" }; + preferences_autosave = autosave; } -// Re::sub (R"(([a-zA-Z])([0-9]))", "$1 $2", s) -static String -spaced_nums (String s) -{ - for (ssize_t i = s.size() - 1; i > 0; i--) - if (isdigit (s[i]) && !isdigit (s[i-1]) && !isspace (s[i-1])) - s.insert (s.begin() + i, ' '); - return s; -} - -/// Create a few letter nick name from a multi word property label. -String -property_guess_nick (const String &property_label) -{ - // seperate numbers from words, increases word count - String string = spaced_nums (property_label); - - // use various letter extractions to construct nick portions - const auto& [a, b, c] = make_nick3 (string); - - // combine from right to left to increase word variance - String nick; - if (c.size() > 0) - nick = a.substr (0, 1) + b.substr (0, 1) + c.substr (0, 1); - else if (b.size() > 0) - nick = a.substr (0, 1) + b.substr (0, 2); - else - nick = a.substr (0, 3); - return nick; +void +Preference::save_preferences () +{ + ValueR precord; + for (auto ident : list()) + precord[ident] = get (ident); + const String new_jsontext = json_stringify (precord, Writ::RELAXED | Writ::SKIP_EMPTYSTRING) + "\n"; + const String cur_jsontext = Path::stringread (pathname_anklangrc()); + if (new_jsontext != cur_jsontext) { + PDEBUG ("%s: %s\n", __func__, precord.repr()); + Path::stringwrite (pathname_anklangrc(), new_jsontext, true); + } } } // Ase diff --git a/ase/properties.hh b/ase/properties.hh index 128e020d..20155c39 100644 --- a/ase/properties.hh +++ b/ase/properties.hh @@ -2,85 +2,86 @@ #ifndef __ASE_PROPERTIES_HH__ #define __ASE_PROPERTIES_HH__ -#include +#include #include #include namespace Ase { -String property_guess_nick (const String &property_label); - -/// Implementation namespace for Property helpers -namespace Properties { - -struct PropertyImpl; -using ValueGetter = std::function; -using ValueSetter = std::function; -using ValueLister = std::function; - -template inline ValueGetter Getter (const float *v); -template inline ValueSetter Setter (const float *v); - -/// Helper for property hint construction. -String construct_hints (const String &hints, const String &more, double pmin = 0, double pmax = 0); - -/// Construct Bool property. -PropertyP Bool (const String &ident, bool *v, const String &label, const String &nickname, bool dflt, const String &hints = "", const String &blurb = "", const String &description = ""); -inline PropertyP Bool (bool *v, const String &label, const String &nickname, bool dflt, const String &hints = "", const String &blurb = "", const String &description = "") -{ return Bool (label, v, label, nickname, dflt, hints, blurb, description); } +/// Class for preference parameters (global settings) +class Preference : public ParameterProperty { + /*ctor*/ Preference (ParameterC parameter); +public: + using DelCb = std::function; + using StringValueF = std::function; + virtual ~Preference (); + /*ctor*/ Preference (const CString &ident, const Param&, const StringValueF& = nullptr); + String gets () const { return const_cast (this)->get_value().as_string(); } + bool getb () const { return const_cast (this)->get_value().as_int(); } + int64 getn () const { return const_cast (this)->get_value().as_int(); } + uint64 getu () const { return const_cast (this)->get_value().as_int(); } + double getd () const { return const_cast (this)->get_value().as_double(); } + bool set (const Value &value) { return set_value (value); } + bool set (const String &string) { return set_value (string); } + Value get_value () override; + bool set_value (const Value &v) override; + static Value get (const String &ident); + static PreferenceP find (const String &ident); + static CStringS list (); + static DelCb listen (const std::function&); + static void save_preferences (); + static void load_preferences (bool autosave); +private: + DelCb sigh_; + Connection *connection_ = nullptr; + ASE_DEFINE_MAKE_SHARED (Preference); +}; -/// Construct Range property. -PropertyP Range (const String &ident, const ValueGetter &getter, const ValueSetter &setter, const String &label, const String &nickname, double pmin, double pmax, double dflt, - const String &unit = "", const String &hints = "", const String &blurb = "", const String &description = ""); +/// Function type for Property value getters. +using PropertyGetter = std::function; -/// Construct integer Range property. -PropertyP Range (const String &ident, int32 *v, const String &label, const String &nickname, int32 pmin, int32 pmax, int32 dflt, - const String &unit = "", const String &hints = "", const String &blurb = "", const String &description = ""); -inline PropertyP Range (int32 *v, const String &label, const String &nickname, int32 pmin, int32 pmax, int32 dflt, - const String &unit = "", const String &hints = "", const String &blurb = "", const String &description = "") -{ return Range (label, v, label, nickname, pmin, pmax, dflt, unit, hints, blurb, description); } +/// Function type for Property value setters. +using PropertySetter = std::function; -/// Construct float Range property. -PropertyP Range (const String &ident, float *v, const String &label, const String &nickname, double pmin, double pmax, double dflt, - const String &unit = "", const String &hints = "", const String &blurb = "", const String &description = ""); -inline PropertyP Range (float *v, const String &label, const String &nickname, double pmin, double pmax, double dflt, - const String &unit = "", const String &hints = "", const String &blurb = "", const String &description = "") -{ return Range (label, v, label, nickname, pmin, pmax, dflt, unit, hints, blurb, description); } +/// Function type to list Choice Property values. +using PropertyLister = std::function; -/// Construct double Range property. -PropertyP Range (const String &ident, double *v, const String &label, const String &nickname, double pmin, double pmax, double dflt, - const String &unit = "", const String &hints = "", const String &blurb = "", const String &description = ""); -inline PropertyP Range (double *v, const String &label, const String &nickname, double pmin, double pmax, double dflt, - const String &unit = "", const String &hints = "", const String &blurb = "", const String &description = "") -{ return Range (label, v, label, nickname, pmin, pmax, dflt, unit, hints, blurb, description); } +/// Structured initializer for PropertyImpl +struct Prop { + CString ident; ///< Valid NCName identifier. + PropertyGetter getter; ///< Lambda implementing the Property value getter. + PropertySetter setter; ///< Lambda implementing the Property value setter. + Param param; ///< Parameter meta data for this Property. + PropertyLister lister; ///< Lambda providing a list of possible Property value choices. +}; -/// Construct Text string property. -PropertyP Text (const String &ident, String *v, const String &label, const String &nickname, const String &hints = "", const String &blurb = "", const String &description = ""); -inline PropertyP Text (String *v, const String &label, const String &nickname, const String &hints = "", const String &blurb = "", const String &description = "") -{ return Text (label, v, label, nickname, hints, blurb, description); } +/// Property implementation for GadgetImpl, using lambdas as accessors. +class PropertyImpl : public ParameterProperty { + PropertyGetter getter_; PropertySetter setter_; PropertyLister lister_; + PropertyImpl (CString, const Param&, const PropertyGetter&, const PropertySetter&, const PropertyLister&); +public: + ASE_DEFINE_MAKE_SHARED (PropertyImpl); + Value get_value () override { Value v; getter_ (v); return v; } + bool set_value (const Value &v) override { return setter_ (v); } + ChoiceS choices () override { return lister_ ? lister_ (*this) : parameter_->choices(); } +}; -/// Construct Choice property. -PropertyP Text (const String &ident, String *v, const String &label, const String &nickname, const ValueLister &vl, const String &hints = "", const String &blurb = "", const String &description = ""); -inline PropertyP Text (String *v, const String &label, const String &nickname, const ValueLister &vl, const String &hints = "", const String &blurb = "", const String &description = "") -{ return Text (label, v, label, nickname, vl, hints, blurb, description); } +/// Helper to simplify property registrations. +struct PropertyBag { + using RegisterF = std::function; + explicit PropertyBag (const RegisterF &f) : add_ (f) {} + void operator+= (const Prop&) const; + CString group; +private: + RegisterF add_; +}; -/// Construct Enum property. -template::value > = true> inline PropertyP -Enum (const String &ident, E *v, const String &label, const String &nickname, const String &hints = "", const String &blurb = "", const String &description = "") +/// Value getter for enumeration types. +template std::function +make_enum_getter (Enum *v) { - using EnumType = Jsonipc::Enum; - ASE_ASSERT_RETURN (v, nullptr); - auto setter = [v] (const Value &val) { - E e = *v; - if (val.index() == Value::STRING) - e = EnumType::get_value (val.as_string(), e); - else if (val.index() == Value::INT64) - e = E (val.as_int()); - ASE_RETURN_UNLESS (e != *v, false); - *v = e; - return true; - }; - auto getter = [v] (Value &val) { + using EnumType = Jsonipc::Enum; + return [v] (Value &val) { if (EnumType::has_names()) { const String &name = EnumType::get_name (*v); @@ -90,82 +91,43 @@ Enum (const String &ident, E *v, const String &label, const String &nickname, co return; } } - val = int64 (*v); - }; - auto lister = [] (PropertyImpl &prop) { - ChoiceS choices; - for (const auto &evalue : EnumType::list_values()) - { - Choice choice (evalue.second, evalue.second); - choices.push_back (choice); - } - return choices; + val = int64_t (*v); }; - return mkprop ({ .ident = ident, .label = label, .nickname = nickname, .blurb = blurb, .description = description, - .hints = construct_hints (hints, "bool"), }, getter, setter, lister); -} - -/// Helper for construction of Property lists. -class Bag { -public: - using ConnectionS = std::vector; - Bag& operator+= (PropertyP p); - void on_events (const String &eventselector, const EventHandler &eventhandler); - ConnectionS connections; - CString group; - PropertyS props; -}; - -// == Implementation Helpers == -struct Initializer { - String ident; - String label; - String nickname; - String unit; - String blurb; - String description; - String groupname; - String hints; - double pmin = -1.7976931348623157e+308; - double pmax = +1.7976931348623157e+308; - double pdef = 0; -}; - -struct PropertyImpl : public EmittableImpl, public virtual Property { - virtual ~PropertyImpl (); -}; -using PropertyImplP = std::shared_ptr; - -/// Construct Property with handlers, emits `Event { .type = "change", .detail = identifier() }`. -PropertyImplP mkprop (const Initializer &initializer, const ValueGetter&, const ValueSetter&, const ValueLister&); - -/// == Implementations == -template inline ValueGetter -Getter (const V *p) -{ - return [p] (Value &val) { val = *p; }; } -template inline ValueSetter -Setter (V *p) +/// Value setter for enumeration types. +template std::function +make_enum_setter (Enum *v) { - return [p] (const Value &val) { - V v = {}; - if constexpr (std::is_floating_point::value) - v = val.as_double(); - else if constexpr (std::is_integral::value) - v = val.as_int(); - else if constexpr (std::is_base_of<::std::string, V>::value) - v = val.as_string(); - else - static_assert (sizeof (V) < 0, "Setter for type `V` unimplemented"); - return v == *p ? false : (*p = v, true); + using EnumType = Jsonipc::Enum; + return [v] (const Value &val) { + Enum e = *v; + if (val.index() == Value::STRING) + e = EnumType::get_value (val.as_string(), e); + else if (val.index() == Value::INT64) + e = Enum (val.as_int()); + ASE_RETURN_UNLESS (e != *v, false); + *v = e; + return true; }; } -} // Properties +template concept IsEnum = std::is_enum_v; -using PropertyBag = Properties::Bag; +/// Helper to list Jsonipc::Enum<> type values as Choice. +template requires IsEnum +ChoiceS +enum_lister (ParameterProperty&) +{ + using EnumType = Jsonipc::Enum; + ChoiceS choices; + for (const auto &evalue : EnumType::list_values()) + { + Choice choice (evalue.second, evalue.second); + choices.push_back (choice); + } + return choices; +} } // Ase From f5d0d139a49884785d31afb85c20ca5be48e664d Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Fri, 13 Oct 2023 14:25:47 +0200 Subject: [PATCH 22/50] ASE: api.hh, server: add Server.list_preferences() Signed-off-by: Tim Janik --- ase/api.hh | 1 + ase/server.cc | 7 +++++++ ase/server.hh | 1 + 3 files changed, 9 insertions(+) diff --git a/ase/api.hh b/ase/api.hh index 200764c2..c40ebc2f 100644 --- a/ase/api.hh +++ b/ase/api.hh @@ -396,6 +396,7 @@ public: virtual bool user_reply (uint64 noteid, uint r) = 0; virtual bool broadcast_telemetry (const TelemetrySegmentS &segments, int32 interval_ms) = 0; ///< Broadcast telemetry memory segments to the current Jsonipc connection. + virtual StringS list_preferences () = 0; ///< Retrieve a list of all preference identifiers. virtual PropertyP access_preference (const String &ident) = 0; ///< Retrieve property handle for a Preference identifier. // projects virtual ProjectP last_project () = 0; ///< Retrieve the last created project. diff --git a/ase/server.cc b/ase/server.cc index 54434ec5..ccbc98fd 100644 --- a/ase/server.cc +++ b/ase/server.cc @@ -235,6 +235,13 @@ ServerImpl::create_project (String projectname) return ProjectImpl::create (projectname); } +StringS +ServerImpl::list_preferences () +{ + const CStringS list = Preference::list(); + return { std::begin (list), std::end (list) }; +} + PropertyP ServerImpl::access_preference (const String &ident) { diff --git a/ase/server.hh b/ase/server.hh index 2ad84830..991cd561 100644 --- a/ase/server.hh +++ b/ase/server.hh @@ -33,6 +33,7 @@ public: ProjectP last_project () override; ProjectP create_project (String projectname) override; PropertyP access_preference (const String &ident) override; + StringS list_preferences () override; const Preferences& preferences () const { return prefs_; } using Block = FastMemory::Block; Block telemem_allocate (uint32 length) const; From d320b1dded92e815097b4c7be47a7755d66abf04 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Fri, 13 Oct 2023 14:36:35 +0200 Subject: [PATCH 23/50] ASE: api.hh, server: remove all old preference handling Signed-off-by: Tim Janik --- ase/api.hh | 26 ---------- ase/server.cc | 137 -------------------------------------------------- ase/server.hh | 4 -- 3 files changed, 167 deletions(-) diff --git a/ase/api.hh b/ase/api.hh index c40ebc2f..7579347f 100644 --- a/ase/api.hh +++ b/ase/api.hh @@ -156,32 +156,6 @@ public: virtual ChoiceS choices () = 0; ///< Enumerate choices for choosable properties. }; -// Preferences -struct Preferences { - // Synthesis Settings - String pcm_driver; ///< Driver and device to be used for PCM input and output. - int32 synth_latency = 5; ///< Processing duration between input and output of a single sample, smaller values increase CPU load. - int32 synth_mixing_freq = 48000; ///< Unused, synthesis mixing frequency is always 48000 Hz. - int32 synth_control_freq = 1500; ///< Unused frequency setting. - // MIDI - String midi_driver_1; ///< Driver and device to be used for MIDI input and output. - String midi_driver_2; - String midi_driver_3; - String midi_driver_4; - bool invert_sustain = false; - // Default Values - String author_default; ///< Default value for 'Author' fields. - String license_default; ///< Default value for 'License' fields. - String sample_path; ///< Search path of directories, seperated by ";", used to find audio samples. - String effect_path; ///< Search path of directories, seperated by ";", used to find effect files. - String instrument_path; ///< Search path of directories, seperated by ";", used to find instrument files. - String plugin_path; ///< Search path of directories, seperated by \";\", used to find plugins. - ///< This path is searched for in addition to the standard plugin location on this system. -private: - PropertyS access_properties (const EventHandler&); ///< Retrieve handles for all properties. - friend class ServerImpl; -}; - /// Base type for classes with Property interfaces. class Object : public virtual Emittable { protected: diff --git a/ase/server.cc b/ase/server.cc index ccbc98fd..51fd47cd 100644 --- a/ase/server.cc +++ b/ase/server.cc @@ -17,128 +17,6 @@ namespace Ase { -// == Preferences == -static Choice -choice_from_driver_entry (const DriverEntry &e, const String &icon_keywords) -{ - String blurb; - if (!e.device_info.empty() && !e.capabilities.empty()) - blurb = e.capabilities + "\n" + e.device_info; - else if (!e.capabilities.empty()) - blurb = e.capabilities; - else - blurb = e.device_info; - Choice c (e.devid, e.device_name, blurb); - if (string_startswith (string_tolower (e.notice), "warn")) - c.warning = e.notice; - else - c.notice = e.notice; - // e.priority - // e.readonly - // e.writeonly - // e.modem - c.icon = MakeIcon::KwIcon (icon_keywords + "," + e.hints); - return c; -} - -static ChoiceS -pcm_driver_choices (Properties::PropertyImpl&) -{ - ChoiceS choices; - for (const DriverEntry &e : PcmDriver::list_drivers()) - choices.push_back (choice_from_driver_entry (e, "pcm")); - return choices; -} - -static ChoiceS -midi_driver_choices (Properties::PropertyImpl&) -{ - ChoiceS choices; - for (const DriverEntry &e : MidiDriver::list_drivers()) - if (!e.writeonly) - choices.push_back (choice_from_driver_entry (e, "midi")); - return choices; -} - -PropertyS -Preferences::access_properties (const EventHandler &eventhandler) -{ - using namespace Properties; - static PropertyBag bag; - return_unless (bag.props.empty(), bag.props); - bag.group = _("Synthesis Settings"); - bag += Text (&pcm_driver, _("PCM Driver"), "", pcm_driver_choices, STANDARD, _("Driver and device to be used for PCM input and output")); - bag += Range (&synth_latency, _("Latency"), "", 0, 3000, 5, "ms", STANDARD + String ("step=5"), - _("Processing duration between input and output of a single sample, smaller values increase CPU load")); - bag += Range (&synth_mixing_freq, _("Synth Mixing Frequency"), "", 48000, 48000, 48000, "Hz", STANDARD, - _("Unused, synthesis mixing frequency is always 48000 Hz")); - bag += Range (&synth_control_freq, _("Synth Control Frequency"), "", 1500, 1500, 1500, "Hz", STANDARD, - _("Unused frequency setting")); - bag.group = _("MIDI"); - bag += Bool (&invert_sustain, _("Invert Sustain"), "", false, STANDARD, - _("Invert the state of sustain (damper) pedal so on/off meanings are reversed")); - bag += Text (&midi_driver_1, _("MIDI Controller"), "", midi_driver_choices, STANDARD, _("MIDI controller device to be used for MIDI input")); - bag += Text (&midi_driver_2, _("MIDI Controller"), "", midi_driver_choices, STANDARD, _("MIDI controller device to be used for MIDI input")); - bag += Text (&midi_driver_3, _("MIDI Controller"), "", midi_driver_choices, STANDARD, _("MIDI controller device to be used for MIDI input")); - bag += Text (&midi_driver_4, _("MIDI Controller"), "", midi_driver_choices, STANDARD, _("MIDI controller device to be used for MIDI input")); - bag.group = _("Default Values"); - bag += Text (&author_default, _("Default Author"), "", STANDARD, _("Default value for 'Author' fields")); - bag += Text (&license_default, _("Default License"), "", STANDARD, _("Default value for 'License' fields")); - bag.group = _("Search Paths"); - bag += Text (&sample_path, _("Sample Path"), "", STANDARD + String ("searchpath"), - _("Search path of directories, seperated by \";\", used to find audio samples.")); - bag += Text (&effect_path, _("Effect Path"), "", STANDARD + String ("searchpath"), - _("Search path of directories, seperated by \";\", used to find effect files.")); - bag += Text (&instrument_path, _("Instrument Path"), "", STANDARD + String ("searchpath"), - _("Search path of directories, seperated by \";\", used to find instrument files.")); - bag += Text (&plugin_path, _("Plugin Path"), "", STANDARD + String ("searchpath"), - _("Search path of directories, seperated by \";\", used to find plugins. This path " - "is searched for in addition to the standard plugin location on this system.")); - bag.on_events ("notify", eventhandler); - return bag.props; -} - -static Preferences -preferences_defaults() -{ - // Server is *not* yet available - Preferences prefs; - // static defaults - prefs.pcm_driver = "auto"; - prefs.synth_latency = 22; - prefs.synth_mixing_freq = 48000; - prefs.synth_control_freq = 1500; - prefs.midi_driver_1 = "null"; - prefs.midi_driver_2 = "null"; - prefs.midi_driver_3 = "null"; - prefs.midi_driver_4 = "null"; - prefs.invert_sustain = false; - prefs.license_default = "Creative Commons Attribution-ShareAlike 4.0 (https://creativecommons.org/licenses/by-sa/4.0/)"; - // dynamic defaults - const String default_user_path = Path::join (Path::user_home(), "Anklang"); - prefs.effect_path = default_user_path + "/Effects"; - prefs.instrument_path = default_user_path + "/Instruments"; - prefs.plugin_path = default_user_path + "/Plugins"; - prefs.sample_path = default_user_path + "/Samples"; - String user = user_name(); - if (!user.empty()) - { - String name = user_real_name(); - if (!name.empty() && user != name) - prefs.author_default = name; - else - prefs.author_default = user; - } - return prefs; -} - -static String -pathname_anklangrc() -{ - static const String anklangrc = Path::join (Path::config_home(), "anklang", "anklangrc.json"); - return anklangrc; -} - // == ServerImpl == JSONIPC_INHERIT (ServerImpl, Server); @@ -149,21 +27,6 @@ ServerImpl *SERVER = nullptr; ServerImpl::ServerImpl () : telemetry_arena (telemetry_size) { - // initialize preferences to defaults - prefs_ = preferences_defaults(); - // create preference properties, capturing defaults - prefs_properties_ = prefs_.access_properties ([this] (const Event&) { - ValueR args { { "prefs", json_parse (json_stringify (prefs_)) } }; - emit_event ("change", "prefs", args); - }); - // load preferences - const String jsontext = Path::stringread (pathname_anklangrc()); - if (!jsontext.empty()) - json_parse (jsontext, prefs_); - pchange_ = - on_event ("change:prefs", [this] (auto...) { - Path::stringwrite (pathname_anklangrc(), json_stringify (prefs_, Writ::RELAXED | Writ::SKIP_EMPTYSTRING), true); - }); assert_return (telemetry_arena.reserved() >= telemetry_size); Block telemetry_header = telemetry_arena.allocate (64); assert_return (telemetry_arena.location() == uint64 (telemetry_header.block_start)); diff --git a/ase/server.hh b/ase/server.hh index 991cd561..a4c63a04 100644 --- a/ase/server.hh +++ b/ase/server.hh @@ -8,9 +8,6 @@ namespace Ase { class ServerImpl : public GadgetImpl, public virtual Server { - Preferences prefs_; - PropertyS prefs_properties_; - Connection pchange_; FastMemory::Arena telemetry_arena; public: static ServerImplP instancep (); @@ -34,7 +31,6 @@ public: ProjectP create_project (String projectname) override; PropertyP access_preference (const String &ident) override; StringS list_preferences () override; - const Preferences& preferences () const { return prefs_; } using Block = FastMemory::Block; Block telemem_allocate (uint32 length) const; void telemem_release (Block telememblock) const; From 4ae99f9b846c0b5a08cf8f2b790b7e9399b35dda Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Thu, 5 Oct 2023 21:01:54 +0200 Subject: [PATCH 24/50] ASE: driver*: allow to query all PcmConfig values from drivers Signed-off-by: Tim Janik --- ase/driver-alsa.cc | 15 ++++++++++----- ase/driver-jack.cc | 15 ++++++++++----- ase/driver.cc | 25 +++++++++++++++---------- ase/driver.hh | 29 +++++++++++++++-------------- 4 files changed, 50 insertions(+), 34 deletions(-) diff --git a/ase/driver-alsa.cc b/ase/driver-alsa.cc index 419b92bd..aecc7a18 100644 --- a/ase/driver-alsa.cc +++ b/ase/driver-alsa.cc @@ -369,13 +369,18 @@ class AlsaPcmDriver : public PcmDriver { snd_pcm_close (write_handle_); delete[] period_buffer_; } - virtual float - pcm_frequency () const override + uint + pcm_n_channels () const override + { + return n_channels_; + } + uint + pcm_mix_freq () const override { return mix_freq_; } - virtual uint - block_length () const override + uint + pcm_block_length () const override { return period_size_; } @@ -639,7 +644,7 @@ class AlsaPcmDriver : public PcmDriver { *timeoutp = diff_frames * 1000 / mix_freq_; return false; } - virtual void + void pcm_latency (uint *rlatency, uint *wlatency) const override { snd_pcm_sframes_t rdelay, wdelay; diff --git a/ase/driver-jack.cc b/ase/driver-jack.cc index 21b4ec4c..e85f7555 100644 --- a/ase/driver-jack.cc +++ b/ase/driver-jack.cc @@ -558,13 +558,18 @@ class JackPcmDriver : public PcmDriver { if (jack_client_) close(); } - virtual float - pcm_frequency () const override + uint + pcm_n_channels () const override + { + return n_channels_; + } + uint + pcm_mix_freq () const override { return mix_freq_; } - virtual uint - block_length () const override + uint + pcm_block_length () const override { return block_length_; } @@ -785,7 +790,7 @@ class JackPcmDriver : public PcmDriver { *timeoutp = std::max (*timeoutp, 1); return false; } - virtual void + void pcm_latency (uint *rlatency, uint *wlatency) const override { assert_return (jack_client_ != NULL); diff --git a/ase/driver.cc b/ase/driver.cc index 5869be52..2ef82131 100644 --- a/ase/driver.cc +++ b/ase/driver.cc @@ -281,16 +281,27 @@ class NullPcmDriver : public PcmDriver { auto pdriverp = std::make_shared (kvpair_key (devid), kvpair_value (devid)); return pdriverp; } - virtual float - pcm_frequency () const override + uint + pcm_n_channels () const override + { + return n_channels_; + } + uint + pcm_mix_freq () const override { return mix_freq_; } - virtual uint - block_length () const override + uint + pcm_block_length () const override { return block_size_; } + void + pcm_latency (uint *rlatency, uint *wlatency) const override + { + *rlatency = mix_freq_ / 10; + *wlatency = mix_freq_ / 10; + } virtual void close () override { @@ -313,12 +324,6 @@ class NullPcmDriver : public PcmDriver { DDEBUG ("NULL-PCM: opening with freq=%f channels=%d: %s", mix_freq_, n_channels_, ase_error_blurb (Error::NONE)); return Error::NONE; } - virtual void - pcm_latency (uint *rlatency, uint *wlatency) const override - { - *rlatency = mix_freq_ / 10; - *wlatency = mix_freq_ / 10; - } virtual bool pcm_check_io (int64 *timeout_usecs) override { diff --git a/ase/driver.hh b/ase/driver.hh index 33db7aaa..ee4ba10b 100644 --- a/ase/driver.hh +++ b/ase/driver.hh @@ -88,27 +88,28 @@ using MidiDriverP = MidiDriver::MidiDriverP; struct PcmDriverConfig { uint n_channels = 0; uint mix_freq = 0; - uint latency_ms = 0; uint block_length = 0; + uint latency_ms = 0; }; class PcmDriver : public Driver { protected: - explicit PcmDriver (const String &driver, const String &devid); - virtual Ase::Error open (IODir iodir, const PcmDriverConfig &config) = 0; + explicit PcmDriver (const String &driver, const String &devid); + virtual Ase::Error open (IODir iodir, const PcmDriverConfig &config) = 0; public: typedef std::shared_ptr PcmDriverP; - static PcmDriverP open (const String &devid, IODir desired, IODir required, const PcmDriverConfig &config, Ase::Error *ep); - virtual bool pcm_check_io (int64 *timeoutp) = 0; - virtual void pcm_latency (uint *rlatency, uint *wlatency) const = 0; - virtual float pcm_frequency () const = 0; - virtual uint block_length () const = 0; - virtual size_t pcm_read (size_t n, float *values) = 0; - virtual void pcm_write (size_t n, const float *values) = 0; - static EntryVec list_drivers (); - static String register_driver (const String &driverid, - const std::function &create, - const std::function &list); + static PcmDriverP open (const String &devid, IODir desired, IODir required, const PcmDriverConfig &config, Ase::Error *ep); + virtual uint pcm_n_channels () const = 0; + virtual uint pcm_mix_freq () const = 0; + virtual uint pcm_block_length () const = 0; + virtual void pcm_latency (uint *rlatency, uint *wlatency) const = 0; + virtual bool pcm_check_io (int64 *timeoutp) = 0; + virtual size_t pcm_read (size_t n, float *values) = 0; + virtual void pcm_write (size_t n, const float *values) = 0; + static EntryVec list_drivers (); + static String register_driver (const String &driverid, + const std::function &create, + const std::function &list); }; using PcmDriverP = PcmDriver::PcmDriverP; From 0dba45dfcfc9a7454f857dcb2ca7275795502e14 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Tue, 3 Oct 2023 03:08:40 +0200 Subject: [PATCH 25/50] ASE: engine.cc: add audio.synth_latency Preference Signed-off-by: Tim Janik --- ase/engine.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ase/engine.cc b/ase/engine.cc index fb8ede8e..1dcb2794 100644 --- a/ase/engine.cc +++ b/ase/engine.cc @@ -17,6 +17,12 @@ namespace Ase { +static std::atomic pref_synth_latency; +static Preference synth_latency_p = Preference ("audio.synth_latency", + { _("Latency"), "", 15, { 0, 3000, 5 }, "ms", STANDARD, + "", _("Processing duration between input and output of a single sample, smaller values increase CPU load") }, + [] (const CString &ident, const Value &value) { pref_synth_latency = value.as_int(); printerr ("audio.synth_latency: %d\n", pref_synth_latency); }); + // == decls == using VoidFunc = std::function; using StartQueue = AsyncBlockingQueue; From 3bee1e89cb077dd4e8ab624e36258fc558a129b8 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sat, 7 Oct 2023 03:34:08 +0200 Subject: [PATCH 26/50] ASE: engine: change PCM driver when preferences change Signed-off-by: Tim Janik --- ase/engine.cc | 365 +++++++++++++++++++++++++++----------------------- ase/engine.hh | 9 ++ 2 files changed, 208 insertions(+), 166 deletions(-) diff --git a/ase/engine.cc b/ase/engine.cc index 1dcb2794..ded60df2 100644 --- a/ase/engine.cc +++ b/ase/engine.cc @@ -17,17 +17,15 @@ namespace Ase { -static std::atomic pref_synth_latency; -static Preference synth_latency_p = Preference ("audio.synth_latency", - { _("Latency"), "", 15, { 0, 3000, 5 }, "ms", STANDARD, - "", _("Processing duration between input and output of a single sample, smaller values increase CPU load") }, - [] (const CString &ident, const Value &value) { pref_synth_latency = value.as_int(); printerr ("audio.synth_latency: %d\n", pref_synth_latency); }); +constexpr const uint FIXED_N_CHANNELS = 2; +constexpr const uint FIXED_SAMPLE_RATE = 48000; +constexpr const uint FIXED_N_MIDI_DRIVERS = 4; // == decls == using VoidFunc = std::function; using StartQueue = AsyncBlockingQueue; ASE_CLASS_DECLS (EngineMidiInput); -constexpr uint fixed_sample_rate = 48000; +static void apply_driver_preferences (); // == EngineJobImpl == struct EngineJobImpl { @@ -41,15 +39,23 @@ atomic_next_ptrref (EngineJobImpl *j) return j->next; } +struct DriverSet { + PcmDriverP null_pcm_driver; + String pcm_name; + PcmDriverP pcm_driver; + StringS midi_names; + MidiDriverS midi_drivers; +}; + // == AudioEngineThread == class AudioEngineThread : public AudioEngine { - PcmDriverP null_pcm_driver_, pcm_driver_; - MidiDriverS midi_drivers_; +public: static constexpr uint fixed_n_channels = 2; + PcmDriverP null_pcm_driver_, pcm_driver_; constexpr static size_t MAX_BUFFER_SIZE = AUDIO_BLOCK_MAX_RENDER_SIZE; size_t buffer_size_ = MAX_BUFFER_SIZE; // mono buffer size float chbuffer_data_[MAX_BUFFER_SIZE * fixed_n_channels] = { 0, }; - uint64 write_stamp_ = 0, render_stamp_ = AUDIO_BLOCK_MAX_RENDER_SIZE; + uint64 write_stamp_ = 0, render_stamp_ = MAX_BUFFER_SIZE; std::vector schedule_; EngineMidiInputP midi_proc_; bool schedule_invalid_ = true; @@ -58,12 +64,11 @@ class AudioEngineThread : public AudioEngine { const VoidF owner_wakeup_; std::thread *thread_ = nullptr; MainLoopP event_loop_ = MainLoop::create(); - Emittable::Connection onchange_prefs_; AudioProcessorS oprocs_; ProjectImplP project_; WaveWriterP wwriter_; FastMemory::Block transport_block_; -public: + DriverSet driver_set_ml; // accessed by main_loop thread std::atomic autostop_ = U64MAX; struct UserNoteJob { std::atomic next = nullptr; @@ -79,8 +84,6 @@ class AudioEngineThread : public AudioEngine { uint64 frame_counter () const { return render_stamp_; } void schedule_render (uint64 frames); void enable_output (AudioProcessor &aproc, bool onoff); - void start_threads (); - void stop_threads (); void wakeup_thread_mt (); void capture_start (const String &filename, bool needsrunning); void capture_stop (); @@ -91,12 +94,14 @@ class AudioEngineThread : public AudioEngine { bool pcm_check_write (bool write_buffer, int64 *timeout_usecs_p = nullptr); bool driver_dispatcher (const LoopState &state); bool process_jobs (AtomicIntrusiveStack &joblist); - void update_drivers (bool fullio, uint latency); void run (StartQueue *sq); - void swap_midi_drivers_sync (const MidiDriverS &midi_drivers); void queue_user_note (const String &channel, UserNote::Flags flags, const String &text); void set_project (ProjectImplP project); ProjectImplP get_project (); + void update_driver_set (DriverSet &dset); + void start_threads_ml (); + void stop_threads_ml (); + void create_processors_ml (); }; static std::thread::id audio_engine_thread_id = {}; @@ -239,111 +244,6 @@ AudioEngineThread::enable_output (AudioProcessor &aproc, bool onoff) } } -void -AudioEngineThread::update_drivers (bool fullio, uint latency) -{ - Error er = {}; - // PCM fallback - PcmDriverConfig pconfig { .n_channels = fixed_n_channels, .mix_freq = fixed_sample_rate, - .latency_ms = latency, .block_length = AUDIO_BLOCK_MAX_RENDER_SIZE }; - const String null_driver = "null"; - if (!null_pcm_driver_) - { - null_pcm_driver_ = PcmDriver::open (null_driver, Driver::WRITEONLY, Driver::WRITEONLY, pconfig, &er); - if (!null_pcm_driver_ || er != 0) - fatal_error ("failed to open internal PCM driver ('%s'): %s", null_driver, ase_error_blurb (er)); - } - if (!pcm_driver_) - pcm_driver_ = null_pcm_driver_; - // MIDI Processor - if (!midi_proc_) - swap_midi_drivers_sync ({}); - if (!fullio) - return; - // PCM Output - if (pcm_driver_ == null_pcm_driver_) - { - const String pcm_driver_name = ServerImpl::instancep()->preferences().pcm_driver; - er = {}; - if (pcm_driver_name != null_driver) - { - PcmDriverP new_pcm_driver = PcmDriver::open (pcm_driver_name, Driver::WRITEONLY, Driver::WRITEONLY, pconfig, &er); - if (new_pcm_driver) - pcm_driver_ = new_pcm_driver; - else - { - auto s = string_format ("# Audio I/O Error\n" - "Failed to open audio device:\n" - "%s:\n" - "%s", - pcm_driver_name, ase_error_blurb (er)); - queue_user_note ("pcm-driver", UserNote::CLEAR, s); - printerr ("%s\n", string_replace (s, "\n", " ")); - } - } - } - buffer_size_ = std::min (MAX_BUFFER_SIZE, size_t (pcm_driver_->block_length())); - floatfill (chbuffer_data_, 0.0, buffer_size_ * fixed_n_channels); - write_stamp_ = render_stamp_ - buffer_size_; // force zeros initially - EDEBUG ("AudioEngineThread::update_drivers: PCM: channels=%d pcmblock=%d enginebuffer=%d\n", - fixed_n_channels, pcm_driver_->block_length(), buffer_size_); - // MIDI Driver List - MidiDriverS old_drivers = midi_drivers_, new_drivers; - const auto midi_driver_names = { - ServerImpl::instancep()->preferences().midi_driver_1, - ServerImpl::instancep()->preferences().midi_driver_2, - ServerImpl::instancep()->preferences().midi_driver_3, - ServerImpl::instancep()->preferences().midi_driver_4, - }; - int midi_errors = 0; - auto midi_err = [&] (const String &devid, int nth, Error er) { - auto s = string_format ("## MIDI I/O Failure\n" - "Failed to open MIDI device #%u:\n" - "%s:\n" - "%s", - nth, devid, ase_error_blurb (er)); - queue_user_note ("midi-driver", midi_errors++ == 0 ? UserNote::CLEAR : UserNote::APPEND, s); - printerr ("%s\n", string_replace (s, "\n", " ")); - }; - int midi_dev = 0; - for (const auto &devid : midi_driver_names) - { - midi_dev += 1; - if (devid == null_driver) - continue; - if (Aux::contains (new_drivers, [&] (auto &d) { - return d->devid() == devid; })) - { - midi_err (devid, midi_dev, Error::DEVICE_BUSY); - continue; - } - MidiDriverP d; - for (MidiDriverP o : old_drivers) - if (o->devid() == devid) - d = o; - if (d) - { - Aux::erase_first (old_drivers, [&] (auto &o) { return o == d; }); - new_drivers.push_back (d); // keep opened driver - continue; - } - er = {}; - d = MidiDriver::open (devid, Driver::READONLY, &er); - if (d) - new_drivers.push_back (d); // add new driver - else - midi_err (devid, midi_dev, er); - } - midi_drivers_ = new_drivers; - swap_midi_drivers_sync (midi_drivers_); - while (!old_drivers.empty()) - { - MidiDriverP old = old_drivers.back(); - old_drivers.pop_back(); - old->close(); // close old driver *after* sync - } -} - void AudioEngineThread::capture_start (const String &filename, bool needsrunning) { @@ -385,7 +285,12 @@ AudioEngineThread::capture_stop() void AudioEngineThread::run (StartQueue *sq) { - assert_return (pcm_driver_); + assert_return (null_pcm_driver_); + if (!pcm_driver_) + pcm_driver_ = null_pcm_driver_; + floatfill (chbuffer_data_, 0.0, MAX_BUFFER_SIZE * fixed_n_channels); + buffer_size_ = std::min (MAX_BUFFER_SIZE, size_t (pcm_driver_->pcm_block_length())); + write_stamp_ = render_stamp_ - buffer_size_; // write an initial buffer of zeros // FIXME: assert owner_wakeup and free trash this_thread_set_name ("AudioEngine-0"); // max 16 chars audio_engine_thread_id = std::this_thread::get_id(); @@ -462,7 +367,8 @@ AudioEngineThread::driver_dispatcher (const LoopState &state) proc->schedule_processor(); schedule_invalid_ = false; } - schedule_render (buffer_size_); + if (render_stamp_ <= write_stamp_) // async jobs may have adjusted stamps + schedule_render (buffer_size_); pcm_check_write (true); // minimize drop outs } if (!const_jobs_.empty()) { // owner may be blocking for const_jobs_ execution @@ -522,34 +428,33 @@ AudioEngineThread::wakeup_thread_mt() } void -AudioEngineThread::start_threads() +AudioEngineThread::start_threads_ml() { - schedule_.reserve (8192); + assert_return (this_thread_is_ase()); // main_loop thread assert_return (thread_ == nullptr); - const uint latency = SERVER->preferences().synth_latency; - update_drivers (false, latency); + assert_return (midi_proc_ == nullptr); + schedule_.reserve (8192); + create_processors_ml(); + update_drivers ("null", 0, {}); // create drivers + null_pcm_driver_ = driver_set_ml.null_pcm_driver; schedule_queue_update(); StartQueue start_queue; thread_ = new std::thread (&AudioEngineThread::run, this, &start_queue); const char reply = start_queue.pop(); // synchronize with thread start assert_return (reply == 'R'); - onchange_prefs_ = ASE_SERVER.on_event ("change:prefs", [this, latency] (auto...) { - update_drivers (true, latency); - }); - update_drivers (true, latency); + apply_driver_preferences(); } void -AudioEngineThread::stop_threads() +AudioEngineThread::stop_threads_ml() { - AudioEngineThread &engine = *dynamic_cast (this); - assert_return (engine.thread_ != nullptr); - onchange_prefs_.reset(); - engine.event_loop_->quit (0); - engine.thread_->join(); + assert_return (this_thread_is_ase()); // main_loop thread + assert_return (thread_ != nullptr); + event_loop_->quit (0); + thread_->join(); audio_engine_thread_id = {}; - auto oldthread = engine.thread_; - engine.thread_ = nullptr; + auto oldthread = thread_; + thread_ = nullptr; delete oldthread; } @@ -634,6 +539,8 @@ AudioEngineThread::AudioEngineThread (const VoidF &owner_wakeup, uint sample_rat AudioEngine& make_audio_engine (const VoidF &owner_wakeup, uint sample_rate, SpeakerArrangement speakerarrangement) { + ASE_ASSERT_ALWAYS (sample_rate == FIXED_SAMPLE_RATE); + ASE_ASSERT_ALWAYS (speaker_arrangement_count_channels (speakerarrangement) == FIXED_N_CHANNELS); FastMemory::Block transport_block = ServerImpl::instancep()->telemem_allocate (sizeof (AudioTransport)); return *new AudioEngineThread (owner_wakeup, sample_rate, speakerarrangement, transport_block); } @@ -688,14 +595,14 @@ void AudioEngine::start_threads() { AudioEngineThread &impl = static_cast (*this); - return impl.start_threads(); + return impl.start_threads_ml(); } void AudioEngine::stop_threads() { AudioEngineThread &impl = static_cast (*this); - return impl.stop_threads(); + return impl.stop_threads_ml(); } void @@ -773,9 +680,71 @@ AudioEngine::JobQueue::operator+= (const std::function &job) return audio_engine_thread.add_job_mt (new EngineJobImpl (job), this); } -// == MidiInput == -// Processor providing MIDI device events +bool +AudioEngine::update_drivers (const String &pcm_name, uint latency_ms, const StringS &midis) +{ + AudioEngineThread &engine_thread = static_cast (*this); + DriverSet &dset = engine_thread.driver_set_ml; + const char *const null_driver = "null"; + int must_update = 0; + // PCM Config + const PcmDriverConfig pcm_config { .n_channels = engine_thread.fixed_n_channels, .mix_freq = FIXED_SAMPLE_RATE, + .block_length = AUDIO_BLOCK_MAX_RENDER_SIZE, .latency_ms = latency_ms }; + // PCM Fallback + if (!dset.null_pcm_driver) { + must_update++; + Error er = {}; + dset.null_pcm_driver = PcmDriver::open (null_driver, Driver::WRITEONLY, Driver::WRITEONLY, pcm_config, &er); + if (!dset.null_pcm_driver || er != 0) + fatal_error ("failed to open internal PCM driver ('%s'): %s", null_driver, ase_error_blurb (er)); + } + // PCM Driver + if (pcm_name != dset.pcm_name) { + must_update++; + dset.pcm_name = pcm_name; + Error er = {}; + dset.pcm_driver = dset.pcm_name == null_driver ? dset.null_pcm_driver : + PcmDriver::open (dset.pcm_name, Driver::WRITEONLY, Driver::WRITEONLY, pcm_config, &er); + if (!dset.pcm_driver || er != 0) { + dset.pcm_driver = dset.null_pcm_driver; + const String errmsg = string_format ("# Audio I/O Error\n" "Failed to open audio device:\n" "%s:\n" "%s", + dset.pcm_name, ase_error_blurb (er)); + engine_thread.queue_user_note ("driver.pcm", UserNote::CLEAR, errmsg); + printerr ("%s\n", string_replace (errmsg, "\n", " ")); + } + } + // MIDI Drivers + dset.midi_drivers.resize (FIXED_N_MIDI_DRIVERS); + dset.midi_names.resize (dset.midi_drivers.size()); + for (size_t i = 0; i < dset.midi_drivers.size(); i++) { + const String midi_name = i < midis.size() ? midis[i] : null_driver; + if (midi_name == dset.midi_names[i]) + continue; + must_update++; + dset.midi_names[i] = midi_name; + Error er = {}; + dset.midi_drivers[i] = dset.midi_names[i] == null_driver ? nullptr : + MidiDriver::open (dset.midi_names[i], Driver::READONLY, &er); + if (er != 0) { + dset.midi_drivers[i] = nullptr; + const String errmsg = string_format ("# MIDI I/O Error\n" "Failed to open MIDI device #%u:\n" "%s:\n" "%s", + 1 + i, dset.midi_names[i], ase_error_blurb (er)); + engine_thread.queue_user_note ("driver.midi", UserNote::CLEAR, errmsg); + printerr ("%s\n", string_replace (errmsg, "\n", " ")); + } + } + // Update running engine + if (must_update) { + Mutable mdset = dset; // use Mutable so Job can swap and the remains are cleaned up in ~Job + synchronized_jobs += [mdset,&engine_thread] () { engine_thread.update_driver_set (mdset.value); }; + return true; + } + return false; +} + +// == EngineMidiInput == class EngineMidiInput : public AudioProcessor { + // Processor providing MIDI device events void initialize (SpeakerArrangement busses) override { @@ -794,7 +763,8 @@ class EngineMidiInput : public AudioProcessor { MidiEventStream &estream = get_event_output(); estream.clear(); for (size_t i = 0; i < midi_drivers_.size(); i++) - midi_drivers_[i]->fetch_events (estream, sample_rate()); + if (midi_drivers_[i]) + midi_drivers_[i]->fetch_events (estream, sample_rate()); } public: MidiDriverS midi_drivers_; @@ -803,31 +773,18 @@ class EngineMidiInput : public AudioProcessor { {} }; -template struct Mutable { - mutable T value; - Mutable (const T &v) : value (v) {} - operator T& () { return value; } -}; - void -AudioEngineThread::swap_midi_drivers_sync (const MidiDriverS &midi_drivers) -{ - if (!midi_proc_) - { - AudioProcessorP aprocp = AudioProcessor::create_processor (*this); - assert_return (aprocp); - midi_proc_ = std::dynamic_pointer_cast (aprocp); - assert_return (midi_proc_); - EngineMidiInputP midi_proc = midi_proc_; - async_jobs += [midi_proc] () { - midi_proc->enable_engine_output (true); - }; - } +AudioEngineThread::create_processors_ml () +{ + assert_return (this_thread_is_ase()); // main_loop thread + assert_return (midi_proc_ == nullptr); + AudioProcessorP aprocp = AudioProcessor::create_processor (*this); + assert_return (aprocp); + midi_proc_ = std::dynamic_pointer_cast (aprocp); + assert_return (midi_proc_); EngineMidiInputP midi_proc = midi_proc_; - Mutable new_drivers { midi_drivers }; - synchronized_jobs += [midi_proc, new_drivers] () { - midi_proc->midi_drivers_.swap (new_drivers.value); - // use swap() to defer dtor to user thread + async_jobs += [midi_proc] () { + midi_proc->enable_engine_output (true); // MUST_SCHEDULE }; } @@ -837,4 +794,80 @@ AudioEngineThread::get_event_source () return midi_proc_; } +void +AudioEngineThread::update_driver_set (DriverSet &dset) +{ + // use swap() to defer dtor to user thread + assert_return (midi_proc_); + // PCM Driver + if (pcm_driver_ != dset.pcm_driver) { + pcm_driver_.swap (dset.pcm_driver); + floatfill (chbuffer_data_, 0.0, MAX_BUFFER_SIZE * fixed_n_channels); + buffer_size_ = std::min (MAX_BUFFER_SIZE, size_t (pcm_driver_->pcm_block_length())); + write_stamp_ = render_stamp_ - buffer_size_; // write an initial buffer of zeros + EDEBUG ("AudioEngineThread::%s: update PCM to \"%s\": channels=%d pcmblock=%d enginebuffer=%d ws=%u rs=%u bs=%u\n", __func__, + dset.pcm_name, fixed_n_channels, pcm_driver_->pcm_block_length(), buffer_size_, write_stamp_, render_stamp_, buffer_size_); + } + // MIDI Drivers + if (midi_proc_->midi_drivers_ != dset.midi_drivers) { + midi_proc_->midi_drivers_.swap (dset.midi_drivers); + EDEBUG ("AudioEngineThread::%s: swapping %u MIDI drivers: \"%s\"\n", __func__, midi_proc_->midi_drivers_.size(), string_join ("\" \"", dset.midi_names)); + } +} + +// == DriverSet == +static Choice +choice_from_driver_entry (const DriverEntry &e, const String &icon_keywords) +{ + String blurb; + if (!e.device_info.empty() && !e.capabilities.empty()) + blurb = e.capabilities + "\n" + e.device_info; + else if (!e.capabilities.empty()) + blurb = e.capabilities; + else + blurb = e.device_info; + Choice c (e.devid, e.device_name, blurb); + if (string_startswith (string_tolower (e.notice), "warn")) + c.warning = e.notice; + else + c.notice = e.notice; + // e.priority + // e.readonly + // e.writeonly + // e.modem + c.icon = MakeIcon::KwIcon (icon_keywords + "," + e.hints); + return c; +} + +static ChoiceS +pcm_driver_pref_list_choices (const CString &ident) +{ + ChoiceS choices; + for (const DriverEntry &e : PcmDriver::list_drivers()) + choices.push_back (choice_from_driver_entry (e, "pcm")); + return choices; +} + +static Preference pcm_driver_pref = + Preference ("driver.pcm.devid", + { _("PCM Driver"), "", "auto", "ms", { pcm_driver_pref_list_choices }, + STANDARD, "", _("Driver and device to be used for PCM input and output"), }, + [] (const CString&,const Value&) { apply_driver_preferences(); }); + +static Preference synth_latency_pref = + Preference ("driver.pcm.synth_latency", + { _("Synth Latency"), "", 15, "ms", MinMaxStep { 0, 3000, 5 }, STANDARD + String ("step=5"), + "", _("Processing duration between input and output of a single sample, smaller values increase CPU load") }, + [] (const CString&,const Value&) { apply_driver_preferences(); }); + +static void +apply_driver_preferences () +{ + static uint engine_driver_set_timerid = 0; + main_loop->exec_once (97, &engine_driver_set_timerid, + []() { + main_config.engine->update_drivers (pcm_driver_pref.gets(), synth_latency_pref.getn(), {}); + }); +} + } // Ase diff --git a/ase/engine.hh b/ase/engine.hh index ecc8d136..683e9c56 100644 --- a/ase/engine.hh +++ b/ase/engine.hh @@ -48,6 +48,7 @@ public: void set_autostop (uint64_t nsamples); void queue_capture_start (CallbackS&, const String &filename, bool needsrunning); void queue_capture_stop (CallbackS&); + bool update_drivers (const String &pcm, uint latency_ms, const StringS &midis); static bool thread_is_engine () { return std::this_thread::get_id() == thread_id; } static const ThreadId &thread_id; // JobQueues @@ -66,6 +67,14 @@ protected: AudioEngine& make_audio_engine (const VoidF &owner_wakeup, uint sample_rate, SpeakerArrangement speakerarrangement); +/// Helper to modify const struct contents, e.g. asyn job lambda members. +template +struct Mutable { + mutable T value; + Mutable (const T &v) : value (v) {} + operator T& () { return value; } +}; + } // Ase #endif /* __ASE_ENGINE_HH__ */ From ef3598f9c0e64bc4df3f78550077a68ca40e9b3a Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sat, 7 Oct 2023 03:38:50 +0200 Subject: [PATCH 27/50] ASE: engine.cc: cache PCM driver live listing for ca half a second Signed-off-by: Tim Janik --- ase/engine.cc | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ase/engine.cc b/ase/engine.cc index ded60df2..e52237b8 100644 --- a/ase/engine.cc +++ b/ase/engine.cc @@ -842,9 +842,14 @@ choice_from_driver_entry (const DriverEntry &e, const String &icon_keywords) static ChoiceS pcm_driver_pref_list_choices (const CString &ident) { - ChoiceS choices; - for (const DriverEntry &e : PcmDriver::list_drivers()) - choices.push_back (choice_from_driver_entry (e, "pcm")); + static ChoiceS choices; + static uint64 cache_age = 0; + if (choices.empty() || timestamp_realtime() > cache_age + 500 * 1000) { + choices.clear(); + for (const DriverEntry &e : PcmDriver::list_drivers()) + choices.push_back (choice_from_driver_entry (e, "pcm")); + cache_age = timestamp_realtime(); + } return choices; } From 1a328c56bc7ba98d195f7dc94107653877f13433 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sat, 7 Oct 2023 15:27:34 +0200 Subject: [PATCH 28/50] ASE: engine.cc: add preferences for up to 4 MIDI controllers Signed-off-by: Tim Janik --- ase/engine.cc | 63 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/ase/engine.cc b/ase/engine.cc index e52237b8..b8ac4937 100644 --- a/ase/engine.cc +++ b/ase/engine.cc @@ -681,7 +681,7 @@ AudioEngine::JobQueue::operator+= (const std::function &job) } bool -AudioEngine::update_drivers (const String &pcm_name, uint latency_ms, const StringS &midis) +AudioEngine::update_drivers (const String &pcm_name, uint latency_ms, const StringS &midi_prefs) { AudioEngineThread &engine_thread = static_cast (*this); DriverSet &dset = engine_thread.driver_set_ml; @@ -713,15 +713,26 @@ AudioEngine::update_drivers (const String &pcm_name, uint latency_ms, const Stri printerr ("%s\n", string_replace (errmsg, "\n", " ")); } } + // Deduplicate MIDI Drivers + StringS midis = midi_prefs; + midis.resize (FIXED_N_MIDI_DRIVERS); + for (size_t i = 0; i < midis.size(); i++) + if (midis[i].empty()) + midis[i] = null_driver; + else + for (size_t j = 0; j < i; j++) // dedup + if (midis[i] != null_driver && midis[i] == midis[j]) { + midis[i] = null_driver; + break; + } // MIDI Drivers - dset.midi_drivers.resize (FIXED_N_MIDI_DRIVERS); - dset.midi_names.resize (dset.midi_drivers.size()); + dset.midi_names.resize (midis.size()); + dset.midi_drivers.resize (dset.midi_names.size()); for (size_t i = 0; i < dset.midi_drivers.size(); i++) { - const String midi_name = i < midis.size() ? midis[i] : null_driver; - if (midi_name == dset.midi_names[i]) + if (midis[i] == dset.midi_names[i]) continue; must_update++; - dset.midi_names[i] = midi_name; + dset.midi_names[i] = midis[i]; Error er = {}; dset.midi_drivers[i] = dset.midi_names[i] == null_driver ? nullptr : MidiDriver::open (dset.midi_names[i], Driver::READONLY, &er); @@ -853,6 +864,21 @@ pcm_driver_pref_list_choices (const CString &ident) return choices; } +static ChoiceS +midi_driver_pref_list_choices (const CString &ident) +{ + static ChoiceS choices; + static uint64 cache_age = 0; + if (choices.empty() || timestamp_realtime() > cache_age + 500 * 1000) { + choices.clear(); + for (const DriverEntry &e : MidiDriver::list_drivers()) + if (!e.writeonly) + choices.push_back (choice_from_driver_entry (e, "midi")); + cache_age = timestamp_realtime(); + } + return choices; +} + static Preference pcm_driver_pref = Preference ("driver.pcm.devid", { _("PCM Driver"), "", "auto", "ms", { pcm_driver_pref_list_choices }, @@ -865,13 +891,36 @@ static Preference synth_latency_pref = "", _("Processing duration between input and output of a single sample, smaller values increase CPU load") }, [] (const CString&,const Value&) { apply_driver_preferences(); }); +static Preference midi1_driver_pref = + Preference ("driver.midi1.devid", + { _("MIDI Controller (1)"), "", "auto", "ms", { midi_driver_pref_list_choices }, + STANDARD, "", _("MIDI controller device to be used for MIDI input"), }, + [] (const CString&,const Value&) { apply_driver_preferences(); }); +static Preference midi2_driver_pref = + Preference ("driver.midi2.devid", + { _("MIDI Controller (2)"), "", "auto", "ms", { midi_driver_pref_list_choices }, + STANDARD, "", _("MIDI controller device to be used for MIDI input"), }, + [] (const CString&,const Value&) { apply_driver_preferences(); }); +static Preference midi3_driver_pref = + Preference ("driver.midi3.devid", + { _("MIDI Controller (3)"), "", "auto", "ms", { midi_driver_pref_list_choices }, + STANDARD, "", _("MIDI controller device to be used for MIDI input"), }, + [] (const CString&,const Value&) { apply_driver_preferences(); }); +static Preference midi4_driver_pref = + Preference ("driver.midi4.devid", + { _("MIDI Controller (4)"), "", "auto", "ms", { midi_driver_pref_list_choices }, + STANDARD, "", _("MIDI controller device to be used for MIDI input"), }, + [] (const CString&,const Value&) { apply_driver_preferences(); }); + + static void apply_driver_preferences () { static uint engine_driver_set_timerid = 0; main_loop->exec_once (97, &engine_driver_set_timerid, []() { - main_config.engine->update_drivers (pcm_driver_pref.gets(), synth_latency_pref.getn(), {}); + StringS midis = { midi1_driver_pref.gets(), midi2_driver_pref.gets(), midi3_driver_pref.gets(), midi4_driver_pref.gets(), }; + main_config.engine->update_drivers (pcm_driver_pref.gets(), synth_latency_pref.getn(), midis); }); } From ba784dd4d6706574ef6464a2108347e1a57b6a0c Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Thu, 5 Oct 2023 15:52:10 +0200 Subject: [PATCH 29/50] UI: b/contextmenu.js: support focus_uri Signed-off-by: Tim Janik --- ui/b/contextmenu.js | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/ui/b/contextmenu.js b/ui/b/contextmenu.js index d862d319..26148f9d 100644 --- a/ui/b/contextmenu.js +++ b/ui/b/contextmenu.js @@ -34,10 +34,11 @@ * *close (event)* * : Event signaling closing of the menu, regardless of whether menu item activation occoured or not. * ### Methods: - * *popup (event, { origin, data-contextmenu })* + * *popup (event, { origin, focus_uri, data-contextmenu })* * : Popup the contextmenu, propagation of `event` is halted and the event coordinates or target is * : used for positioning unless `origin` is given. * : The `origin` is a reference DOM element to use for drop-down positioning. + * : The `focus_uri` is a URI to receive focus after popup. * : The `data-contextmenu` element (or `origin`) has the `data-contextmenu=true` attribute assigned during popup. * *close()* * : Hide the contextmenu. @@ -153,6 +154,7 @@ class BContextMenu extends LitComponent { this.xscale = 1; this.yscale = 1; this.origin = null; + this.focus_uri = ''; this.data_contextmenu = null; this.checkuri = () => true; this.reposition = false; @@ -206,7 +208,8 @@ class BContextMenu extends LitComponent { if (this.reposition) { this.reposition = false; - const p = Util.popup_position (this.dialog, { origin: this.origin, x: this.page_x, y: this.page_y, xscale: this.xscale, yscale: this.yscale, }); + const p = Util.popup_position (this.dialog, { origin: this.origin, focus_uri: this.focus_uri, + x: this.page_x, y: this.page_y, xscale: this.xscale, yscale: this.yscale, }); this.dialog.style.left = p.x + "px"; this.dialog.style.top = p.y + "px"; // chrome does auto-focus for showModal(), make FF behave the same @@ -222,6 +225,7 @@ class BContextMenu extends LitComponent { const origin = popup_options.origin?.$el || popup_options.origin || event?.currentTarget; if (origin instanceof Element && Util.inside_display_none (origin)) return false; // cannot popup around hidden origin + this.focus_uri = popup_options.focus_uri || ''; this.origin = origin instanceof Element ? origin : null; this.menudata.menu_stamp = Util.frame_stamp(); // allows one popup per frame if (event && event.pageX && event.pageY) @@ -240,18 +244,26 @@ class BContextMenu extends LitComponent { this.dialog.showModal(); App.zmove(); // force changes to be picked up // check items (and this used to handle auto-focus) - await this.check_isactive(); + const focussable = await this.check_isactive (this.focus_uri); + this.focus_uri = ''; + if (focussable) + focussable.focus(); }) (); } - async check_isactive() + async check_isactive (finduri = null) { const w = document.createTreeWalker (this, NodeFilter.SHOW_ELEMENT); - let e, a = []; + let hasuri = null, e, a = []; while ( (e = w.nextNode()) ) { /**@type{any}*/ const any = e; - a.push (any.check_isactive?.()); + if (any.check_isactive) { + if (any.uri === finduri) + hasuri = any; + a.push (any.check_isactive()); + } } await Promise.all (a); + return hasuri; } bubbeling_click_ (event) { @@ -308,6 +320,7 @@ class BContextMenu extends LitComponent { this.dialog.close(); } this.origin = null; + this.focus_uri = ''; this.data_contextmenu?.removeAttribute ('data-contextmenu', 'true'); this.data_contextmenu = null; App.zmove(); // force changes to be picked up From 853183ea22fe5b7050d8c9105509a3d49c9b7a3d Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Thu, 5 Oct 2023 15:52:40 +0200 Subject: [PATCH 30/50] UI: b/objecteditor.js: rename internal function on xprop Signed-off-by: Tim Janik --- ui/b/objecteditor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/b/objecteditor.js b/ui/b/objecteditor.js index e081a06a..88aaf6a9 100644 --- a/ui/b/objecteditor.js +++ b/ui/b/objecteditor.js @@ -107,7 +107,7 @@ class BObjectEditor extends LitComponent { for (const group of this.gprops) { content.push (GROUP_HTML (this, group)); for (const prop of group.props) { - const component_html = prop.component_html_ (this, prop); + const component_html = prop.b_objecteditor_component_html_ (this, prop); content.push (PROP_HTML (this, prop, component_html)); } } @@ -177,7 +177,7 @@ class BObjectEditor extends LitComponent { component_html = CHOICE_HTML; else component_html = TEXT_HTML; - xprop.component_html_ = component_html; + xprop.b_objecteditor_component_html_ = component_html; } Object.freeze (groups[k]); } From c1e1cff1dce4488343fee59c0f07e7dfa85cfbd2 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Thu, 5 Oct 2023 15:52:59 +0200 Subject: [PATCH 31/50] UI: b/preferencesdialog.vue: special case driver.pcm.devid Signed-off-by: Tim Janik --- ui/b/preferencesdialog.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/b/preferencesdialog.vue b/ui/b/preferencesdialog.vue index 9bd42644..6d6cba58 100644 --- a/ui/b/preferencesdialog.vue +++ b/ui/b/preferencesdialog.vue @@ -37,7 +37,7 @@ async function augment_property (xprop) { for (let i = 0; i < xprop.value_.choices.length; i++) { const c = xprop.value_.choices[i]; - if (xprop.ident_ == 'pcm_driver') + if (xprop.ident_ == 'driver.pcm.devid') augment_choice_entry (c, 'pcm'); else if (xprop.ident_.match (/midi/i)) augment_choice_entry (c, 'midi'); From ed893f79ff5eaf84d97b03dfb4234d3cb079e8b6 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Thu, 5 Oct 2023 15:53:39 +0200 Subject: [PATCH 32/50] UI: b/choiceinput.js: treat choice property as string Signed-off-by: Tim Janik --- ui/b/choiceinput.js | 47 +++++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/ui/b/choiceinput.js b/ui/b/choiceinput.js index 73053982..210a9651 100644 --- a/ui/b/choiceinput.js +++ b/ui/b/choiceinput.js @@ -12,7 +12,7 @@ import * as Util from '../util.js'; * protocol by emitting an `input` event on value changes and accepting inputs via the `value` prop. * ### Props: * *value* - * : Integer, the index of the choice value to be displayed. + * : The choice value to be displayed. * *choices* * : List of choices: `[ { icon, label, blurb }... ]` * *title* @@ -119,7 +119,7 @@ const CONTEXTMENU_HTML = (t) => html` `; const CONTEXTMENU_ITEM = (t, c) => html` - + ${ c.label } ${ c.blurb } ${ c.line2 } @@ -180,43 +180,33 @@ class BChoiceInput extends LitComponent { const mchoices = []; for (let i = 0; i < this.choices.length; i++) { const c = Object.assign ({}, this.choices[i]); - c.uri = c.uri || c.ident; mchoices.push (c); } return mchoices; } - index() + current() { const mchoices = this.mchoices(); - for (let i = 0; i < mchoices.length; i++) - if (mchoices[i].uri == this.value_) - return i; for (let i = 0; i < mchoices.length; i++) if (mchoices[i].ident == this.value_) - return i; - return 999e99; + return mchoices[i]; + return {}; } data_tip() { - const mchoices = this.mchoices(); - const index = this.index(); + const choice = this.current(); let tip = "**CLICK** Select Choice"; const plabel = this.label || this.prop?.label_; - if (!plabel || !mchoices || index >= mchoices.length) + if (!plabel || !choice.label) return tip; - const c = mchoices[index]; let val = "**" + plabel + "** "; - val += c.label; + val += choice.label; return val + " " + tip; } nick() { - const mchoices = this.mchoices(); - const index = this.index(); - if (!mchoices || index >= mchoices.length) - return ""; - const c = mchoices[index]; - return c.label ? c.label : ''; + const choice = this.current(); + return choice.label || ""; } activate (uri) { @@ -236,7 +226,7 @@ class BChoiceInput extends LitComponent { this.performUpdate(); } this.pophere.focus(); - this.cmenu.popup (event, { origin: this.pophere }); + this.cmenu.popup (event, { origin: this.pophere, focus_uri: this.value }); } keydown (event) { @@ -248,12 +238,15 @@ class BChoiceInput extends LitComponent { event.preventDefault(); event.stopPropagation(); const mchoices = this.mchoices(); - let index = this.index(); - if (mchoices) { - index += event.keyCode == Util.KeyCode.DOWN ? +1 : -1; - if (index >= 0 && index < mchoices.length) - this.activate (mchoices[index].uri); - } + const choice = this.current(); + if (choice.ident) + for (let i = 0; i < mchoices.length; i++) + if (mchoices[i].ident == choice.ident) { + const index = i + (event.keyCode == Util.KeyCode.DOWN ? +1 : -1); + if (index >= 0 && index < mchoices.length) + this.activate (mchoices[index].ident); + break; + } } else if (event.keyCode == Util.KeyCode.ENTER) this.popup_menu (event); From 9181d605be94e8bf4852d2dd0c2ed37c696ba199 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Fri, 13 Oct 2023 12:24:23 +0200 Subject: [PATCH 33/50] UI: util.js: rename extended Property descr() (abbreviate) Signed-off-by: Tim Janik --- ui/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/util.js b/ui/util.js index ee4417c0..dc974f20 100644 --- a/ui/util.js +++ b/ui/util.js @@ -935,7 +935,7 @@ export async function extend_property (prop, disconnector = undefined, augment = unit_: prop.unit(), group_: prop.group(), blurb_: prop.blurb(), - description_: prop.description(), + descr_: prop.descr(), min_: prop.get_min(), max_: prop.get_max(), step_: prop.get_step(), From 0c6264b46b6ea899bb8c039a4cc948ae9e288503 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Fri, 13 Oct 2023 12:27:27 +0200 Subject: [PATCH 34/50] UI: index.html, eslintrc.js: allow _("for translation") markup Signed-off-by: Tim Janik --- ui/eslintrc.js | 1 + ui/index.html | 1 + 2 files changed, 2 insertions(+) diff --git a/ui/eslintrc.js b/ui/eslintrc.js index 9e937157..c1f4f5a2 100644 --- a/ui/eslintrc.js +++ b/ui/eslintrc.js @@ -15,6 +15,7 @@ module.exports = { sourceType: "module" }, globals: { + _: false, App: false, Ase: false, Data: false, diff --git a/ui/index.html b/ui/index.html index b76c81ca..a7b902bb 100644 --- a/ui/index.html +++ b/ui/index.html @@ -43,6 +43,7 @@ From 572a042fffedc16d71f479c35cf5ff8c12791d12 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sat, 14 Oct 2023 21:57:00 +0200 Subject: [PATCH 38/50] ASE: gadget: add property_bag() and create_properties() Signed-off-by: Tim Janik --- ase/gadget.cc | 17 ++++++++++++++++- ase/gadget.hh | 4 ++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/ase/gadget.cc b/ase/gadget.cc index f4be65b1..6f59a8cd 100644 --- a/ase/gadget.cc +++ b/ase/gadget.cc @@ -154,7 +154,9 @@ GadgetImpl::name (String newname) PropertyS GadgetImpl::access_properties () { - return {}; // TODO: implement access_properties + if (props_.empty()) + create_properties(); + return { begin (props_), end (props_) }; } // == Gadget == @@ -201,4 +203,17 @@ Gadget::set_value (String ident, const Value &v) return prop && prop->set_value (v); } +PropertyBag +GadgetImpl::property_bag () +{ + auto add_prop = [this] (const Prop &prop, CString group) { + Param param = prop.param; + if (param.group.empty() && !group.empty()) + param.group = group; + this->props_.push_back (PropertyImpl::make_shared (prop.ident, param, prop.getter, prop.setter, prop.lister)); + // PropertyImpl &property = *gadget_.props_.back(); + }; + return PropertyBag (add_prop); +} + } // Ase diff --git a/ase/gadget.hh b/ase/gadget.hh index e4f9a3bd..191495fd 100644 --- a/ase/gadget.hh +++ b/ase/gadget.hh @@ -4,6 +4,7 @@ #include #include +#include namespace Ase { @@ -13,6 +14,7 @@ class GadgetImpl : public ObjectImpl, public CustomDataContainer, public virtual uint64_t gadget_flags_ = 0; ValueR session_data_; protected: + PropertyImplS props_; enum : uint64_t { GADGET_DESTROYED = 0x1, DEVICE_ACTIVE = 0x2, MASTER_TRACK = 0x4 }; uint64_t gadget_flags () const { return gadget_flags_; } uint64_t gadget_flags (uint64_t setbits, uint64_t mask = ~uint64_t (0)); @@ -20,6 +22,8 @@ protected: virtual ~GadgetImpl (); virtual String fallback_name () const; void serialize (WritNode &xs) override; + PropertyBag property_bag (); + virtual void create_properties () {} public: void _set_parent (GadgetImpl *parent) override; GadgetImpl* _parent () const override { return parent_; } From c1625f38865a0c277fbc01b73a99ab8fef625f10 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sat, 14 Oct 2023 21:57:22 +0200 Subject: [PATCH 39/50] ASE: clapdevice.cc: use parameter_guess_nick() Signed-off-by: Tim Janik --- ase/clapdevice.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ase/clapdevice.cc b/ase/clapdevice.cc index 617d2bd1..9fdedfb2 100644 --- a/ase/clapdevice.cc +++ b/ase/clapdevice.cc @@ -29,7 +29,7 @@ struct ClapPropertyImpl : public Property, public virtual EmittableImpl { public: String identifier () override { return ident_; } String label () override { return label_; } - String nick () override { return property_guess_nick (label_); } + String nick () override { return parameter_guess_nick (label_); } String unit () override { return ""; } String hints () override { return ClapParamInfo::hints_from_param_info_flags (flags); } String group () override { return module_; } From 337f93ae38c2e3fb5574636556fd6af4dcd2656b Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sat, 14 Oct 2023 21:58:18 +0200 Subject: [PATCH 40/50] ASE: project: create_properties: use property_bag() and Prop Signed-off-by: Tim Janik --- ase/project.cc | 33 +++++++++++++++++---------------- ase/project.hh | 2 +- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/ase/project.cc b/ase/project.cc index d529981d..720783d1 100644 --- a/ase/project.cc +++ b/ase/project.cc @@ -786,33 +786,34 @@ ProjectImpl::master_track () return tracks_.back(); } -PropertyS -ProjectImpl::access_properties () +void +ProjectImpl::create_properties () { + // chain to base class + DeviceImpl::create_properties(); + // create own properties auto getbpm = [this] (Value &val) { val = tick_sig_.bpm(); }; auto setbpm = [this] (const Value &val) { return set_bpm (val.as_double()); }; auto getbpb = [this] (Value &val) { val = tick_sig_.beats_per_bar(); }; auto setbpb = [this] (const Value &val) { return set_numerator (val.as_int()); }; auto getunt = [this] (Value &val) { val = tick_sig_.beat_unit(); }; auto setunt = [this] (const Value &val) { return set_denominator (val.as_int()); }; - using namespace Properties; - PropertyBag bag; + PropertyBag bag = property_bag(); // bag.group = _("State"); // TODO: bag += Bool ("dirty", &dirty_, _("Modification Flag"), _("Dirty"), false, ":r:G:", _("Flag indicating modified project state")); + // struct Prop { CString ident; ValueGetter getter; ValueSetter setter; Param param; ValueLister lister; }; bag.group = _("Timing"); - bag += Range ("numerator", getbpb, setbpb, _("Signature Numerator"), _("Numerator"), 1., 63., 4., STANDARD); - bag += Range ("denominator", getunt, setunt, _("Signature Denominator"), _("Denominator"), 1, 16, 4, STANDARD); - bag += Range ("bpm", getbpm, setbpm, _("Beats Per Minute"), _("BPM"), 10., 1776., 90., STANDARD); + bag += Prop ("numerator", getbpb, setbpb, { _("Signature Numerator"), _("Numerator"), 4., "", MinMaxStep { 1., 63., 0 }, STANDARD }); + bag += Prop ("denominator", getunt, setunt, { _("Signature Denominator"), _("Denominator"), 4, "", MinMaxStep { 1, 16, 0 }, STANDARD }); + bag += Prop ("bpm", getbpm, setbpm, { _("Beats Per Minute"), _("BPM"), 90., "", MinMaxStep { 10., 1776., 0 }, STANDARD }); bag.group = _("Tuning"); - bag += Enum ("musical_tuning", &musical_tuning_, _("Musical Tuning"), _("Tuning"), STANDARD, "", - _("The tuning system which specifies the tones or pitches to be used. " - "Due to the psychoacoustic properties of tones, various pitch combinations can " - "sound \"natural\" or \"pleasing\" when used in combination, the musical " - "tuning system defines the number and spacing of frequency values applied.")); - bag.on_events ("change", [this] (const Event &e) { - emit_event (e.type(), e.detail()); - }); - return bag.props; + bag += Prop ("musical_tuning", make_enum_getter (&musical_tuning_), make_enum_setter (&musical_tuning_), + { _("Musical Tuning"), _("Tuning"), uint32_t (MusicalTuning::OD_12_TET), "", {}, STANDARD, "", + _("The tuning system which specifies the tones or pitches to be used. " + "Due to the psychoacoustic properties of tones, various pitch combinations can " + "sound \"natural\" or \"pleasing\" when used in combination, the musical " + "tuning system defines the number and spacing of frequency values applied.") }, + enum_lister); } DeviceInfo diff --git a/ase/project.hh b/ase/project.hh index a6329b91..758847f9 100644 --- a/ase/project.hh +++ b/ase/project.hh @@ -40,10 +40,10 @@ protected: virtual ~ProjectImpl (); void serialize (WritNode &xs) override; void update_tempo (); + void create_properties () override; public: void _activate () override; void _deactivate () override; - PropertyS access_properties () override; const TickSignature& signature () const { return tick_sig_; } void discard () override; AudioProcessorP _audio_processor () const override; From 69be427fe525088d53d65e25270a1fb6ef8179b3 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sun, 15 Oct 2023 14:58:32 +0200 Subject: [PATCH 41/50] ASE: strings: base option search on string_option_find_value() (without malloc) Signed-off-by: Tim Janik --- ase/strings.cc | 265 +++++++++++++++++++++++-------------------------- ase/strings.hh | 13 +-- 2 files changed, 126 insertions(+), 152 deletions(-) diff --git a/ase/strings.cc b/ase/strings.cc index 1778590a..dc987210 100644 --- a/ase/strings.cc +++ b/ase/strings.cc @@ -1257,111 +1257,87 @@ kvpairs_search (const StringS &kvs, const String &k, const bool casesensitive) } // === String Options === -#define is_sep(c) (c == ';' || c == ':') -#define is_spacesep(c) (isspace (c) || is_sep (c)) -#define find_sep(str) (strpbrk (str, ";:")) - -static void -string_option_add (const String &assignment, - std::vector *option_namesp, - std::vector &option_values, - const String &empty_default, - const String *filter) -{ - assert_return ((option_namesp != NULL) ^ (filter != NULL)); - const char *n = assignment.c_str(); - while (isspace (*n)) - n++; - const char *p = n; - while (isalnum (*p) || *p == '-' || *p == '_') - p++; - const String name = String (n, p - n); - if (filter && name != *filter) - return; - while (isspace (*p)) - p++; - const String value = *p == '=' ? String (p + 1) : empty_default; - if (!name.empty() && (*p == '=' || *p == 0)) // valid name - { - if (!filter) - option_namesp->push_back (name); - option_values.push_back (value); - } -} - -static void -string_options_split_filtered (const String &option_string, - std::vector *option_namesp, - std::vector &option_values, - const String &empty_default, - const String *filter) -{ - const char *s = option_string.c_str(); - while (s) - { - // find next separator - const char *b = find_sep (s); - string_option_add (String (s, b ? b - s : strlen (s)), option_namesp, option_values, empty_default, filter); - s = b ? b + 1 : NULL; - } -} - -/// Split an option list string into name/value pairs. -void -string_options_split (const String &option_string, - std::vector &option_names, - std::vector &option_values, - const String &empty_default) -{ - string_options_split_filtered (option_string, &option_names, option_values, empty_default, NULL); -} - -static String -string_option_find_value (const String &option_string, - const String &option) -{ - std::vector option_names, option_values; - string_options_split_filtered (option_string, NULL, option_values, "1", &option); - return option_values.empty() ? "0" : option_values[option_values.size() - 1]; -} - -/// Retrieve the option value from an options list separated by ':' or ';'. +static bool is_separator (char c) { return c == ';' || c == ':'; } + +static const char* +find_option (const char *haystack, const char *const needle, const size_t l, const int allowoption) +{ + const char *match = nullptr; + for (const char *c = strcasestr (haystack, needle); c; c = strcasestr (c + 1, needle)) + if (!allowoption && + (c[l] == 0 && is_separator (c[l])) && + ((c == haystack + 3 && strncasecmp (haystack, "no-", 3) == 0) || + (c >= haystack + 4 && is_separator (haystack[0]) && strncasecmp (haystack + 1, "no-", 3) == 0))) + match = c; + else if (allowoption && + ((allowoption >= 2 && c[l] == '=') || c[l] == 0 || is_separator (c[l])) && + (c == haystack || is_separator (c[-1]))) + match = c; + return match; +} + +static size_t +separator_strlen (const char *const s) +{ + const char *c = s; + while (c[0] && !is_separator (c[0])) + c++; + return c - s; +} + +static std::string_view +string_option_find_value (const char *string, const char *feature, const char *fallback, const char *denied, const int matching) +{ + if (!string || !feature || !string[0] || !feature[0]) + return { fallback, strlen (fallback) }; // not-found + const size_t l = strlen (feature); + const char *match = find_option (string, feature, l, 2); // .prio=2 + if (matching >= 2) { + const char *deny = find_option (string, feature, l, 0); // .prio=1 + if (deny > match) + return { denied, strlen (denied) }; // denied + } + if (match && match[l] == '=') + return { match + l + 1, separator_strlen (match + l + 1) }; // value + if (match) + return { "1", 1 }; // allowed + if (matching >= 3) { + if (find_option (string, "all", 3, 1)) // .prio=3 + return { "1", 1 }; // allowed + if (find_option (string, "none", 4, 1)) // .prio=4 + return { denied, strlen (denied) }; // denied + } + return { fallback, strlen (fallback) }; // not-found +} + +/// Low level option search, avoids dynamic allocations. +std::string_view +string_option_find_value (const char *string, const char *feature, const String &fallback, const String &denied, bool matchallnone) +{ + return string_option_find_value (string, feature, fallback.c_str(), denied.c_str(), matchallnone ? 3 : 2); +} + +/// Find `feature` in an `optionlist`, return its value or `fallback`. String -string_option_get (const String &option_string, - const String &option) +string_option_find (const String &optionlist, const String &feature, const String &fallback) { - return string_option_find_value (option_string, option); + std::string_view sv = string_option_find_value (optionlist.data(), feature.data(), fallback.data(), "0", 3); + return { sv.data(), sv.size() }; } /// Check if an option is set/unset in an options list string. bool -string_option_check (const String &option_string, - const String &option) +string_option_check (const String &optionlist, const String &feature) { - const String value = string_option_find_value (option_string, option); - return string_to_bool (value, true); + return string_to_bool (string_option_find (optionlist, feature, "0"), true); } -/// Find @a feature in @a config, return its value or @a fallback. +/// Retrieve the option value from an options list separated by ':' or ';'. String -string_option_find (const String &config, const String &feature, const String &fallback) -{ - String haystack = ":" + config + ":"; - String needle0 = ":no-" + feature + ":"; - String needle1 = ":" + feature + ":"; - String needle2 = ":" + feature + "="; - const char *n0 = g_strrstr (haystack.c_str(), needle0.c_str()); - const char *n1 = g_strrstr (haystack.c_str(), needle1.c_str()); - const char *n2 = g_strrstr (haystack.c_str(), needle2.c_str()); - if (n0 && (!n1 || n0 > n1) && (!n2 || n0 > n2)) - return "0"; // ":no-feature:" is the last toggle in config - if (n1 && (!n2 || n1 > n2)) - return "1"; // ":feature:" is the last toggle in config - if (!n2) - return fallback; // no "feature" variant found - const char *value = n2 + strlen (needle2.c_str()); - const char *end = strchr (value, ':'); - return end ? String (value, end - value) : String (value); +string_option_get (const String &optionlist, const String &feature) +{ + std::string_view sv = string_option_find_value (optionlist.data(), feature.data(), "", "", 3); + return { sv.data(), sv.size() }; } // == Strings == @@ -1618,54 +1594,57 @@ string_tests() TCMP (string_join (";", sv), ==, "a;b;cdef"); sv = string_split_any (" foo , bar , \t\t baz \n", ","); TCMP (string_join (";", sv), ==, " foo ; bar ; \t\t baz \n"); - TASSERT (string_option_check (" foo ", "foo") == true); - TASSERT (string_option_check (" foo9 ", "foo9") == true); - TASSERT (string_option_check (" foo7 ", "foo9") == false); - TASSERT (string_option_check (" bar ", "bar") == true); - TASSERT (string_option_check (" bar= ", "bar") == true); - TASSERT (string_option_check (" bar=0 ", "bar") == false); - TASSERT (string_option_check (" bar=no ", "bar") == false); - TASSERT (string_option_check (" bar=false ", "bar") == false); - TASSERT (string_option_check (" bar=off ", "bar") == false); - TASSERT (string_option_check (" bar=1 ", "bar") == true); - TASSERT (string_option_check (" bar=2 ", "bar") == true); - TASSERT (string_option_check (" bar=3 ", "bar") == true); - TASSERT (string_option_check (" bar=4 ", "bar") == true); - TASSERT (string_option_check (" bar=5 ", "bar") == true); - TASSERT (string_option_check (" bar=6 ", "bar") == true); - TASSERT (string_option_check (" bar=7 ", "bar") == true); - TASSERT (string_option_check (" bar=8 ", "bar") == true); - TASSERT (string_option_check (" bar=9 ", "bar") == true); - TASSERT (string_option_check (" bar=09 ", "bar") == true); - TASSERT (string_option_check (" bar=yes ", "bar") == true); - TASSERT (string_option_check (" bar=true ", "bar") == true); - TASSERT (string_option_check (" bar=on ", "bar") == true); - TASSERT (string_option_check (" bar=1false ", "bar") == true); - TASSERT (string_option_check (" bar=0true ", "bar") == false); - TASSERT (string_option_check (" foo ", "foo") == true); - TASSERT (string_option_check (" foo9 ", "foo9") == true); - TASSERT (string_option_check (" foo7 ", "foo9") == false); - TASSERT (string_option_check (" bar ", "bar") == true); - TASSERT (string_option_check (" bar= ", "bar") == true); - TASSERT (string_option_check (" bar=0 ", "bar") == false); - TASSERT (string_option_check (" bar=no ", "bar") == false); - TASSERT (string_option_check (" bar=false ", "bar") == false); - TASSERT (string_option_check (" bar=off ", "bar") == false); - TASSERT (string_option_check (" bar=1 ", "bar") == true); - TASSERT (string_option_check (" bar=2 ", "bar") == true); - TASSERT (string_option_check (" bar=3 ", "bar") == true); - TASSERT (string_option_check (" bar=4 ", "bar") == true); - TASSERT (string_option_check (" bar=5 ", "bar") == true); - TASSERT (string_option_check (" bar=6 ", "bar") == true); - TASSERT (string_option_check (" bar=7 ", "bar") == true); - TASSERT (string_option_check (" bar=8 ", "bar") == true); - TASSERT (string_option_check (" bar=9 ", "bar") == true); - TASSERT (string_option_check (" bar=09 ", "bar") == true); - TASSERT (string_option_check (" bar=yes ", "bar") == true); - TASSERT (string_option_check (" bar=true ", "bar") == true); - TASSERT (string_option_check (" bar=on ", "bar") == true); - TASSERT (string_option_check (" bar=1false ", "bar") == true); - TASSERT (string_option_check (" bar=0true ", "bar") == false); + TASSERT (string_option_check (":foo:", "foo") == true); + TASSERT (string_option_check (":foo9:", "foo9") == true); + TASSERT (string_option_check (":foo7:", "foo9") == false); + TASSERT (string_option_check (":bar:", "bar") == true); + TASSERT (string_option_check (":bar=:", "bar") == true); + TASSERT (string_option_get (":bar:", "bar") == "1"); + TASSERT (string_option_check (":bar=0:", "bar") == false); + TASSERT (string_option_get (":bar=0:", "bar") == "0"); + TASSERT (string_option_get (":no-bar:", "bar") == ""); + TASSERT (string_option_check (":bar=no:", "bar") == false); + TASSERT (string_option_check (":bar=false:", "bar") == false); + TASSERT (string_option_check (":bar=off:", "bar") == false); + TASSERT (string_option_check (":bar=1:", "bar") == true); + TASSERT (string_option_check (":bar=2:", "bar") == true); + TASSERT (string_option_check (":bar=3:", "bar") == true); + TASSERT (string_option_check (":bar=4:", "bar") == true); + TASSERT (string_option_check (":bar=5:", "bar") == true); + TASSERT (string_option_check (":bar=6:", "bar") == true); + TASSERT (string_option_check (":bar=7:", "bar") == true); + TASSERT (string_option_check (":bar=8:", "bar") == true); + TASSERT (string_option_check (":bar=9:", "bar") == true); + TASSERT (string_option_check (":bar=09:", "bar") == true); + TASSERT (string_option_check (":bar=yes:", "bar") == true); + TASSERT (string_option_check (":bar=true:", "bar") == true); + TASSERT (string_option_check (":bar=on:", "bar") == true); + TASSERT (string_option_check (":bar=1false:", "bar") == true); + TASSERT (string_option_check (":bar=0true:", "bar") == false); + TASSERT (string_option_check (":foo:", "foo") == true); + TASSERT (string_option_check (":foo9:", "foo9") == true); + TASSERT (string_option_check (":foo7:", "foo9") == false); + TASSERT (string_option_check (":bar:", "bar") == true); + TASSERT (string_option_check (":bar=:", "bar") == true); + TASSERT (string_option_check (":bar=0:", "bar") == false); + TASSERT (string_option_check (":bar=no:", "bar") == false); + TASSERT (string_option_check (":bar=false:", "bar") == false); + TASSERT (string_option_check (":bar=off:", "bar") == false); + TASSERT (string_option_check (":bar=1:", "bar") == true); + TASSERT (string_option_check (":bar=2:", "bar") == true); + TASSERT (string_option_check (":bar=3:", "bar") == true); + TASSERT (string_option_check (":bar=4:", "bar") == true); + TASSERT (string_option_check (":bar=5:", "bar") == true); + TASSERT (string_option_check (":bar=6:", "bar") == true); + TASSERT (string_option_check (":bar=7:", "bar") == true); + TASSERT (string_option_check (":bar=8:", "bar") == true); + TASSERT (string_option_check (":bar=9:", "bar") == true); + TASSERT (string_option_check (":bar=09:", "bar") == true); + TASSERT (string_option_check (":bar=yes:", "bar") == true); + TASSERT (string_option_check (":bar=true:", "bar") == true); + TASSERT (string_option_check (":bar=on:", "bar") == true); + TASSERT (string_option_check (":bar=1false:", "bar") == true); + TASSERT (string_option_check (":bar=0true:", "bar") == false); String r; r = string_option_find ("a:b", "a"); TCMP (r, ==, "1"); r = string_option_find ("a:b", "b"); TCMP (r, ==, "1"); diff --git a/ase/strings.hh b/ase/strings.hh index 22eb81bc..cb0968b4 100644 --- a/ase/strings.hh +++ b/ase/strings.hh @@ -138,15 +138,10 @@ template<> inline String string_from_type (String value) // == String Options == -bool string_option_check (const String &option_string, - const String &option); -String string_option_get (const String &option_string, - const String &option); -void string_options_split (const String &option_string, - std::vector &option_names, - std::vector &option_values, - const String &empty_default = ""); -String string_option_find (const String &config, const String &feature, const String &fallback = "0"); +bool string_option_check (const String &optionlist, const String &feature); +String string_option_get (const String &optionlist, const String &feature); +String string_option_find (const String &optionlist, const String &feature, const String &fallback = "0"); +std::string_view string_option_find_value (const char *string, const char *feature, const String &fallback, const String &denied, bool matchallnone); // == Generic Key-Value-Pairs == String kvpair_key (const String &key_value_pair); From 03d33be0b2a8fbe219cf38bce5c3df321a01819a Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sun, 15 Oct 2023 14:59:02 +0200 Subject: [PATCH 42/50] ASE: utils.cc: base debug key checks on string_option_find_value() Signed-off-by: Tim Janik --- ase/utils.cc | 63 +++++++++++++++------------------------------------- 1 file changed, 18 insertions(+), 45 deletions(-) diff --git a/ase/utils.cc b/ase/utils.cc index 83838f1e..e097daa0 100644 --- a/ase/utils.cc +++ b/ase/utils.cc @@ -17,13 +17,27 @@ namespace Ase { // == Debugging == bool ase_debugging_enabled = true; static bool ase_fatal_warnings = false; +static const char* +getenv_ase_debug() +{ + // cache $ASE_DEBUG and setup debug_any_enabled; + static const char *const ase_debug = [] { + const char *const d = getenv ("ASE_DEBUG"); + ase_debugging_enabled = d && d[0]; + ase_fatal_warnings = string_to_bool (String (string_option_find_value (d, "fatal-warnings", "0", "0", true))); + // ase_sigquit_on_abort = string_to_bool (String (string_option_find_value (d, "sigquit-on-abort", "0", "0", true))); + // ase_backtrace_on_error = string_to_bool (String (string_option_find_value (d, "backtrace", "0", "0", true))); + return d; + }(); + return ase_debug; +} /// Check if `conditional` is enabled by $ASE_DEBUG. bool debug_key_enabled (const char *conditional) { - const std::string value = debug_key_value (conditional); - return !value.empty() && (strchr ("123456789yYtT", value[0]) || strncasecmp (value.c_str(), "on", 2) == 0); + const std::string_view sv = string_option_find_value (getenv_ase_debug(), conditional, "0", "0", true); + return string_to_bool (String (sv)); } /// Check if `conditional` is enabled by $ASE_DEBUG. @@ -37,49 +51,8 @@ debug_key_enabled (const ::std::string &conditional) ::std::string debug_key_value (const char *conditional) { - // cache $ASE_DEBUG and setup debug_any_enabled; - static const std::string debug_flags = [] () { - const char *f = getenv ("ASE_DEBUG"); - const std::string cflags = !f ? "" : ":" + std::string (f) + ":"; - const std::string lflags = string_tolower (cflags); - ase_debugging_enabled = !lflags.empty() && lflags != ":none:"; - const ssize_t fw = lflags.rfind (":fatal-warnings:"); - const ssize_t nf = lflags.rfind (":no-fatal-warnings:"); - if (fw >= 0 && nf <= fw) - ase_fatal_warnings = true; - const ssize_t sq = lflags.rfind (":sigquit-on-abort:"); - const ssize_t nq = lflags.rfind (":no-sigquit-on-abort:"); - if (sq >= 0 && nq <= sq) - {} // ase_sigquit_on_abort = true; - const ssize_t wb = lflags.rfind (":backtrace:"); - const ssize_t nb = lflags.rfind (":no-backtrace:"); - if (wb > nb) - {} // ase_backtrace_on_error = true; - if (nb > wb) - {} // ase_backtrace_on_error = false; - return lflags; - } (); - // find key in colon-separated debug flags - const ::std::string key = conditional ? string_tolower (conditional) : ""; - static const std::string all = ":all:", none = ":none:"; - const std::string condr = ":no-" + key + ":"; - const std::string condc = ":" + key + ":"; - const std::string conde = ":" + key + "="; - const ssize_t pa = debug_flags.rfind (all); - const ssize_t pn = debug_flags.rfind (none); - const ssize_t pr = debug_flags.rfind (condr); - const ssize_t pc = debug_flags.rfind (condc); - const ssize_t pe = debug_flags.rfind (conde); - const ssize_t pmax = std::max (pr, std::max (std::max (pa, pn), std::max (pc, pe))); - if (pn == pmax || pr == pmax) - return "false"; // found no key or ':none:' or ':no-key:' - if (pa == pmax || pc == pmax) - return "true"; // last setting is ':key:' or ':all:' - // pe == pmax, assignment via equal sign - const ssize_t pv = pe + conde.size(); - const ssize_t pw = debug_flags.find (":", pv); - const std::string value = debug_flags.substr (pv, pw < 0 ? pw : pw - pv); - return value; + const std::string_view sv = string_option_find_value (getenv_ase_debug(), conditional, "", "", true); + return String (sv); } /// Print a debug message, called from ::Ase::debug(). From 7a2843132d05ed0ff28acb8ca87a1cffb74fa92f Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sun, 15 Oct 2023 16:53:19 +0200 Subject: [PATCH 43/50] ASE: utils: export ase_fatal_warnings Signed-off-by: Tim Janik --- ase/utils.cc | 2 +- ase/utils.hh | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ase/utils.cc b/ase/utils.cc index e097daa0..d65da09c 100644 --- a/ase/utils.cc +++ b/ase/utils.cc @@ -16,7 +16,7 @@ namespace Ase { // == Debugging == bool ase_debugging_enabled = true; -static bool ase_fatal_warnings = false; +bool ase_fatal_warnings = false; static const char* getenv_ase_debug() { diff --git a/ase/utils.hh b/ase/utils.hh index 6df3f37e..6886d92b 100644 --- a/ase/utils.hh +++ b/ase/utils.hh @@ -172,6 +172,8 @@ void diag_message (uint8 code, const std::string &message); /// Global boolean to reduce debugging penalty where possible extern bool ase_debugging_enabled; +/// Global boolean to cause the program to abort on warnings. +extern bool ase_fatal_warnings; /// Check if any kind of debugging is enabled by $ASE_DEBUG. inline bool ASE_ALWAYS_INLINE ASE_PURE From 2783e6b07d6f182abc6fe18b167bba53143f8301 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sun, 15 Oct 2023 16:54:50 +0200 Subject: [PATCH 44/50] ASE: main: remove feature toggles, use ase_fatal_warnings Signed-off-by: Tim Janik --- ase/main.cc | 74 ++--------------------------------------------------- ase/main.hh | 6 ----- 2 files changed, 2 insertions(+), 78 deletions(-) diff --git a/ase/main.cc b/ase/main.cc index 66563156..7ad5c583 100644 --- a/ase/main.cc +++ b/ase/main.cc @@ -87,53 +87,6 @@ main_rt_jobs_process() } } -// == Feature Toggles == -/// Find @a feature in @a config, return its value or @a fallback. -String -feature_toggle_find (const String &config, const String &feature, const String &fallback) -{ - String haystack = ":" + config + ":"; - String needle0 = ":no-" + feature + ":"; - String needle1 = ":" + feature + ":"; - String needle2 = ":" + feature + "="; - const char *n0 = strrstr (haystack.c_str(), needle0.c_str()); - const char *n1 = strrstr (haystack.c_str(), needle1.c_str()); - const char *n2 = strrstr (haystack.c_str(), needle2.c_str()); - if (n0 && (!n1 || n0 > n1) && (!n2 || n0 > n2)) - return "0"; // ":no-feature:" is the last toggle in config - if (n1 && (!n2 || n1 > n2)) - return "1"; // ":feature:" is the last toggle in config - if (!n2) - return fallback; // no "feature" variant found - const char *value = n2 + strlen (needle2.c_str()); - const char *end = strchr (value, ':'); - return end ? String (value, end - value) : String (value); -} - -/// Check for @a feature in @a config, if @a feature is empty, checks for *any* feature. -bool -feature_toggle_bool (const char *config, const char *feature) -{ - if (feature && feature[0]) - return string_to_bool (feature_toggle_find (config ? config : "", feature)); - // check if *any* feature is enabled in config - if (!config || !config[0]) - return false; - const size_t l = strlen (config); - for (size_t i = 0; i < l; i++) - if (config[i] && !strchr (": \t\n\r=", config[i])) - return true; // found *some* non-space and non-separator config item - return false; // just whitespace -} - -/// Check if `feature` is enabled via $ASE_FEATURE. -bool -feature_check (const char *feature) -{ - const char *const asefeature = getenv ("ASE_FEATURE"); - return asefeature ? feature_toggle_bool (asefeature, feature) : false; -} - // == MainConfig and arguments == static void print_usage (bool help) @@ -176,7 +129,6 @@ parse_args (int *argcp, char **argv) config.jsonapi_logflags |= debug_key_enabled ("jsbin") ? jsbin_logflags : 0; config.jsonapi_logflags |= debug_key_enabled ("jsipc") ? jsipc_logflags : 0; } - config.fatal_warnings = feature_check ("fatal-warnings"); bool norc = false; bool sep = false; // -- separator @@ -186,7 +138,7 @@ parse_args (int *argcp, char **argv) if (sep) config.args.push_back (argv[i]); else if (strcmp (argv[i], "--fatal-warnings") == 0 || strcmp (argv[i], "--g-fatal-warnings") == 0) - config.fatal_warnings = true; + ase_fatal_warnings = true; else if (strcmp ("--disable-randomization", argv[i]) == 0) config.allow_randomization = false; else if (strcmp ("--norc", argv[i]) == 0) @@ -206,7 +158,7 @@ parse_args (int *argcp, char **argv) else if (strcmp ("--check", argv[i]) == 0) { config.mode = MainConfig::CHECK_INTEGRITY_TESTS; - config.fatal_warnings = true; + ase_fatal_warnings = true; } else if (argv[i] == String ("--blake3") && i + 1 < size_t (argc)) { @@ -665,28 +617,6 @@ job_queue_tests() assert_return (seen_deleter == true); } -TEST_INTEGRITY (test_feature_toggles); -static void -test_feature_toggles() -{ - String r; - r = feature_toggle_find ("a:b", "a"); TCMP (r, ==, "1"); - r = feature_toggle_find ("a:b", "b"); TCMP (r, ==, "1"); - r = feature_toggle_find ("a:b", "c"); TCMP (r, ==, "0"); - r = feature_toggle_find ("a:b", "c", "7"); TCMP (r, ==, "7"); - r = feature_toggle_find ("a:no-b", "b"); TCMP (r, ==, "0"); - r = feature_toggle_find ("no-a:b", "a"); TCMP (r, ==, "0"); - r = feature_toggle_find ("no-a:b:a", "a"); TCMP (r, ==, "1"); - r = feature_toggle_find ("no-a:b:a=5", "a"); TCMP (r, ==, "5"); - r = feature_toggle_find ("no-a:b:a=5:c", "a"); TCMP (r, ==, "5"); - bool b; - b = feature_toggle_bool ("", "a"); TCMP (b, ==, false); - b = feature_toggle_bool ("a:b:c", "a"); TCMP (b, ==, true); - b = feature_toggle_bool ("no-a:b:c", "a"); TCMP (b, ==, false); - b = feature_toggle_bool ("no-a:b:a=5:c", "b"); TCMP (b, ==, true); - b = feature_toggle_bool ("x", ""); TCMP (b, ==, true); // *any* feature? -} - struct JWalker : Jsonipc::ClassWalker { struct Class { String name; int depth = 0; Class *base = nullptr; StringS derived; }; std::map classmap; diff --git a/ase/main.hh b/ase/main.hh index bff7745a..6914d924 100644 --- a/ase/main.hh +++ b/ase/main.hh @@ -19,7 +19,6 @@ struct MainConfig { std::vector args; uint16 websocket_port = 0; int jsonapi_logflags = 1; - bool fatal_warnings = false; bool allow_randomization = true; bool list_drivers = false; bool play_autostart = false; @@ -29,11 +28,6 @@ struct MainConfig { }; extern const MainConfig &main_config; -// == Feature Toggles == -String feature_toggle_find (const String &config, const String &feature, const String &fallback = "0"); -bool feature_toggle_bool (const char *config, const char *feature); -bool feature_check (const char *feature); - // == Jobs & main loop == extern MainLoopP main_loop; void main_loop_wakeup (); From 917085b5d46d2fa22547af2ccf86cdf1cb0c96c8 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sun, 15 Oct 2023 16:55:47 +0200 Subject: [PATCH 45/50] ASE: cxxaux.cc: use ase_fatal_warnings Signed-off-by: Tim Janik --- ase/cxxaux.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ase/cxxaux.cc b/ase/cxxaux.cc index 815c21ad..9284d007 100644 --- a/ase/cxxaux.cc +++ b/ase/cxxaux.cc @@ -1,6 +1,6 @@ // This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 #include "cxxaux.hh" -#include "main.hh" // main_config +#include "utils.hh" // ase_fatal_warnings #include "backtrace.hh" #include // abi::__cxa_demangle #include @@ -45,7 +45,7 @@ assertion_failed (const std::string &msg, const char *file, int line, const char ASE_PRINT_BACKTRACE (__FILE__, __LINE__, __func__); else if (debug_key_enabled ("break")) breakpoint(); - if (main_config.fatal_warnings) + if (ase_fatal_warnings) raise (SIGQUIT); } From 75ff901b723834e1091dbc7b8bd3b2ee310864c1 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sun, 15 Oct 2023 16:56:08 +0200 Subject: [PATCH 46/50] ASE: processor.cc: use string_option_find() Signed-off-by: Tim Janik --- ase/processor.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ase/processor.cc b/ase/processor.cc index 6ad01774..16745f8f 100644 --- a/ase/processor.cc +++ b/ase/processor.cc @@ -164,7 +164,7 @@ construct_hints (String hints, double pmin, double pmax, const String &more = "" if (hints.back() != ':') hints = hints + ":"; for (const auto &s : string_split (more)) - if (!s.empty() && "" == feature_toggle_find (hints, s, "")) + if (!s.empty() && "" == string_option_find (hints, s, "")) hints += s + ":"; if (hints[0] != ':') hints = ":" + hints; From 45c8fb74bcb4bab61c2c9b8c9c630741943e6cd4 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sun, 15 Oct 2023 16:56:34 +0200 Subject: [PATCH 47/50] ASE: testing.cc: use string_option_find_value() Signed-off-by: Tim Janik --- ase/testing.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ase/testing.cc b/ase/testing.cc index d30bcd39..b90bd444 100644 --- a/ase/testing.cc +++ b/ase/testing.cc @@ -170,14 +170,14 @@ test_output (int kind, const String &msg) bool slow() { - static bool cached_slow = feature_toggle_bool (getenv ("ASE_TEST"), "slow"); + static bool cached_slow = string_to_bool (String (string_option_find_value (getenv ("ASE_TEST"), "slow", "0", "0", true))); return cached_slow; } bool verbose() { - static bool cached_verbose = feature_toggle_bool (getenv ("ASE_TEST"), "verbose"); + static bool cached_verbose = string_to_bool (String (string_option_find_value (getenv ("ASE_TEST"), "verbose", "0", "0", true))); return cached_verbose; } From 50e54a41c58cefb6e9789cfdfc8efc79c245a294 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sun, 15 Oct 2023 16:57:22 +0200 Subject: [PATCH 48/50] ASE: strings: add fallback to string_option_find(), remove string_option_get() Signed-off-by: Tim Janik --- ase/strings.cc | 28 +++++++++++++--------------- ase/strings.hh | 3 +-- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/ase/strings.cc b/ase/strings.cc index dc987210..d43c1e3d 100644 --- a/ase/strings.cc +++ b/ase/strings.cc @@ -1317,7 +1317,7 @@ string_option_find_value (const char *string, const char *feature, const String return string_option_find_value (string, feature, fallback.c_str(), denied.c_str(), matchallnone ? 3 : 2); } -/// Find `feature` in an `optionlist`, return its value or `fallback`. +/// Retrieve the option value from an options list separated by ':' or ';' or `fallback`. String string_option_find (const String &optionlist, const String &feature, const String &fallback) { @@ -1332,14 +1332,6 @@ string_option_check (const String &optionlist, const String &feature) return string_to_bool (string_option_find (optionlist, feature, "0"), true); } -/// Retrieve the option value from an options list separated by ':' or ';'. -String -string_option_get (const String &optionlist, const String &feature) -{ - std::string_view sv = string_option_find_value (optionlist.data(), feature.data(), "", "", 3); - return { sv.data(), sv.size() }; -} - // == Strings == Strings::Strings (CS &s1) { push_back (s1); } @@ -1599,10 +1591,10 @@ string_tests() TASSERT (string_option_check (":foo7:", "foo9") == false); TASSERT (string_option_check (":bar:", "bar") == true); TASSERT (string_option_check (":bar=:", "bar") == true); - TASSERT (string_option_get (":bar:", "bar") == "1"); + TASSERT (string_option_find (":bar:", "bar") == "1"); TASSERT (string_option_check (":bar=0:", "bar") == false); - TASSERT (string_option_get (":bar=0:", "bar") == "0"); - TASSERT (string_option_get (":no-bar:", "bar") == ""); + TASSERT (string_option_find (":bar=0:", "bar") == "0"); + TASSERT (string_option_find (":no-bar:", "bar") == ""); TASSERT (string_option_check (":bar=no:", "bar") == false); TASSERT (string_option_check (":bar=false:", "bar") == false); TASSERT (string_option_check (":bar=off:", "bar") == false); @@ -1648,13 +1640,19 @@ string_tests() String r; r = string_option_find ("a:b", "a"); TCMP (r, ==, "1"); r = string_option_find ("a:b", "b"); TCMP (r, ==, "1"); - r = string_option_find ("a:b", "c"); TCMP (r, ==, "0"); + r = string_option_find ("a:b", "c", "0"); TCMP (r, ==, "0"); r = string_option_find ("a:b", "c", "7"); TCMP (r, ==, "7"); - r = string_option_find ("a:no-b", "b"); TCMP (r, ==, "0"); - r = string_option_find ("no-a:b", "a"); TCMP (r, ==, "0"); + r = string_option_find ("a:no-b", "b", "0"); TCMP (r, ==, "0"); + r = string_option_find ("no-a:b", "a", "0"); TCMP (r, ==, "0"); r = string_option_find ("no-a:b:a", "a"); TCMP (r, ==, "1"); r = string_option_find ("no-a:b:a=5", "a"); TCMP (r, ==, "5"); r = string_option_find ("no-a:b:a=5:c", "a"); TCMP (r, ==, "5"); + bool b; + b = string_option_check ("", "a"); TCMP (b, ==, false); + b = string_option_check ("a:b:c", "a"); TCMP (b, ==, true); + b = string_option_check ("no-a:b:c", "a"); TCMP (b, ==, false); + b = string_option_check ("no-a:b:a=5:c", "b"); TCMP (b, ==, true); + b = string_option_check ("x:all", ""); TCMP (b, ==, false); // must have feature? TASSERT (typeid_name() == "int"); TASSERT (typeid_name() == "bool"); TASSERT (typeid_name<::Ase::Strings>() == "Ase::Strings"); diff --git a/ase/strings.hh b/ase/strings.hh index cb0968b4..8148e7b3 100644 --- a/ase/strings.hh +++ b/ase/strings.hh @@ -139,8 +139,7 @@ template<> inline String string_from_type (String value) // == String Options == bool string_option_check (const String &optionlist, const String &feature); -String string_option_get (const String &optionlist, const String &feature); -String string_option_find (const String &optionlist, const String &feature, const String &fallback = "0"); +String string_option_find (const String &optionlist, const String &feature, const String &fallback = ""); std::string_view string_option_find_value (const char *string, const char *feature, const String &fallback, const String &denied, bool matchallnone); // == Generic Key-Value-Pairs == From 59bf977d6b8034782bc28effaacef750f08f4822 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Mon, 16 Oct 2023 01:44:18 +0200 Subject: [PATCH 49/50] ASE: shorten Parameter method ident() Signed-off-by: Tim Janik --- ase/api.hh | 2 +- ase/clapdevice.cc | 30 +++++++++++++++--------------- ase/gadget.cc | 10 +++++----- ase/parameter.hh | 2 +- ase/processor.cc | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/ase/api.hh b/ase/api.hh index 7579347f..8e0c64cf 100644 --- a/ase/api.hh +++ b/ase/api.hh @@ -134,7 +134,7 @@ class Property : public virtual Emittable { protected: virtual ~Property () = 0; public: - virtual String identifier () = 0; ///< Unique name (per owner) of this Property. + virtual String ident () = 0; ///< Unique name (per owner) of this Property. virtual String label () = 0; ///< Preferred user interface name. virtual String nick () = 0; ///< Abbreviated user interface name, usually not more than 6 characters. virtual String unit () = 0; ///< Units of the values within range. diff --git a/ase/clapdevice.cc b/ase/clapdevice.cc index 9fdedfb2..73eff318 100644 --- a/ase/clapdevice.cc +++ b/ase/clapdevice.cc @@ -27,21 +27,21 @@ struct ClapPropertyImpl : public Property, public virtual EmittableImpl { String ident_, label_, module_; double min_value = NAN, max_value = NAN, default_value = NAN; public: - String identifier () override { return ident_; } - String label () override { return label_; } - String nick () override { return parameter_guess_nick (label_); } - String unit () override { return ""; } - String hints () override { return ClapParamInfo::hints_from_param_info_flags (flags); } - String group () override { return module_; } - String blurb () override { return ""; } - String descr () override { return ""; } - double get_min () override { return min_value; } - double get_max () override { return max_value; } - double get_step () override { return is_stepped() ? 1 : 0; } - bool is_numeric () override { return true; } - bool is_stepped () { return strstr (hints().c_str(), ":stepped:"); } - void reset () override { set_value (default_value); } - ClapPropertyImpl (ClapDeviceImplP device, const ClapParamInfo info) : + String ident () override { return ident_; } + String label () override { return label_; } + String nick () override { return parameter_guess_nick (label_); } + String unit () override { return ""; } + String hints () override { return ClapParamInfo::hints_from_param_info_flags (flags); } + String group () override { return module_; } + String blurb () override { return ""; } + String descr () override { return ""; } + double get_min () override { return min_value; } + double get_max () override { return max_value; } + double get_step () override { return is_stepped() ? 1 : 0; } + bool is_numeric () override { return true; } + bool is_stepped () { return strstr (hints().c_str(), ":stepped:"); } + void reset () override { set_value (default_value); } + ClapPropertyImpl (ClapDeviceImplP device, const ClapParamInfo info) : device_ (device) { param_id = info.param_id; diff --git a/ase/gadget.cc b/ase/gadget.cc index 6f59a8cd..e23043a6 100644 --- a/ase/gadget.cc +++ b/ase/gadget.cc @@ -89,12 +89,12 @@ GadgetImpl::serialize (WritNode &xs) if (xs.in_save() && string_option_check (hints, "r")) { Value v = p->get_value(); - xs[p->identifier()] & v; + xs[p->ident()] & v; } - if (xs.in_load() && string_option_check (hints, "w") && xs.has (p->identifier())) + if (xs.in_load() && string_option_check (hints, "w") && xs.has (p->ident())) { Value v; - xs[p->identifier()] & v; + xs[p->ident()] & v; p->set_value (v); } } @@ -176,7 +176,7 @@ Gadget::list_properties () StringS names; names.reserve (props.size()); for (const PropertyP &prop : props) - names.push_back (prop->identifier()); + names.push_back (prop->ident()); return names; } @@ -184,7 +184,7 @@ PropertyP Gadget::access_property (String ident) { for (const auto &p : access_properties()) - if (p->identifier() == ident) + if (p->ident() == ident) return p; return {}; } diff --git a/ase/parameter.hh b/ase/parameter.hh index e19201e9..dfe82553 100644 --- a/ase/parameter.hh +++ b/ase/parameter.hh @@ -82,7 +82,7 @@ class ParameterProperty : public EmittableImpl, public virtual Property { protected: ParameterC parameter_; public: - String identifier () override { return parameter_->cident; } + String ident () override { return parameter_->cident; } String label () override { return parameter_->label(); } String nick () override { return parameter_->nick(); } String unit () override { return parameter_->unit(); } diff --git a/ase/processor.cc b/ase/processor.cc index 16745f8f..c7c43ee1 100644 --- a/ase/processor.cc +++ b/ase/processor.cc @@ -1034,7 +1034,7 @@ class AudioPropertyImpl : public Property, public virtual EmittableImpl { double inflight_value_ = 0; std::atomic inflight_counter_ = 0; public: - String identifier () override { return parameter_->ident(); } + String ident () override { return parameter_->ident(); } String label () override { return parameter_->label(); } String nick () override { return parameter_->nick(); } String unit () override { return parameter_->unit(); } From cbdc9fc0fa2c9f7da7ebd30bed612006a381e7d5 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Mon, 16 Oct 2023 01:44:41 +0200 Subject: [PATCH 50/50] UI: util.js: call prop->ident() Signed-off-by: Tim Janik --- ui/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/util.js b/ui/util.js index dc974f20..ac05209d 100644 --- a/ui/util.js +++ b/ui/util.js @@ -928,7 +928,7 @@ export async function extend_property (prop, disconnector = undefined, augment = const notify_cbs = []; // custom notify callbacks const xprop = { hints_: prop.hints(), - ident_: prop.identifier(), + ident_: prop.ident(), is_numeric_: prop.is_numeric(), label_: prop.label(), nick_: prop.nick(),