]> git.alsa-project.org Git - alsa-lib.git/commitdiff
ucm: implement DeviceVariant configuration extension
authorJaroslav Kysela <perex@perex.cz>
Thu, 20 Nov 2025 15:11:32 +0000 (16:11 +0100)
committerJaroslav Kysela <perex@perex.cz>
Thu, 4 Dec 2025 11:11:41 +0000 (12:11 +0100)
It may be useful for the channel count specification for example.

Signed-off-by: Jaroslav Kysela <perex@perex.cz>
src/ucm/parser.c
src/ucm/ucm_confdoc.h
src/ucm/ucm_local.h

index a9be3fead8f28581f76218a1ba573264f3b3aafd..7ab7c99e1ae90b68da696c1d802eddd5c57eb1fa 100644 (file)
@@ -1549,75 +1549,16 @@ static int parse_modifier(snd_use_case_mgr_t *uc_mgr,
 }
 
 /*
- * Parse Device Use Cases
- *
- * # Each device is described in new section. N devices are allowed
- * SectionDevice."Headphones" {
- *     Comment "Headphones connected to 3.5mm jack"
- *
- *     SupportedDevice [
- *             "x"
- *             "y"
- *     ]
- *
- *     ConflictingDevice [
- *             "x"
- *             "y"
- *     ]
- *
- *     EnableSequence [
- *             ....
- *     ]
- *
- *     DisableSequence [
- *             ...
- *     ]
- *
- *      TransitionSequence."ToDevice" [
- *             ...
- *     ]
- *
- *     Value {
- *             PlaybackVolume "name='Master Playback Volume',index=2"
- *             PlaybackSwitch "name='Master Playback Switch',index=2"
- *     }
- * }
+ * Parse device configuration fields
  */
