ucm: implement DeviceVariant configuration extension

It may be useful for the channel count specification for example.

Signed-off-by: Jaroslav Kysela <perex@perex.cz>
This commit is contained in:
Jaroslav Kysela 2025-11-20 16:11:32 +01:00
parent 554efca497
commit 3149ca0f1c
3 changed files with 323 additions and 63 deletions

View 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) {

View 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).
*/
/**

View 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;
};
/*