]> git.alsa-project.org Git - alsa-lib.git/commitdiff
ucm: add Repeat block - repetitive pattern substitution (Syntax 9)
authorJaroslav Kysela <perex@perex.cz>
Fri, 6 Feb 2026 12:43:11 +0000 (13:43 +0100)
committerJaroslav Kysela <perex@perex.cz>
Fri, 6 Feb 2026 18:32:28 +0000 (19:32 +0100)
Implements Repeat blocks for generating repetitive configuration patterns
with variable substitution. This feature allows applying a configuration
block multiple times with different variable values, significantly reducing
duplication in UCM configuration files.

iterator abstraction allows easy extension for future pattern types.

Example:

  Repeat.VolumeInit {
    Pattern {
      Variable 'ch'
      Type Integer
      First 0
      Last 7
      Step 1
    }
    Apply {
      cset "name='PCM Channel ${var:ch} Volume' 100%"
    }
  }

Signed-off-by: Jaroslav Kysela <perex@perex.cz>
src/ucm/Makefile.am
src/ucm/parser.c
src/ucm/ucm_confdoc.h
src/ucm/ucm_local.h
src/ucm/ucm_repeat.c [new file with mode: 0644]

index 7108968f1988368fbd7cb049d2cd45f91eddc609..f13ec118153131f4f3f7811969fe38b08e8a3b8f 100644 (file)
@@ -1,7 +1,7 @@
 EXTRA_LTLIBRARIES = libucm.la
 
 libucm_la_SOURCES = utils.c parser.c ucm_cond.c ucm_subs.c ucm_include.c \
-                   ucm_regex.c ucm_exec.c main.c
+                   ucm_regex.c ucm_repeat.c ucm_exec.c main.c
 
 noinst_HEADERS = ucm_local.h ucm_confdoc.h
 