-static int parse_device(snd_use_case_mgr_t *uc_mgr,
-                       snd_config_t *cfg,
-                       void *data1, void *data2)
+static int parse_device_fields(snd_use_case_mgr_t *uc_mgr,
+                              snd_config_t *cfg,
+                              struct use_case_device *device)
 {
-       struct use_case_verb *verb = data1;
-       char *name;
-       struct use_case_device *device;
        snd_config_iterator_t i, next;
        snd_config_t *n;
        int err;
 
-       if (parse_get_safe_name(uc_mgr, cfg, data2, &name) < 0)
-               return -EINVAL;
-
-       device = calloc(1, sizeof(*device));
-       if (device == NULL) {
-               free(name);
-               return -ENOMEM;
-       }
-       INIT_LIST_HEAD(&device->enable_list);
-       INIT_LIST_HEAD(&device->disable_list);
-       INIT_LIST_HEAD(&device->transition_list);
-       INIT_LIST_HEAD(&device->dev_list.list);
-       INIT_LIST_HEAD(&device->value_list);
-       list_add_tail(&device->list, &verb->device_list);
-       device->name = name;
-       device->orig_name = strdup(name);
-       if (device->orig_name == NULL)
-               return -ENOMEM;
-
-       /* in-place evaluation */
-       err = uc_mgr_evaluate_inplace(uc_mgr, cfg);
-       if (err < 0)
-               return err;
-
        snd_config_for_each(i, next, cfg) {
                const char *id;
                n = snd_config_iterator_entry(i);
@@ -1695,6 +1636,246 @@ static int parse_device(snd_use_case_mgr_t *uc_mgr,
        return 0;
 }
 
+/*
+ * Helper function to copy, evaluate and optionally merge configuration trees.
+ */
+static int uc_mgr_config_copy_eval_merge(snd_use_case_mgr_t *uc_mgr,
+                                        snd_config_t **dst,
+                                        snd_config_t *src,
+                                        snd_config_t *merge_from)
+{
+       snd_config_t *tmp = NULL;
+       int err;
+
+       err = snd_config_copy(&tmp, src);
+       if (err < 0)
+               return err;
+
+       err = uc_mgr_evaluate_inplace(uc_mgr, tmp);
+       if (err < 0) {
+               snd_config_delete(tmp);
+               return err;
+       }
+
+       if (merge_from) {
+               err = uc_mgr_config_tree_merge(uc_mgr, tmp, merge_from, NULL, NULL);
+               if (err < 0) {
+                       snd_config_delete(tmp);
+                       return err;
+               }
+       }
+
+       *dst = tmp;
+       return 0;
+}
+
+/*
+ * Parse Device Use Cases
+ *
+ * # Each device is described in new section. N devices are allowed
+ * SectionDevice."Headphones" {
+ *     Comment "Headphones connected to 3.5mm jack"
+ *
+ *     SupportedDevice [
+ *             "x"
+ *             "y"
+ *     ]
+ *
+ *     ConflictingDevice [
+ *             "x"
+ *             "y"
+ *     ]
+ *
+ *     EnableSequence [
+ *             ....
+ *     ]
+ *
+ *     DisableSequence [
+ *             ...
+ *     ]
+ *
+ *      TransitionSequence."ToDevice" [
+ *             ...
+ *     ]
+ *
+ *     Value {
+ *             PlaybackVolume "name='Master Playback Volume',index=2"
+ *             PlaybackSwitch "name='Master Playback Switch',index=2"
+ *     }
+ * }
+ */
+
+static int parse_device_by_name(snd_use_case_mgr_t *uc_mgr,
+                               snd_config_t *cfg,
+                               struct use_case_verb *verb,
+                               const char *name,
+                               struct use_case_device **ret_device)
+{
+       struct use_case_device *device;
+       int err;
+
+       device = calloc(1, sizeof(*device));
+       if (device == NULL)
+               return -ENOMEM;
+
+       INIT_LIST_HEAD(&device->enable_list);
+       INIT_LIST_HEAD(&device->disable_list);
+       INIT_LIST_HEAD(&device->transition_list);
+       INIT_LIST_HEAD(&device->dev_list.list);
+       INIT_LIST_HEAD(&device->value_list);
+       INIT_LIST_HEAD(&device->variants);
+       INIT_LIST_HEAD(&device->variant_list);
+       list_add_tail(&device->list, &verb->device_list);
+       device->name = strdup(name);
+       if (device->name == NULL) {
+               free(device);
+               return -ENOMEM;
+       }
+       device->orig_name = strdup(name);
+       if (device->orig_name == NULL)
+               return -ENOMEM;
+
+       err = parse_device_fields(uc_mgr, cfg, device);
+       if (err < 0)
+               return err;
+
+       if (ret_device)
+               *ret_device = device;
+
+       return 0;
+}
+
+static int parse_device(snd_use_case_mgr_t *uc_mgr,
+                       snd_config_t *cfg,
+                       void *data1, void *data2)
+{
+       struct use_case_verb *verb = data1;
+       char *name, *colon;
+       const char *variant_label = NULL;
+       struct use_case_device *device = NULL;
+       snd_config_t *primary_cfg_copy = NULL;
+       snd_config_t *device_variant = NULL;
+       snd_config_t *merged_cfg = NULL;
+       snd_config_iterator_t i, next;
+       snd_config_t *n;
+       int err;
+
+       if (parse_get_safe_name(uc_mgr, cfg, data2, &name) < 0)
+               return -EINVAL;
+
+       if (uc_mgr->conf_format >= 8 && (colon = strchr(name, ':'))) {
+               variant_label = colon + 1;
+
+               err = snd_config_search(cfg, "DeviceVariant", &device_variant);
+               if (err == 0) {
+                       snd_config_t *variant_cfg = NULL;
+
+                       /* Save a copy of the primary config for creating variant devices */
+                       err = snd_config_copy(&primary_cfg_copy, cfg);
+                       if (err < 0) {
+                               free(name);
+                               return err;
+                       }
+
+                       err = snd_config_search(device_variant, variant_label, &variant_cfg);
+                       if (err == 0) {
+                               err = uc_mgr_config_copy_eval_merge(uc_mgr, &merged_cfg, cfg, variant_cfg);
+                               if (err < 0) {
+                                       free(name);
+                                       return err;
+                               }
+                               cfg = merged_cfg;
+                       }
+               }
+       }
+
+       /* in-place evaluation */
+       if (cfg != merged_cfg) {
+               err = uc_mgr_evaluate_inplace(uc_mgr, cfg);
+               if (err < 0) {
+                       free(name);
+                       goto __error;
+               }
+       }
+
+       err = parse_device_by_name(uc_mgr, cfg, verb, name, &device);
+       free(name);
+       if (err < 0)
+               goto __error;
+
+       if (merged_cfg) {
+               snd_config_delete(merged_cfg);
+               merged_cfg = NULL;
+       }
+
+       if (device_variant == NULL)
+               goto __end;
+
+       if (device->dev_list.type == DEVLIST_SUPPORTED) {
+               snd_error(UCM, "DeviceVariant cannot be used with SupportedDevice");
+               err = -EINVAL;
+               goto __error;
+       }
+
+       if (snd_config_get_type(device_variant) != SND_CONFIG_TYPE_COMPOUND) {
+               snd_error(UCM, "compound type expected for DeviceVariant");
+               err = -EINVAL;
+               goto __error;
+       }
+
+       colon = strchr(device->name, ':');
+       if (!colon) {
+               snd_error(UCM, "DeviceVariant requires ':' in device name");
+               err = -EINVAL;
+               goto __error;
+       }
+
+       snd_config_for_each(i, next, device_variant) {
+               const char *variant_name;
+               char variant_device_name[128];
+               struct use_case_device *variant = NULL;
+
+               n = snd_config_iterator_entry(i);
+
+               if (snd_config_get_id(n, &variant_name) < 0)
+                       continue;
+
+               /* Create variant device name: base:variant_name */
+               snprintf(variant_device_name, sizeof(variant_device_name),
+                        "%.*s:%s", (int)(colon - device->name),
+                        device->name, variant_name);
+
+               err = uc_mgr_config_copy_eval_merge(uc_mgr, &merged_cfg, primary_cfg_copy, n);
+               if (err < 0)
+                       goto __error;
+
+               err = parse_device_by_name(uc_mgr, merged_cfg, verb,
+                                          variant_device_name, &variant);
+               snd_config_delete(merged_cfg);
+               merged_cfg = NULL;
+               if (err < 0)
+                       goto __error;
+
+               /* Link variant to primary device */
+               list_add(&variant->variant_list, &device->variants);
+
+               err = uc_mgr_put_to_dev_list(&device->dev_list, variant->name);
+               if (err < 0)
+                       goto __error;
+               if (device->dev_list.type == DEVLIST_NONE)
+                       device->dev_list.type = DEVLIST_CONFLICTING;
+       }
+
+__end:
+       err = 0;
+__error:
+       if (merged_cfg)
+               snd_config_delete(merged_cfg);
+       if (primary_cfg_copy)
+               snd_config_delete(primary_cfg_copy);
+       return err;
+}
+
 /*
  * Parse Device Rename/Delete Command
  *
@@ -1843,6 +2024,7 @@ static int verb_dev_list_add(struct use_case_verb *verb,
 
        list_for_each(pos, &verb->device_list) {
                device = list_entry(pos, struct use_case_device, list);
+
                if (strcmp(device->name, dst) != 0)
                        continue;
                if (device->dev_list.type != dst_type) {
index 894b59a99443c1330bea392202e81ef6e78a2234..aa796b37d25e3662d4160beae0c7148a8a4436cd 100644 (file)
@@ -914,6 +914,78 @@ SectionDevice."Speaker" {
 }
 ~~~
 
+### Device Variants
+
+Starting with **Syntax 8**, devices can define variants using the *DeviceVariant* block.
+Device variants provide a convenient way to define multiple related devices with different
+configurations (such as different channel counts) in a single device definition.
+
+When a device name contains a colon (':') character and the device configuration includes
+*DeviceVariant* blocks, the UCM parser handles variant configuration in two ways:
+
+1. **Primary device configuration**: If the text after the colon (variant label) matches a
+   variant identifier in the *DeviceVariant* block, that variant's configuration is merged
+   with the primary device configuration before parsing. This allows the primary device to
+   inherit base configuration while overriding specific values from the variant.
+
+2. **Additional variant devices**: The UCM parser automatically creates multiple distinct
+   UCM devices:
+   - The base device (with the name specified in the *Device* or *SectionDevice* block)
+   - One additional device for each *DeviceVariant* block
+
+Each variant device name is constructed by combining the base device name with the variant
+identifier. Variant devices are automatically added to the base device's conflicting device
+list, since these configurations are mutually exclusive (e.g., you cannot use 2.0, 5.1, and
+7.1 speaker configurations simultaneously).
+
+Example - Speaker with multiple channel configurations:
+
+~~~{.html}
+Device."Speaker:2.0" {
+  Value {
+    PlaybackChannels 2
+  }
+  DeviceVariant."5.1".Value {
+    PlaybackChannels 6
+  }
+  DeviceVariant."7.1".Value {
+    PlaybackChannels 8
+  }
+}
+~~~
+
+This configuration creates three UCM devices:
+- **Speaker:2.0** - 2 playback channels (base device)
+- **Speaker:5.1** - 6 playback channels (variant)
+- **Speaker:7.1** - 8 playback channels (variant)
+
+The variant devices (**Speaker:5.1** and **Speaker:7.1**) inherit all configuration from the
+base device and override only the values specified in their *DeviceVariant* block. The devices
+are automatically marked as conflicting with each other.
+
+Example - HDMI output with different sample rates:
+
+~~~{.html}
+SectionDevice."HDMI:LowRate" {
+  Comment "HDMI output - standard rate"
+  EnableSequence [
+    cset "name='HDMI Switch' on"
+  ]
+  Value {
+    PlaybackPCM "hw:${CardId},3"
+    PlaybackRate 48000
+  }
+  DeviceVariant."HighRate" {
+    Comment "HDMI output - high sample rate"
+    Value {
+      PlaybackRate 192000
+    }
+  }
+}
+~~~
+
+This creates two devices: **HDMI:LowRate** (48kHz) and **HDMI:HighRate** (192kHz).
+
 */
 
 /**
index 2b30a67fc3b5e0d17918606b9413050b4207e935..8fd996ff1a4a88435211ec07915b5572546ec981 100644 (file)
@@ -183,6 +183,12 @@ struct use_case_device {
 
        /* cached priority for sorting (LONG_MIN if not determined) */
        long sort_priority;
+
+       /* list of variant devices */
+       struct list_head variants;
+
+       /* list link for variant devices */
+       struct list_head variant_list;
 };
 
 /*