From 3149ca0f1cb4492fb3a6116ed2eba3affaa1528b Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Thu, 20 Nov 2025 16:11:32 +0100 Subject: [PATCH] ucm: implement DeviceVariant configuration extension It may be useful for the channel count specification for example. Signed-off-by: Jaroslav Kysela --- src/ucm/parser.c | 308 +++++++++++++++++++++++++++++++++--------- src/ucm/ucm_confdoc.h | 72 ++++++++++ src/ucm/ucm_local.h | 6 + 3 files changed, 323 insertions(+), 63 deletions(-) diff --git a/src/ucm/parser.c b/src/ucm/parser.c index a9be3fea..7ab7c99e 100644 --- a/src/ucm/parser.c +++ b/src/ucm/parser.c @@ -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) { diff --git a/src/ucm/ucm_confdoc.h b/src/ucm/ucm_confdoc.h index 894b59a9..aa796b37 100644 --- a/src/ucm/ucm_confdoc.h +++ b/src/ucm/ucm_confdoc.h @@ -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). + */ /** diff --git a/src/ucm/ucm_local.h b/src/ucm/ucm_local.h index 2b30a67f..8fd996ff 100644 --- a/src/ucm/ucm_local.h +++ b/src/ucm/ucm_local.h @@ -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; }; /* -- 2.47.3