index a7ef1b1112b418cfd7e6cae67a824f7669af5556..3212fda09f926b9ec501ae29050e935234bba365 100644 (file)
@@ -729,9 +729,9 @@ int uc_mgr_evaluate_inplace(snd_use_case_mgr_t *uc_mgr,
                            snd_config_t *cfg)
 {
        long iterations = 10000;
-       int err1 = 0, err2 = 0, err3 = 0, err4 = 0, err5 = 0;
+       int err1 = 0, err2 = 0, err3 = 0, err4 = 0, err5 = 0, err6 = 0;
 
-       while (err1 == 0 || err2 == 0 || err3 == 0 || err4 == 0 || err5 == 0) {
+       while (err1 == 0 || err2 == 0 || err3 == 0 || err4 == 0 || err5 == 0 || err6 == 0) {
                if (iterations == 0) {
                        snd_error(UCM, "Maximal inplace evaluation iterations number reached (recursive references?)");
                        return -EINVAL;
@@ -768,6 +768,9 @@ int uc_mgr_evaluate_inplace(snd_use_case_mgr_t *uc_mgr,
                err5 = evaluate_condition(uc_mgr, cfg);
                if (err5 < 0)
                        return err5;
+               err6 = uc_mgr_evaluate_repeat(uc_mgr, cfg);
+               if (err6 < 0)
+                       return err6;
        }
        return 0;
 }
index 0a344b7ec14b12088415d19a07c8917bd53991a2..d5bd3484ed17019ee8b52e34c1a0be6812daeb4a 100644 (file)
@@ -1012,6 +1012,136 @@ SectionDevice."HDMI:LowRate" {
 
 This creates two devices: **HDMI:LowRate** (48kHz) and **HDMI:HighRate** (192kHz).
 
+### Repetitive Pattern Substitution
+
+Starting with **Syntax 9**, the UCM configuration supports the **Repeat** block for generating
+repetitive configuration patterns. This feature allows you to apply a configuration block multiple
+times with different variable values, reducing duplication in configuration files.
+
+The **Repeat** block contains two main components:
+
+1. **Pattern**: Defines the iteration pattern (how many times to repeat and what values to use)
+2. **Apply**: The configuration block to be applied on each iteration
+
+#### Pattern Types
+
+The **Pattern** block supports two types: **Integer** and **Array**.
+
+**Integer Pattern**: Iterates over a range of integer values
+
+~~~{.html}
+Repeat.MyRepeat {
+  Pattern {
+    Variable 'ChannelNum'
+    Type Integer
+    First 0
+    Last 15
+    Step 2
+  }
+  Apply {
+    ... configuration using ${var:ChannelNum} ...
+  }
+}
+~~~
+
+Fields for Integer pattern:
+- **Variable**: Name of the variable to substitute (without ${var:} prefix)
+- **Type**: Must be "Integer"
+- **First**: Starting value (integer)
+- **Last**: Ending value (integer)
+- **Step**: Increment value (integer, default 1)
+
+The iteration supports reverse order automatically when First is greater than Last.
+
+**Array Pattern**: Iterates over a list of string values
+
+~~~{.html}
+Repeat.DeviceList {
+  Pattern {
+    Variable 'DevName'
+    Type Array
+    Array [
+      "Speaker"
+      "Headphones"
+      "HDMI"
+    ]
+  }
+  Apply {
+    ... configuration using ${var:DevName} ...
+  }
+}
+~~~
+
+Fields for Array pattern:
+- **Variable**: Name of the variable to substitute (without ${var:} prefix)
+- **Type**: Must be "Array"
+- **Array**: A compound node containing string values to iterate over
+
+**String Pattern**: Pattern can also be specified as a string that will be parsed as a
+configuration block. This allows for dynamic pattern generation.
+
+~~~{.html}
+Repeat.Dynamic {
+  Pattern "
+    Variable 'Index'
+    Type Integer
+    First 1
+    Last 4
+  "
+  Apply {
+    ... configuration using ${var:Index} ...
+  }
+}
+~~~
+
+#### Complete Example
+
+Example using Integer pattern to create multiple similar control settings:
+
+~~~{.html}
+EnableSequence [
+  Repeat.VolumeInit {
+    Pattern {
+      Variable 'ch'
+      Type Integer
+      First 0
+      Last 7
+    }
+    Apply {
+      cset "name='PCM Channel ${var:ch} Volume' 100%"
+    }
+  }
+]
+~~~
+
+This generates 8 cset commands for channels 0 through 7.
+
+Example using Array pattern for different device configurations:
+
+~~~{.html}
+Repeat.Devices {
+  Pattern {
+    Variable 'output'
+    Type Array
+    Array [
+      "Speaker"
+      "Headphones"
+      "LineOut"
+    ]
+  }
+  Apply {
+    SectionDevice."${var:output}" {
+      Comment "${var:output} Output"
+      EnableSequence [
+        cset "name='${var:output} Switch' on"
+      ]
+    }
+  }
+}
+~~~
+
+This creates three SectionDevice blocks for Speaker, Headphones, and LineOut.
+
 */
 
 /**
index 957cb751c91055d6397c49e85f752ab05fe8e648..5ac4d3c4d753d4cd1c0cf4fb4724b0424725cac1 100644 (file)
@@ -384,6 +384,9 @@ int uc_mgr_evaluate_condition(snd_use_case_mgr_t *uc_mgr,
                              snd_config_t *parent,
                              snd_config_t *cond);
 
+int uc_mgr_evaluate_repeat(snd_use_case_mgr_t *uc_mgr,
+                           snd_config_t *cfg);
+
 int uc_mgr_define_regex(snd_use_case_mgr_t *uc_mgr,
                        const char *name,
                        snd_config_t *eval);
diff --git a/src/ucm/ucm_repeat.c b/src/ucm/ucm_repeat.c
new file mode 100644 (file)
index 0000000..e3e08e0
--- /dev/null
@@ -0,0 +1,401 @@
+/*
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2 of the License, or (at your option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *  Copyright (C) 2026 Red Hat Inc.
+ *  Authors: Jaroslav Kysela <perex@perex.cz>
+ */
+
+#include "ucm_local.h"
+
+/*
+ * get_string helper
+ */
+static int get_string(snd_config_t *compound, const char *key, const char **str)
+{
+       snd_config_t *node;
+       int err;
+
+       err = snd_config_search(compound, key, &node);
+       if (err < 0)
+               return err;
+       return snd_config_get_string(node, str);
+}
+
+/*
+ * get_integer helper
+ */
+static int get_integer(snd_config_t *compound, const char *key, long long *val)
+{
+       snd_config_type_t t;
+       snd_config_t *node;
+       const char *str;
+       int err;
+
+       err = snd_config_search(compound, key, &node);
+       if (err < 0)
+               return err;
+       t = snd_config_get_type(node);
+       if (t == SND_CONFIG_TYPE_INTEGER) {
+               long i;
+               err = snd_config_get_integer(node, &i);
+               if (err >= 0)
+                       *val = i;
+       } else if (t == SND_CONFIG_TYPE_INTEGER64) {
+               err = snd_config_get_integer64(node, val);
+       } else {
+               err = snd_config_get_string(node, &str);
+               if (err < 0)
+                       return err;
+               err = safe_strtoll(str, val);
+       }
+       if (err < 0)
+               return -EINVAL;
+
+       return 0;
+}
+
+/*
+ * Repeat pattern iterator
+ */
+struct repeat_iterator {
+       const char *var_name;
+
+       union {
+               struct {
+                       long long current;
+                       long long last;
+                       long long step;
+                       int iteration;
+                       char value_buf[32];
+               } integer;
+
+               struct {
+                       snd_config_iterator_t pos;
+                       snd_config_iterator_t end;
+                       snd_config_t *array;
+                       char *value_str;
+               } array;
+       } u;
+
+       int (*init)(struct repeat_iterator *it, snd_config_t *pattern);
+       int (*next)(struct repeat_iterator *it, const char **value);
+       void (*done)(struct repeat_iterator *it);
+};
+
+/*
+ * Integer pattern iterator - initialization
+ */
+static int repeat_integer_init(struct repeat_iterator *it, snd_config_t *pattern)
+{
+       long long first;
+       int err;
+
+       err = get_integer(pattern, "First", &first);
+       if (err < 0) {
+               snd_error(UCM, "Repeat.Pattern.First is required for Integer type");
+               return -EINVAL;
+       }
+
+       err = get_integer(pattern, "Last", &it->u.integer.last);
+       if (err < 0) {
+               snd_error(UCM, "Repeat.Pattern.Last is required for Integer type");
+               return -EINVAL;
+       }
+
+       err = get_integer(pattern, "Step", &it->u.integer.step);
+       if (err == -ENOENT) {
+               it->u.integer.step = 1;
+       } else if (err < 0) {
+               snd_error(UCM, "Repeat.Pattern.Step parse error");
+               return -EINVAL;
+       }
+
+       if (it->u.integer.step == 0) {
+               snd_error(UCM, "Repeat.Pattern.Step cannot be zero");
+               return -EINVAL;
+       }
+
+       it->u.integer.current = first;
+       it->u.integer.iteration = 0;
+       return 0;
+}
+
+/*
+ * Integer pattern iterator - get next value
+ * Returns: 1 if value available, 0 if end of iteration, negative on error
+ */
+static int repeat_integer_next(struct repeat_iterator *it, const char **value)
+{
+       const int max_iterations = 10000;
+       int has_value;
+
+       if (it->u.integer.iteration++ > max_iterations) {
+               snd_error(UCM, "Repeat iteration limit exceeded");
+               return -EINVAL;
+       }
+
+       if (it->u.integer.step > 0)
+               has_value = (it->u.integer.current <= it->u.integer.last);
+       else
+               has_value = (it->u.integer.current >= it->u.integer.last);
+
+       if (!has_value)
+               return 0;
+
+       snprintf(it->u.integer.value_buf, sizeof(it->u.integer.value_buf), "%lld", it->u.integer.current);
+       *value = it->u.integer.value_buf;
+
+       it->u.integer.current += it->u.integer.step;
+       return 1;
+}
+
+/*
+ * Array pattern iterator - initialization
+ */
+static int repeat_array_init(struct repeat_iterator *it, snd_config_t *pattern)
+{
+       int err;
+
+       err = snd_config_search(pattern, "Array", &it->u.array.array);
+       if (err < 0) {
+               snd_error(UCM, "Repeat.Pattern.Array is required for Array type");
+               return -EINVAL;
+       }
+
+       if (snd_config_get_type(it->u.array.array) != SND_CONFIG_TYPE_COMPOUND) {
+               snd_error(UCM, "Repeat.Pattern.Array must be a compound");
+               return -EINVAL;
+       }
+
+       it->u.array.pos = snd_config_iterator_first(it->u.array.array);
+       it->u.array.end = snd_config_iterator_end(it->u.array.array);
+       it->u.array.value_str = NULL;
+       return 0;
+}
+
+/*
+ * Array pattern iterator - get next value
+ * Returns: 1 if value available, 0 if end of iteration, negative on error
+ */
+static int repeat_array_next(struct repeat_iterator *it, const char **value)
+{
+       snd_config_t *n;
+       int err;
+
+       /* Free previous value string */
+       free(it->u.array.value_str);
+       it->u.array.value_str = NULL;
+
+       if (it->u.array.pos == it->u.array.end)
+               return 0;
+
+       n = snd_config_iterator_entry(it->u.array.pos);
+       it->u.array.pos = snd_config_iterator_next(it->u.array.pos);
+
+       err = snd_config_get_ascii(n, &it->u.array.value_str);
+       if (err < 0) {
+               snd_error(UCM, "Repeat.Pattern.Array element conversion error");
+               return -EINVAL;
+       }
+
+       *value = it->u.array.value_str;
+       return 1;
+}
+
+/*
+ * Array pattern iterator - cleanup
+ */
+static void repeat_array_done(struct repeat_iterator *it)
+{
+       free(it->u.array.value_str);
+       it->u.array.value_str = NULL;
+}
+
+/*
+ * Evaluate repeat pattern using iterator
+ */
+static int evaluate_repeat_pattern(snd_use_case_mgr_t *uc_mgr,
+                                   snd_config_t *cfg,
+                                   snd_config_t *pattern,
+                                   snd_config_t *apply,
+                                   struct repeat_iterator *it)
+{
+       snd_config_t *apply_copy;
+       const char *value;
+       int err, ret;
+
+       err = it->init(it, pattern);
+       if (err < 0)
+               return err;
+
+       while ((ret = it->next(it, &value)) > 0) {
+               err = uc_mgr_set_variable(uc_mgr, it->var_name, value);
+               if (err < 0)
+                       goto __error;
+
+               err = snd_config_copy(&apply_copy, apply);
+               if (err < 0)
+                       goto __var_error;
+
+               err = uc_mgr_evaluate_inplace(uc_mgr, apply_copy);
+               if (err < 0)
+                       goto __copy_error;
+
+               err = uc_mgr_config_tree_merge(uc_mgr, cfg, apply_copy, NULL, NULL);
+               snd_config_delete(apply_copy);
+               if (err < 0)
+                       goto __var_error;
+       }
+
+       if (ret < 0) {
+               err = ret;
+               goto __var_error;
+       }
+
+       uc_mgr_delete_variable(uc_mgr, it->var_name);
+
+       if (it->done)
+               it->done(it);
+
+       return 0;
+
+__copy_error:
+       snd_config_delete(apply_copy);
+__var_error:
+       uc_mgr_delete_variable(uc_mgr, it->var_name);
+__error:
+       if (it->done)
+               it->done(it);
+       return err;
+}
+
+/*
+ * Evaluate repeat (in-place)
+ */
+int uc_mgr_evaluate_repeat(snd_use_case_mgr_t *uc_mgr, snd_config_t *cfg)
+{
+       snd_config_iterator_t i, next;
+       snd_config_t *repeat_blocks, *n, *pattern = NULL, *pattern_cfg = NULL;
+       const char *id;
+       int err;
+
+       err = snd_config_search(cfg, "Repeat", &repeat_blocks);
+       if (err == -ENOENT)
+               return 1;
+       if (err < 0)
+               return err;
+
+       if (uc_mgr->conf_format < 9) {
+               snd_error(UCM, "Repeat is supported in v9+ syntax");
+               err = -EINVAL;
+               goto __error;
+       }
+
+       if (snd_config_get_type(repeat_blocks) != SND_CONFIG_TYPE_COMPOUND) {
+               snd_error(UCM, "Repeat must be a compound");
+               err = -EINVAL;
+               goto __error;
+       }
+
+       snd_config_for_each(i, next, repeat_blocks) {
+               snd_config_t *apply;
+               struct repeat_iterator it;
+               const char *var_name, *type_str;
+
+               n = snd_config_iterator_entry(i);
+
+               if (snd_config_get_id(n, &id) < 0)
+                       continue;
+
+               err = snd_config_search(n, "Pattern", &pattern);
+               if (err < 0) {
+                       snd_error(UCM, "Repeat.%s.Pattern is required", id);
+                       goto __error;
+               }
+
+               if (snd_config_get_type(pattern) == SND_CONFIG_TYPE_STRING) {
+                       const char *pattern_str;
+                       char *pattern_subst = NULL;
+
+                       err = snd_config_get_string(pattern, &pattern_str);
+                       if (err < 0)
+                               goto __error;
+
+                       err = uc_mgr_get_substituted_value(uc_mgr, &pattern_subst, pattern_str);
+                       if (err < 0)
+                               goto __error;
+
+                       err = snd_config_load_string(&pattern_cfg, pattern_subst, 0);
+                       free(pattern_subst);
+                       if (err < 0) {
+                               snd_error(UCM, "Repeat.%s.Pattern string parse error", id);
+                               goto __error;
+                       }
+               } else {
+                       pattern_cfg = pattern;
+               }
+
+               err = get_string(pattern_cfg, "Variable", &var_name);
+               if (err < 0) {
+                       snd_error(UCM, "Repeat.%s.Pattern.Variable is required", id);
+                       goto __pattern_error;
+               }
+
+               err = get_string(pattern_cfg, "Type", &type_str);
+               if (err < 0) {
+                       snd_error(UCM, "Repeat.%s.Pattern.Type is required", id);
+                       goto __pattern_error;
+               }
+
+               err = snd_config_search(n, "Apply", &apply);
+               if (err < 0) {
+                       snd_error(UCM, "Repeat.%s.Apply is required", id);
+                       goto __pattern_error;
+               }
+
+               memset(&it, 0, sizeof(it));
+               it.var_name = var_name;
+
+               if (strcmp(type_str, "Integer") == 0) {
+                       it.init = repeat_integer_init;
+                       it.next = repeat_integer_next;
+                       it.done = NULL;
+               } else if (strcmp(type_str, "Array") == 0) {
+                       it.init = repeat_array_init;
+                       it.next = repeat_array_next;
+                       it.done = repeat_array_done;
+               } else {
+                       snd_error(UCM, "Repeat.%s.Pattern.Type must be 'Integer' or 'Array'", id);
+                       err = -EINVAL;
+                       goto __pattern_error;
+               }
+
+               err = evaluate_repeat_pattern(uc_mgr, cfg, pattern_cfg, apply, &it);
+               if (err < 0)
+                       goto __pattern_error;
+               if (pattern_cfg != pattern) {
+                       snd_config_delete(pattern_cfg);
+                       pattern_cfg = NULL;
+               }
+       }
+
+       err = 0;
+__pattern_error:
+       if (pattern_cfg && pattern_cfg != pattern)
+               snd_config_delete(pattern_cfg);
+__error:
+       snd_config_delete(repeat_blocks);
+       return err;
+}