}
/*
- * 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);
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
*
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) {
}
~~~
+### 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).
+
*/
/**