diff options
Diffstat (limited to '')
-rw-r--r-- | src/modules/alsa/alsa-ucm.c | 2396 |
1 files changed, 2396 insertions, 0 deletions
diff --git a/src/modules/alsa/alsa-ucm.c b/src/modules/alsa/alsa-ucm.c new file mode 100644 index 0000000..d9cea61 --- /dev/null +++ b/src/modules/alsa/alsa-ucm.c @@ -0,0 +1,2396 @@ +/*** + This file is part of PulseAudio. + + Copyright 2011 Wolfson Microelectronics PLC + Author Margarita Olaya <magi@slimlogic.co.uk> + Copyright 2012 Feng Wei <wei.feng@freescale.com>, Freescale Ltd. + + PulseAudio 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.1 of the License, + or (at your option) any later version. + + PulseAudio 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 + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. + +***/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <ctype.h> +#include <sys/types.h> +#include <limits.h> +#include <alsa/asoundlib.h> + +#ifdef HAVE_VALGRIND_MEMCHECK_H +#include <valgrind/memcheck.h> +#endif + +#include <pulse/sample.h> +#include <pulse/xmalloc.h> +#include <pulse/timeval.h> +#include <pulse/util.h> + +#include <pulsecore/log.h> +#include <pulsecore/macro.h> +#include <pulsecore/core-util.h> +#include <pulsecore/atomic.h> +#include <pulsecore/core-error.h> +#include <pulsecore/once.h> +#include <pulsecore/thread.h> +#include <pulsecore/conf-parser.h> +#include <pulsecore/strbuf.h> + +#include "alsa-mixer.h" +#include "alsa-util.h" +#include "alsa-ucm.h" + +#define PA_UCM_PRE_TAG_OUTPUT "[Out] " +#define PA_UCM_PRE_TAG_INPUT "[In] " + +#define PA_UCM_PLAYBACK_PRIORITY_UNSET(device) ((device)->playback_channels && !(device)->playback_priority) +#define PA_UCM_CAPTURE_PRIORITY_UNSET(device) ((device)->capture_channels && !(device)->capture_priority) +#define PA_UCM_DEVICE_PRIORITY_SET(device, priority) \ + do { \ + if (PA_UCM_PLAYBACK_PRIORITY_UNSET(device)) (device)->playback_priority = (priority); \ + if (PA_UCM_CAPTURE_PRIORITY_UNSET(device)) (device)->capture_priority = (priority); \ + } while (0) +#define PA_UCM_IS_MODIFIER_MAPPING(m) ((pa_proplist_gets((m)->proplist, PA_ALSA_PROP_UCM_MODIFIER)) != NULL) + +#ifdef HAVE_ALSA_UCM + +struct ucm_type { + const char *prefix; + pa_device_port_type_t type; +}; + +struct ucm_items { + const char *id; + const char *property; +}; + +struct ucm_info { + const char *id; + unsigned priority; +}; + +static pa_alsa_jack* ucm_get_jack(pa_alsa_ucm_config *ucm, pa_alsa_ucm_device *device); +static void device_set_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack); +static void device_add_hw_mute_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack); + +static pa_alsa_ucm_device *verb_find_device(pa_alsa_ucm_verb *verb, const char *device_name); + + +static void ucm_port_data_init(pa_alsa_ucm_port_data *port, pa_alsa_ucm_config *ucm, pa_device_port *core_port, + pa_alsa_ucm_device **devices, unsigned n_devices); +static void ucm_port_data_free(pa_device_port *port); +static void ucm_port_update_available(pa_alsa_ucm_port_data *port); + +static struct ucm_type types[] = { + {"None", PA_DEVICE_PORT_TYPE_UNKNOWN}, + {"Speaker", PA_DEVICE_PORT_TYPE_SPEAKER}, + {"Line", PA_DEVICE_PORT_TYPE_LINE}, + {"Mic", PA_DEVICE_PORT_TYPE_MIC}, + {"Headphones", PA_DEVICE_PORT_TYPE_HEADPHONES}, + {"Headset", PA_DEVICE_PORT_TYPE_HEADSET}, + {"Handset", PA_DEVICE_PORT_TYPE_HANDSET}, + {"Bluetooth", PA_DEVICE_PORT_TYPE_BLUETOOTH}, + {"Earpiece", PA_DEVICE_PORT_TYPE_EARPIECE}, + {"SPDIF", PA_DEVICE_PORT_TYPE_SPDIF}, + {"HDMI", PA_DEVICE_PORT_TYPE_HDMI}, + {NULL, 0} +}; + +static struct ucm_items item[] = { + {"PlaybackPCM", PA_ALSA_PROP_UCM_SINK}, + {"CapturePCM", PA_ALSA_PROP_UCM_SOURCE}, + {"PlaybackCTL", PA_ALSA_PROP_UCM_PLAYBACK_CTL_DEVICE}, + {"PlaybackVolume", PA_ALSA_PROP_UCM_PLAYBACK_VOLUME}, + {"PlaybackSwitch", PA_ALSA_PROP_UCM_PLAYBACK_SWITCH}, + {"PlaybackMixer", PA_ALSA_PROP_UCM_PLAYBACK_MIXER_DEVICE}, + {"PlaybackMixerElem", PA_ALSA_PROP_UCM_PLAYBACK_MIXER_ELEM}, + {"PlaybackMasterElem", PA_ALSA_PROP_UCM_PLAYBACK_MASTER_ELEM}, + {"PlaybackMasterType", PA_ALSA_PROP_UCM_PLAYBACK_MASTER_TYPE}, + {"PlaybackPriority", PA_ALSA_PROP_UCM_PLAYBACK_PRIORITY}, + {"PlaybackRate", PA_ALSA_PROP_UCM_PLAYBACK_RATE}, + {"PlaybackChannels", PA_ALSA_PROP_UCM_PLAYBACK_CHANNELS}, + {"CaptureCTL", PA_ALSA_PROP_UCM_CAPTURE_CTL_DEVICE}, + {"CaptureVolume", PA_ALSA_PROP_UCM_CAPTURE_VOLUME}, + {"CaptureSwitch", PA_ALSA_PROP_UCM_CAPTURE_SWITCH}, + {"CaptureMixer", PA_ALSA_PROP_UCM_CAPTURE_MIXER_DEVICE}, + {"CaptureMixerElem", PA_ALSA_PROP_UCM_CAPTURE_MIXER_ELEM}, + {"CaptureMasterElem", PA_ALSA_PROP_UCM_CAPTURE_MASTER_ELEM}, + {"CaptureMasterType", PA_ALSA_PROP_UCM_CAPTURE_MASTER_TYPE}, + {"CapturePriority", PA_ALSA_PROP_UCM_CAPTURE_PRIORITY}, + {"CaptureRate", PA_ALSA_PROP_UCM_CAPTURE_RATE}, + {"CaptureChannels", PA_ALSA_PROP_UCM_CAPTURE_CHANNELS}, + {"TQ", PA_ALSA_PROP_UCM_QOS}, + {"JackCTL", PA_ALSA_PROP_UCM_JACK_DEVICE}, + {"JackControl", PA_ALSA_PROP_UCM_JACK_CONTROL}, + {"JackHWMute", PA_ALSA_PROP_UCM_JACK_HW_MUTE}, + {NULL, NULL}, +}; + +/* UCM verb info - this should eventually be part of policy manangement */ +static struct ucm_info verb_info[] = { + {SND_USE_CASE_VERB_INACTIVE, 0}, + {SND_USE_CASE_VERB_HIFI, 8000}, + {SND_USE_CASE_VERB_HIFI_LOW_POWER, 7000}, + {SND_USE_CASE_VERB_VOICE, 6000}, + {SND_USE_CASE_VERB_VOICE_LOW_POWER, 5000}, + {SND_USE_CASE_VERB_VOICECALL, 4000}, + {SND_USE_CASE_VERB_IP_VOICECALL, 4000}, + {SND_USE_CASE_VERB_ANALOG_RADIO, 3000}, + {SND_USE_CASE_VERB_DIGITAL_RADIO, 3000}, + {NULL, 0} +}; + +/* UCM device info - should be overwritten by ucm property */ +static struct ucm_info dev_info[] = { + {SND_USE_CASE_DEV_SPEAKER, 100}, + {SND_USE_CASE_DEV_LINE, 100}, + {SND_USE_CASE_DEV_HEADPHONES, 100}, + {SND_USE_CASE_DEV_HEADSET, 300}, + {SND_USE_CASE_DEV_HANDSET, 200}, + {SND_USE_CASE_DEV_BLUETOOTH, 400}, + {SND_USE_CASE_DEV_EARPIECE, 100}, + {SND_USE_CASE_DEV_SPDIF, 100}, + {SND_USE_CASE_DEV_HDMI, 100}, + {SND_USE_CASE_DEV_NONE, 100}, + {NULL, 0} +}; + + +static char *ucm_verb_value( + snd_use_case_mgr_t *uc_mgr, + const char *verb_name, + const char *id) { + + const char *value; + char *_id = pa_sprintf_malloc("=%s//%s", id, verb_name); + int err = snd_use_case_get(uc_mgr, _id, &value); + pa_xfree(_id); + if (err < 0) + return NULL; + pa_log_debug("Got %s for verb %s: %s", id, verb_name, value); + /* Use the cast here to allow free() call without casting for callers. + * The snd_use_case_get() returns mallocated string. + * See the Note: in use-case.h for snd_use_case_get(). + */ + return (char *)value; +} + +static int ucm_device_exists(pa_idxset *idxset, pa_alsa_ucm_device *dev) { + pa_alsa_ucm_device *d; + uint32_t idx; + + PA_IDXSET_FOREACH(d, idxset, idx) + if (d == dev) + return 1; + + return 0; +} + +static void ucm_add_devices_to_idxset( + pa_idxset *idxset, + pa_alsa_ucm_device *me, + pa_alsa_ucm_device *devices, + const char **dev_names, + int n) { + + pa_alsa_ucm_device *d; + + PA_LLIST_FOREACH(d, devices) { + const char *name; + int i; + + if (d == me) + continue; + + name = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME); + + for (i = 0; i < n; i++) + if (pa_streq(dev_names[i], name)) + pa_idxset_put(idxset, d, NULL); + } +} + +/* Split a string into words. Like pa_split_spaces() but handle '' and "". */ +static char *ucm_split_devnames(const char *c, const char **state) { + const char *current = *state ? *state : c; + char h; + size_t l; + + if (!*current || *c == 0) + return NULL; + + current += strspn(current, "\n\r \t"); + h = *current; + if (h == '\'' || h =='"') { + c = ++current; + for (l = 0; *c && *c != h; l++) c++; + if (*c != h) + return NULL; + *state = c + 1; + } else { + l = strcspn(current, "\n\r \t"); + *state = current+l; + } + + return pa_xstrndup(current, l); +} + + +static void ucm_volume_free(pa_alsa_ucm_volume *vol) { + pa_assert(vol); + pa_xfree(vol->mixer_elem); + pa_xfree(vol->master_elem); + pa_xfree(vol->master_type); + pa_xfree(vol); +} + +/* Get the volume identifier */ +static char *ucm_get_mixer_id( + pa_alsa_ucm_device *device, + const char *mprop, + const char *cprop, + const char *cid) +{ +#if SND_LIB_VERSION >= 0x10201 /* alsa-lib-1.2.1+ check */ + snd_ctl_elem_id_t *ctl; + int err; +#endif + const char *value; + char *value2; + int index; + + /* mixer element as first, if it's found, return it without modifications */ + value = pa_proplist_gets(device->proplist, mprop); + if (value) + return pa_xstrdup(value); + /* fallback, get the control element identifier */ + /* and try to do some heuristic to determine the mixer element name */ + value = pa_proplist_gets(device->proplist, cprop); + if (value == NULL) + return NULL; +#if SND_LIB_VERSION >= 0x10201 /* alsa-lib-1.2.1+ check */ + /* The new parser may return also element index. */ + snd_ctl_elem_id_alloca(&ctl); + err = snd_use_case_parse_ctl_elem_id(ctl, cid, value); + if (err < 0) + return NULL; + value = snd_ctl_elem_id_get_name(ctl); + index = snd_ctl_elem_id_get_index(ctl); +#else +#warning "Upgrade to alsa-lib 1.2.1!" + index = 0; +#endif + if (!(value2 = pa_str_strip_suffix(value, " Playback Volume"))) + if (!(value2 = pa_str_strip_suffix(value, " Capture Volume"))) + if (!(value2 = pa_str_strip_suffix(value, " Volume"))) + value2 = pa_xstrdup(value); + if (index > 0) { + char *mix = pa_sprintf_malloc("'%s',%d", value2, index); + pa_xfree(value2); + return mix; + } + return value2; +} + +/* Get the volume identifier */ +static pa_alsa_ucm_volume *ucm_get_mixer_volume( + pa_alsa_ucm_device *device, + const char *mprop, + const char *cprop, + const char *cid, + const char *masterid, + const char *mastertype) +{ + pa_alsa_ucm_volume *vol; + char *mixer_elem; + + mixer_elem = ucm_get_mixer_id(device, mprop, cprop, cid); + if (mixer_elem == NULL) + return NULL; + vol = pa_xnew0(pa_alsa_ucm_volume, 1); + if (vol == NULL) { + pa_xfree(mixer_elem); + return NULL; + } + vol->mixer_elem = mixer_elem; + vol->master_elem = pa_xstrdup(pa_proplist_gets(device->proplist, masterid)); + vol->master_type = pa_xstrdup(pa_proplist_gets(device->proplist, mastertype)); + return vol; +} + +/* Get the ALSA mixer device for the UCM device */ +static const char *get_mixer_device(pa_alsa_ucm_device *dev, bool is_sink) +{ + const char *dev_name; + + if (is_sink) { + dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_PLAYBACK_MIXER_DEVICE); + if (!dev_name) + dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_PLAYBACK_CTL_DEVICE); + } else { + dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_CAPTURE_MIXER_DEVICE); + if (!dev_name) + dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_CAPTURE_CTL_DEVICE); + } + return dev_name; +} + +/* Get the ALSA mixer device for the UCM jack */ +static const char *get_jack_mixer_device(pa_alsa_ucm_device *dev, bool is_sink) { + const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_JACK_DEVICE); + if (!dev_name) + return get_mixer_device(dev, is_sink); + return dev_name; +} + +/* Create a property list for this ucm device */ +static int ucm_get_device_property( + pa_alsa_ucm_device *device, + snd_use_case_mgr_t *uc_mgr, + pa_alsa_ucm_verb *verb, + const char *device_name) { + + const char *value; + const char **devices; + char *id, *s; + int i; + int err; + uint32_t ui; + int n_confdev, n_suppdev; + pa_alsa_ucm_volume *vol; + + /* determine the device type */ + device->type = PA_DEVICE_PORT_TYPE_UNKNOWN; + id = s = pa_xstrdup(device_name); + while (s && *s && isalpha(*s)) s++; + if (s) + *s = '\0'; + for (i = 0; types[i].prefix; i++) + if (pa_streq(id, types[i].prefix)) { + device->type = types[i].type; + break; + } + pa_xfree(id); + + /* set properties */ + for (i = 0; item[i].id; i++) { + id = pa_sprintf_malloc("%s/%s", item[i].id, device_name); + err = snd_use_case_get(uc_mgr, id, &value); + pa_xfree(id); + if (err < 0) + continue; + + pa_log_debug("Got %s for device %s: %s", item[i].id, device_name, value); + pa_proplist_sets(device->proplist, item[i].property, value); + free((void*)value); + } + + /* get direction and channels */ + value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_PLAYBACK_CHANNELS); + if (value) { /* output */ + /* get channels */ + if (pa_atou(value, &ui) == 0 && pa_channels_valid(ui)) + device->playback_channels = ui; + else + pa_log("UCM playback channels %s for device %s out of range", value, device_name); + + /* get pcm */ + value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SINK); + if (!value) /* take pcm from verb playback default */ + pa_log("UCM playback device %s fetch pcm failed", device_name); + } + + if (pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SINK) && + device->playback_channels == 0) { + pa_log_info("UCM file does not specify 'PlaybackChannels' " + "for device %s, assuming stereo.", device_name); + device->playback_channels = 2; + } + + value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_CAPTURE_CHANNELS); + if (value) { /* input */ + /* get channels */ + if (pa_atou(value, &ui) == 0 && pa_channels_valid(ui)) + device->capture_channels = ui; + else + pa_log("UCM capture channels %s for device %s out of range", value, device_name); + + /* get pcm */ + value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SOURCE); + if (!value) /* take pcm from verb capture default */ + pa_log("UCM capture device %s fetch pcm failed", device_name); + } + + if (pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SOURCE) && + device->capture_channels == 0) { + pa_log_info("UCM file does not specify 'CaptureChannels' " + "for device %s, assuming stereo.", device_name); + device->capture_channels = 2; + } + + /* get rate and priority of device */ + if (device->playback_channels) { /* sink device */ + /* get rate */ + if ((value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_PLAYBACK_RATE))) { + if (pa_atou(value, &ui) == 0 && pa_sample_rate_valid(ui)) { + pa_log_debug("UCM playback device %s rate %d", device_name, ui); + device->playback_rate = ui; + } else + pa_log_debug("UCM playback device %s has bad rate %s", device_name, value); + } + + value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_PLAYBACK_PRIORITY); + if (value) { + /* get priority from ucm config */ + if (pa_atou(value, &ui) == 0) + device->playback_priority = ui; + else + pa_log_debug("UCM playback priority %s for device %s error", value, device_name); + } + + vol = ucm_get_mixer_volume(device, + PA_ALSA_PROP_UCM_PLAYBACK_MIXER_ELEM, + PA_ALSA_PROP_UCM_PLAYBACK_VOLUME, + "PlaybackVolume", + PA_ALSA_PROP_UCM_PLAYBACK_MASTER_ELEM, + PA_ALSA_PROP_UCM_PLAYBACK_MASTER_TYPE); + if (vol) + pa_hashmap_put(device->playback_volumes, pa_xstrdup(pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME)), vol); + } + + if (device->capture_channels) { /* source device */ + /* get rate */ + if ((value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_CAPTURE_RATE))) { + if (pa_atou(value, &ui) == 0 && pa_sample_rate_valid(ui)) { + pa_log_debug("UCM capture device %s rate %d", device_name, ui); + device->capture_rate = ui; + } else + pa_log_debug("UCM capture device %s has bad rate %s", device_name, value); + } + + value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_CAPTURE_PRIORITY); + if (value) { + /* get priority from ucm config */ + if (pa_atou(value, &ui) == 0) + device->capture_priority = ui; + else + pa_log_debug("UCM capture priority %s for device %s error", value, device_name); + } + + vol = ucm_get_mixer_volume(device, + PA_ALSA_PROP_UCM_CAPTURE_MIXER_ELEM, + PA_ALSA_PROP_UCM_CAPTURE_VOLUME, + "CaptureVolume", + PA_ALSA_PROP_UCM_CAPTURE_MASTER_ELEM, + PA_ALSA_PROP_UCM_CAPTURE_MASTER_TYPE); + if (vol) + pa_hashmap_put(device->capture_volumes, pa_xstrdup(pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME)), vol); + } + + if (PA_UCM_PLAYBACK_PRIORITY_UNSET(device) || PA_UCM_CAPTURE_PRIORITY_UNSET(device)) { + /* get priority from static table */ + for (i = 0; dev_info[i].id; i++) { + if (strcasecmp(dev_info[i].id, device_name) == 0) { + PA_UCM_DEVICE_PRIORITY_SET(device, dev_info[i].priority); + break; + } + } + } + + if (PA_UCM_PLAYBACK_PRIORITY_UNSET(device)) { + /* fall through to default priority */ + device->playback_priority = 100; + } + + if (PA_UCM_CAPTURE_PRIORITY_UNSET(device)) { + /* fall through to default priority */ + device->capture_priority = 100; + } + + id = pa_sprintf_malloc("%s/%s", "_conflictingdevs", device_name); + n_confdev = snd_use_case_get_list(uc_mgr, id, &devices); + pa_xfree(id); + + if (n_confdev <= 0) + pa_log_debug("No %s for device %s", "_conflictingdevs", device_name); + else { + device->conflicting_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + ucm_add_devices_to_idxset(device->conflicting_devices, device, verb->devices, devices, n_confdev); + snd_use_case_free_list(devices, n_confdev); + } + + id = pa_sprintf_malloc("%s/%s", "_supporteddevs", device_name); + n_suppdev = snd_use_case_get_list(uc_mgr, id, &devices); + pa_xfree(id); + + if (n_suppdev <= 0) + pa_log_debug("No %s for device %s", "_supporteddevs", device_name); + else { + device->supported_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + ucm_add_devices_to_idxset(device->supported_devices, device, verb->devices, devices, n_suppdev); + snd_use_case_free_list(devices, n_suppdev); + } + + return 0; +}; + +/* Create a property list for this ucm modifier */ +static int ucm_get_modifier_property(pa_alsa_ucm_modifier *modifier, snd_use_case_mgr_t *uc_mgr, const char *modifier_name) { + const char *value; + char *id; + int i; + + for (i = 0; item[i].id; i++) { + int err; + + id = pa_sprintf_malloc("=%s/%s", item[i].id, modifier_name); + err = snd_use_case_get(uc_mgr, id, &value); + pa_xfree(id); + if (err < 0) + continue; + + pa_log_debug("Got %s for modifier %s: %s", item[i].id, modifier_name, value); + pa_proplist_sets(modifier->proplist, item[i].property, value); + free((void*)value); + } + + id = pa_sprintf_malloc("%s/%s", "_conflictingdevs", modifier_name); + modifier->n_confdev = snd_use_case_get_list(uc_mgr, id, &modifier->conflicting_devices); + pa_xfree(id); + if (modifier->n_confdev < 0) + pa_log_debug("No %s for modifier %s", "_conflictingdevs", modifier_name); + + id = pa_sprintf_malloc("%s/%s", "_supporteddevs", modifier_name); + modifier->n_suppdev = snd_use_case_get_list(uc_mgr, id, &modifier->supported_devices); + pa_xfree(id); + if (modifier->n_suppdev < 0) + pa_log_debug("No %s for modifier %s", "_supporteddevs", modifier_name); + + return 0; +}; + +/* Create a list of devices for this verb */ +static int ucm_get_devices(pa_alsa_ucm_verb *verb, snd_use_case_mgr_t *uc_mgr) { + const char **dev_list; + int num_dev, i; + + num_dev = snd_use_case_get_list(uc_mgr, "_devices", &dev_list); + if (num_dev < 0) + return num_dev; + + for (i = 0; i < num_dev; i += 2) { + pa_alsa_ucm_device *d = pa_xnew0(pa_alsa_ucm_device, 1); + + d->proplist = pa_proplist_new(); + pa_proplist_sets(d->proplist, PA_ALSA_PROP_UCM_NAME, pa_strnull(dev_list[i])); + pa_proplist_sets(d->proplist, PA_ALSA_PROP_UCM_DESCRIPTION, pa_strna(dev_list[i + 1])); + d->ucm_ports = pa_dynarray_new(NULL); + d->hw_mute_jacks = pa_dynarray_new(NULL); + d->available = PA_AVAILABLE_UNKNOWN; + + d->playback_volumes = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree, + (pa_free_cb_t) ucm_volume_free); + d->capture_volumes = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree, + (pa_free_cb_t) ucm_volume_free); + + PA_LLIST_PREPEND(pa_alsa_ucm_device, verb->devices, d); + } + + snd_use_case_free_list(dev_list, num_dev); + + return 0; +}; + +static int ucm_get_modifiers(pa_alsa_ucm_verb *verb, snd_use_case_mgr_t *uc_mgr) { + const char **mod_list; + int num_mod, i; + + num_mod = snd_use_case_get_list(uc_mgr, "_modifiers", &mod_list); + if (num_mod < 0) + return num_mod; + + for (i = 0; i < num_mod; i += 2) { + pa_alsa_ucm_modifier *m; + + if (!mod_list[i]) { + pa_log_warn("Got a modifier with a null name. Skipping."); + continue; + } + + m = pa_xnew0(pa_alsa_ucm_modifier, 1); + m->proplist = pa_proplist_new(); + + pa_proplist_sets(m->proplist, PA_ALSA_PROP_UCM_NAME, mod_list[i]); + pa_proplist_sets(m->proplist, PA_ALSA_PROP_UCM_DESCRIPTION, pa_strna(mod_list[i + 1])); + + PA_LLIST_PREPEND(pa_alsa_ucm_modifier, verb->modifiers, m); + } + + snd_use_case_free_list(mod_list, num_mod); + + return 0; +}; + +static void add_role_to_device(pa_alsa_ucm_device *dev, const char *dev_name, const char *role_name, const char *role) { + const char *cur = pa_proplist_gets(dev->proplist, role_name); + + if (!cur) + pa_proplist_sets(dev->proplist, role_name, role); + else if (!pa_str_in_list_spaces(cur, role)) { /* does not exist */ + char *value = pa_sprintf_malloc("%s %s", cur, role); + + pa_proplist_sets(dev->proplist, role_name, value); + pa_xfree(value); + } + + pa_log_info("Add role %s to device %s(%s), result %s", role, dev_name, role_name, pa_proplist_gets(dev->proplist, + role_name)); +} + +static void add_media_role(const char *name, pa_alsa_ucm_device *list, const char *role_name, const char *role, bool is_sink) { + pa_alsa_ucm_device *d; + + PA_LLIST_FOREACH(d, list) { + const char *dev_name = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME); + + if (pa_streq(dev_name, name)) { + const char *sink = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SINK); + const char *source = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SOURCE); + + if (is_sink && sink) + add_role_to_device(d, dev_name, role_name, role); + else if (!is_sink && source) + add_role_to_device(d, dev_name, role_name, role); + break; + } + } +} + +static char *modifier_name_to_role(const char *mod_name, bool *is_sink) { + char *sub = NULL, *tmp; + + *is_sink = false; + + if (pa_startswith(mod_name, "Play")) { + *is_sink = true; + sub = pa_xstrdup(mod_name + 4); + } else if (pa_startswith(mod_name, "Capture")) + sub = pa_xstrdup(mod_name + 7); + + if (!sub || !*sub) { + pa_xfree(sub); + pa_log_warn("Can't match media roles for modifer %s", mod_name); + return NULL; + } + + tmp = sub; + + do { + *tmp = tolower(*tmp); + } while (*(++tmp)); + + return sub; +} + +static void ucm_set_media_roles(pa_alsa_ucm_modifier *modifier, pa_alsa_ucm_device *list, const char *mod_name) { + int i; + bool is_sink = false; + char *sub = NULL; + const char *role_name; + + sub = modifier_name_to_role(mod_name, &is_sink); + if (!sub) + return; + + modifier->action_direction = is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT; + modifier->media_role = sub; + + role_name = is_sink ? PA_ALSA_PROP_UCM_PLAYBACK_ROLES : PA_ALSA_PROP_UCM_CAPTURE_ROLES; + for (i = 0; i < modifier->n_suppdev; i++) { + /* if modifier has no specific pcm, we add role intent to its supported devices */ + if (!pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_SINK) && + !pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_SOURCE)) + add_media_role(modifier->supported_devices[i], list, role_name, sub, is_sink); + } +} + +static void append_lost_relationship(pa_alsa_ucm_device *dev) { + uint32_t idx; + pa_alsa_ucm_device *d; + + if (dev->conflicting_devices) { + PA_IDXSET_FOREACH(d, dev->conflicting_devices, idx) { + if (!d->conflicting_devices) + d->conflicting_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + + if (pa_idxset_put(d->conflicting_devices, dev, NULL) == 0) + pa_log_warn("Add lost conflicting device %s to %s", + pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME), + pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME)); + } + } + + if (dev->supported_devices) { + PA_IDXSET_FOREACH(d, dev->supported_devices, idx) { + if (!d->supported_devices) + d->supported_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + + if (pa_idxset_put(d->supported_devices, dev, NULL) == 0) + pa_log_warn("Add lost supported device %s to %s", + pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME), + pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME)); + } + } +} + +int pa_alsa_ucm_query_profiles(pa_alsa_ucm_config *ucm, int card_index) { + char *card_name; + const char **verb_list, *value; + int num_verbs, i, err = 0; + + /* support multiple card instances, address card directly by index */ + card_name = pa_sprintf_malloc("hw:%i", card_index); + err = snd_use_case_mgr_open(&ucm->ucm_mgr, card_name); + if (err < 0) { + /* fallback longname: is UCM available for this card ? */ + pa_xfree(card_name); + err = snd_card_get_name(card_index, &card_name); + if (err < 0) { + pa_log("Card can't get card_name from card_index %d", card_index); + err = -PA_ALSA_ERR_UNSPECIFIED; + goto name_fail; + } + + err = snd_use_case_mgr_open(&ucm->ucm_mgr, card_name); + if (err < 0) { + pa_log_info("UCM not available for card %s", card_name); + err = -PA_ALSA_ERR_UCM_OPEN; + goto ucm_mgr_fail; + } + } + + err = snd_use_case_get(ucm->ucm_mgr, "=Linked", &value); + if (err >= 0) { + if (strcasecmp(value, "true") == 0 || strcasecmp(value, "1") == 0) { + free((void *)value); + pa_log_info("Empty (linked) UCM for card %s", card_name); + err = -PA_ALSA_ERR_UCM_LINKED; + goto ucm_verb_fail; + } + free((void *)value); + } + + pa_log_info("UCM available for card %s", card_name); + + /* get a list of all UCM verbs (profiles) for this card */ + num_verbs = snd_use_case_verb_list(ucm->ucm_mgr, &verb_list); + if (num_verbs < 0) { + pa_log("UCM verb list not found for %s", card_name); + err = -PA_ALSA_ERR_UNSPECIFIED; + goto ucm_verb_fail; + } + + /* get the properties of each UCM verb */ + for (i = 0; i < num_verbs; i += 2) { + pa_alsa_ucm_verb *verb; + + /* Get devices and modifiers for each verb */ + err = pa_alsa_ucm_get_verb(ucm->ucm_mgr, verb_list[i], verb_list[i+1], &verb); + if (err < 0) { + pa_log("Failed to get the verb %s", verb_list[i]); + continue; + } + + PA_LLIST_PREPEND(pa_alsa_ucm_verb, ucm->verbs, verb); + } + + if (!ucm->verbs) { + pa_log("No UCM verb is valid for %s", card_name); + err = -PA_ALSA_ERR_UCM_NO_VERB; + } + + snd_use_case_free_list(verb_list, num_verbs); + +ucm_verb_fail: + if (err < 0) { + snd_use_case_mgr_close(ucm->ucm_mgr); + ucm->ucm_mgr = NULL; + } + +ucm_mgr_fail: + pa_xfree(card_name); + +name_fail: + return err; +} + +int pa_alsa_ucm_get_verb(snd_use_case_mgr_t *uc_mgr, const char *verb_name, const char *verb_desc, pa_alsa_ucm_verb **p_verb) { + pa_alsa_ucm_device *d; + pa_alsa_ucm_modifier *mod; + pa_alsa_ucm_verb *verb; + char *value; + unsigned ui; + int err = 0; + + *p_verb = NULL; + pa_log_info("Set UCM verb to %s", verb_name); + err = snd_use_case_set(uc_mgr, "_verb", verb_name); + if (err < 0) + return err; + + verb = pa_xnew0(pa_alsa_ucm_verb, 1); + verb->proplist = pa_proplist_new(); + + pa_proplist_sets(verb->proplist, PA_ALSA_PROP_UCM_NAME, pa_strnull(verb_name)); + pa_proplist_sets(verb->proplist, PA_ALSA_PROP_UCM_DESCRIPTION, pa_strna(verb_desc)); + + value = ucm_verb_value(uc_mgr, verb_name, "Priority"); + if (value && !pa_atou(value, &ui)) + verb->priority = ui > 10000 ? 10000 : ui; + free(value); + + err = ucm_get_devices(verb, uc_mgr); + if (err < 0) + pa_log("No UCM devices for verb %s", verb_name); + + err = ucm_get_modifiers(verb, uc_mgr); + if (err < 0) + pa_log("No UCM modifiers for verb %s", verb_name); + + PA_LLIST_FOREACH(d, verb->devices) { + const char *dev_name = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME); + + /* Devices properties */ + ucm_get_device_property(d, uc_mgr, verb, dev_name); + } + /* make conflicting or supported device mutual */ + PA_LLIST_FOREACH(d, verb->devices) + append_lost_relationship(d); + + PA_LLIST_FOREACH(mod, verb->modifiers) { + const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME); + + /* Modifier properties */ + ucm_get_modifier_property(mod, uc_mgr, mod_name); + + /* Set PA_PROP_DEVICE_INTENDED_ROLES property to devices */ + pa_log_debug("Set media roles for verb %s, modifier %s", verb_name, mod_name); + ucm_set_media_roles(mod, verb->devices, mod_name); + } + + *p_verb = verb; + return 0; +} + +static int pa_alsa_ucm_device_cmp(const void *a, const void *b) { + const pa_alsa_ucm_device *d1 = *(pa_alsa_ucm_device **)a; + const pa_alsa_ucm_device *d2 = *(pa_alsa_ucm_device **)b; + + return strcmp(pa_proplist_gets(d1->proplist, PA_ALSA_PROP_UCM_NAME), pa_proplist_gets(d2->proplist, PA_ALSA_PROP_UCM_NAME)); +} + +static void set_eld_devices(pa_hashmap *hash) +{ + pa_device_port *port; + pa_alsa_ucm_port_data *data; + pa_alsa_ucm_device *dev; + const char *eld_mixer_device_name; + void *state; + int idx, eld_device; + + PA_HASHMAP_FOREACH(port, hash, state) { + data = PA_DEVICE_PORT_DATA(port); + eld_mixer_device_name = NULL; + eld_device = -1; + PA_DYNARRAY_FOREACH(dev, data->devices, idx) { + if (dev->eld_device >= 0 && dev->eld_mixer_device_name) { + if (eld_device >= 0 && eld_device != dev->eld_device) { + pa_log_error("The ELD device is already set!"); + } else if (eld_mixer_device_name && pa_streq(dev->eld_mixer_device_name, eld_mixer_device_name)) { + pa_log_error("The ELD mixer device is already set (%s, %s)!", dev->eld_mixer_device_name, dev->eld_mixer_device_name); + } else { + eld_mixer_device_name = dev->eld_mixer_device_name; + eld_device = dev->eld_device; + } + } + } + data->eld_device = eld_device; + data->eld_mixer_device_name = pa_xstrdup(eld_mixer_device_name); + } +} + +static void probe_volumes(pa_hashmap *hash, bool is_sink, snd_pcm_t *pcm_handle, pa_hashmap *mixers, bool ignore_dB) { + pa_device_port *port; + pa_alsa_path *path; + pa_alsa_ucm_port_data *data; + pa_alsa_ucm_device *dev; + snd_mixer_t *mixer_handle; + const char *profile, *mdev, *mdev2; + void *state, *state2; + int idx; + + PA_HASHMAP_FOREACH(port, hash, state) { + data = PA_DEVICE_PORT_DATA(port); + + mdev = NULL; + PA_DYNARRAY_FOREACH(dev, data->devices, idx) { + mdev2 = get_mixer_device(dev, is_sink); + if (mdev && mdev2 && !pa_streq(mdev, mdev2)) { + pa_log_error("Two mixer device names found ('%s', '%s'), using s/w volume", mdev, mdev2); + goto fail; + } + if (mdev2) + mdev = mdev2; + } + + if (mdev == NULL || !(mixer_handle = pa_alsa_open_mixer_by_name(mixers, mdev, true))) { + pa_log_error("Failed to find a working mixer device (%s).", mdev); + goto fail; + } + + PA_HASHMAP_FOREACH_KV(profile, path, data->paths, state2) { + if (pa_alsa_path_probe(path, NULL, mixer_handle, ignore_dB) < 0) { + pa_log_warn("Could not probe path: %s, using s/w volume", data->path->name); + pa_hashmap_remove(data->paths, profile); + } else if (!path->has_volume) { + pa_log_warn("Path %s is not a volume control", data->path->name); + pa_hashmap_remove(data->paths, profile); + } else + pa_log_debug("Set up h/w volume using '%s' for %s:%s", path->name, profile, port->name); + } + } + + return; + +fail: + /* We could not probe the paths we created. Free them and revert to software volumes. */ + PA_HASHMAP_FOREACH(port, hash, state) { + data = PA_DEVICE_PORT_DATA(port); + pa_hashmap_remove_all(data->paths); + } +} + +static void ucm_add_port_combination( + pa_hashmap *hash, + pa_alsa_ucm_mapping_context *context, + bool is_sink, + pa_alsa_ucm_device **pdevices, + int num, + pa_hashmap *ports, + pa_card_profile *cp, + pa_core *core) { + + pa_device_port *port; + int i; + unsigned priority; + double prio2; + char *name, *desc; + const char *dev_name; + const char *direction; + const char *profile; + pa_alsa_ucm_device *sorted[num], *dev; + pa_alsa_ucm_port_data *data; + pa_alsa_ucm_volume *vol; + pa_alsa_jack *jack, *jack2; + pa_device_port_type_t type, type2; + void *state; + + for (i = 0; i < num; i++) + sorted[i] = pdevices[i]; + + /* Sort by alphabetical order so as to have a deterministic naming scheme + * for combination ports */ + qsort(&sorted[0], num, sizeof(pa_alsa_ucm_device *), pa_alsa_ucm_device_cmp); + + dev = sorted[0]; + dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME); + + name = pa_sprintf_malloc("%s%s", is_sink ? PA_UCM_PRE_TAG_OUTPUT : PA_UCM_PRE_TAG_INPUT, dev_name); + desc = num == 1 ? pa_xstrdup(pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_DESCRIPTION)) + : pa_sprintf_malloc("Combination port for %s", dev_name); + + priority = is_sink ? dev->playback_priority : dev->capture_priority; + prio2 = (priority == 0 ? 0 : 1.0/priority); + jack = ucm_get_jack(context->ucm, dev); + type = dev->type; + + for (i = 1; i < num; i++) { + char *tmp; + + dev = sorted[i]; + dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME); + + tmp = pa_sprintf_malloc("%s+%s", name, dev_name); + pa_xfree(name); + name = tmp; + + tmp = pa_sprintf_malloc("%s,%s", desc, dev_name); + pa_xfree(desc); + desc = tmp; + + priority = is_sink ? dev->playback_priority : dev->capture_priority; + if (priority != 0 && prio2 > 0) + prio2 += 1.0/priority; + + jack2 = ucm_get_jack(context->ucm, dev); + if (jack2) { + if (jack && jack != jack2) + pa_log_warn("Multiple jacks per combined device '%s': '%s' '%s'", name, jack->name, jack2->name); + jack = jack2; + } + + type2 = dev->type; + if (type2 != PA_DEVICE_PORT_TYPE_UNKNOWN) { + if (type != PA_DEVICE_PORT_TYPE_UNKNOWN && type != type2) + pa_log_warn("Multiple device types per combined device '%s': %d %d", name, type, type2); + type = type2; + } + } + + /* Make combination ports always have lower priority, and use the formula + 1/p = 1/p1 + 1/p2 + ... 1/pn. + This way, the result will always be less than the individual components, + yet higher components will lead to higher result. */ + + if (num > 1) + priority = prio2 > 0 ? 1.0/prio2 : 0; + + port = pa_hashmap_get(ports, name); + if (!port) { + pa_device_port_new_data port_data; + + pa_device_port_new_data_init(&port_data); + pa_device_port_new_data_set_name(&port_data, name); + pa_device_port_new_data_set_description(&port_data, desc); + pa_device_port_new_data_set_type(&port_data, type); + pa_device_port_new_data_set_direction(&port_data, is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT); + if (jack) + pa_device_port_new_data_set_availability_group(&port_data, jack->name); + + port = pa_device_port_new(core, &port_data, sizeof(pa_alsa_ucm_port_data)); + pa_device_port_new_data_done(&port_data); + + data = PA_DEVICE_PORT_DATA(port); + ucm_port_data_init(data, context->ucm, port, pdevices, num); + port->impl_free = ucm_port_data_free; + + pa_hashmap_put(ports, port->name, port); + pa_log_debug("Add port %s: %s", port->name, port->description); + + if (num == 1) { + /* To keep things simple and not worry about stacking controls, we only support hardware volumes on non-combination + * ports. */ + data = PA_DEVICE_PORT_DATA(port); + + PA_HASHMAP_FOREACH_KV(profile, vol, is_sink ? dev->playback_volumes : dev->capture_volumes, state) { + pa_alsa_path *path = pa_alsa_path_synthesize(vol->mixer_elem, + is_sink ? PA_ALSA_DIRECTION_OUTPUT : PA_ALSA_DIRECTION_INPUT); + + if (!path) + pa_log_warn("Failed to set up volume control: %s", vol->mixer_elem); + else { + if (vol->master_elem) { + pa_alsa_element *e = pa_alsa_element_get(path, vol->master_elem, false); + e->switch_use = PA_ALSA_SWITCH_MUTE; + e->volume_use = PA_ALSA_VOLUME_MERGE; + } + + pa_hashmap_put(data->paths, pa_xstrdup(profile), path); + + /* Add path also to already created empty path set */ + dev = sorted[0]; + if (is_sink) + pa_hashmap_put(dev->playback_mapping->output_path_set->paths, pa_xstrdup(vol->mixer_elem), path); + else + pa_hashmap_put(dev->capture_mapping->input_path_set->paths, pa_xstrdup(vol->mixer_elem), path); + } + } + } + } + + port->priority = priority; + + pa_xfree(name); + pa_xfree(desc); + + direction = is_sink ? "output" : "input"; + pa_log_debug("Port %s direction %s, priority %d", port->name, direction, priority); + + if (cp) { + pa_log_debug("Adding profile %s to port %s.", cp->name, port->name); + pa_hashmap_put(port->profiles, cp->name, cp); + } + + if (hash) { + pa_hashmap_put(hash, port->name, port); + pa_device_port_ref(port); + } +} + +static int ucm_port_contains(const char *port_name, const char *dev_name, bool is_sink) { + int ret = 0; + const char *r; + const char *state = NULL; + size_t len; + + if (!port_name || !dev_name) + return false; + + port_name += is_sink ? strlen(PA_UCM_PRE_TAG_OUTPUT) : strlen(PA_UCM_PRE_TAG_INPUT); + + while ((r = pa_split_in_place(port_name, "+", &len, &state))) { + if (strlen(dev_name) == len && !strncmp(r, dev_name, len)) { + ret = 1; + break; + } + } + + return ret; +} + +static int ucm_check_conformance( + pa_alsa_ucm_mapping_context *context, + pa_alsa_ucm_device **pdevices, + int dev_num, + pa_alsa_ucm_device *dev) { + + uint32_t idx; + pa_alsa_ucm_device *d; + int i; + + pa_assert(dev); + + pa_log_debug("Check device %s conformance with %d other devices", + pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME), dev_num); + if (dev_num == 0) { + pa_log_debug("First device in combination, number 1"); + return 1; + } + + if (dev->conflicting_devices) { /* the device defines conflicting devices */ + PA_IDXSET_FOREACH(d, dev->conflicting_devices, idx) { + for (i = 0; i < dev_num; i++) { + if (pdevices[i] == d) { + pa_log_debug("Conflicting device found"); + return 0; + } + } + } + } else if (dev->supported_devices) { /* the device defines supported devices */ + for (i = 0; i < dev_num; i++) { + if (!ucm_device_exists(dev->supported_devices, pdevices[i])) { + pa_log_debug("Supported device not found"); + return 0; + } + } + } else { /* not support any other devices */ + pa_log_debug("Not support any other devices"); + return 0; + } + + pa_log_debug("Device added to combination, number %d", dev_num + 1); + return 1; +} + +static inline pa_alsa_ucm_device *get_next_device(pa_idxset *idxset, uint32_t *idx) { + pa_alsa_ucm_device *dev; + + if (*idx == PA_IDXSET_INVALID) + dev = pa_idxset_first(idxset, idx); + else + dev = pa_idxset_next(idxset, idx); + + return dev; +} + +static void ucm_add_ports_combination( + pa_hashmap *hash, + pa_alsa_ucm_mapping_context *context, + bool is_sink, + pa_alsa_ucm_device **pdevices, + int dev_num, + uint32_t map_index, + pa_hashmap *ports, + pa_card_profile *cp, + pa_core *core) { + + pa_alsa_ucm_device *dev; + uint32_t idx = map_index; + + if ((dev = get_next_device(context->ucm_devices, &idx)) == NULL) + return; + + /* check if device at map_index can combine with existing devices combination */ + if (ucm_check_conformance(context, pdevices, dev_num, dev)) { + /* add device at map_index to devices combination */ + pdevices[dev_num] = dev; + /* add current devices combination as a new port */ + ucm_add_port_combination(hash, context, is_sink, pdevices, dev_num + 1, ports, cp, core); + /* try more elements combination */ + ucm_add_ports_combination(hash, context, is_sink, pdevices, dev_num + 1, idx, ports, cp, core); + } + + /* try other device with current elements number */ + ucm_add_ports_combination(hash, context, is_sink, pdevices, dev_num, idx, ports, cp, core); +} + +static char* merge_roles(const char *cur, const char *add) { + char *r, *ret; + const char *state = NULL; + + if (add == NULL) + return pa_xstrdup(cur); + else if (cur == NULL) + return pa_xstrdup(add); + + ret = pa_xstrdup(cur); + + while ((r = pa_split_spaces(add, &state))) { + char *value; + + if (!pa_str_in_list_spaces(ret, r)) + value = pa_sprintf_malloc("%s %s", ret, r); + else { + pa_xfree(r); + continue; + } + + pa_xfree(ret); + ret = value; + pa_xfree(r); + } + + return ret; +} + +void pa_alsa_ucm_add_ports_combination( + pa_hashmap *p, + pa_alsa_ucm_mapping_context *context, + bool is_sink, + pa_hashmap *ports, + pa_card_profile *cp, + pa_core *core) { + + pa_alsa_ucm_device **pdevices; + + pa_assert(context->ucm_devices); + + if (pa_idxset_size(context->ucm_devices) > 0) { + pdevices = pa_xnew(pa_alsa_ucm_device *, pa_idxset_size(context->ucm_devices)); + ucm_add_ports_combination(p, context, is_sink, pdevices, 0, PA_IDXSET_INVALID, ports, cp, core); + pa_xfree(pdevices); + } + + /* ELD devices */ + set_eld_devices(ports); +} + +void pa_alsa_ucm_add_ports( + pa_hashmap **p, + pa_proplist *proplist, + pa_alsa_ucm_mapping_context *context, + bool is_sink, + pa_card *card, + snd_pcm_t *pcm_handle, + bool ignore_dB) { + + uint32_t idx; + char *merged_roles; + const char *role_name = is_sink ? PA_ALSA_PROP_UCM_PLAYBACK_ROLES : PA_ALSA_PROP_UCM_CAPTURE_ROLES; + pa_alsa_ucm_device *dev; + pa_alsa_ucm_modifier *mod; + char *tmp; + + pa_assert(p); + pa_assert(*p); + + /* add ports first */ + pa_alsa_ucm_add_ports_combination(*p, context, is_sink, card->ports, NULL, card->core); + + /* now set up volume paths if any */ + probe_volumes(*p, is_sink, pcm_handle, context->ucm->mixers, ignore_dB); + + /* then set property PA_PROP_DEVICE_INTENDED_ROLES */ + merged_roles = pa_xstrdup(pa_proplist_gets(proplist, PA_PROP_DEVICE_INTENDED_ROLES)); + PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) { + const char *roles = pa_proplist_gets(dev->proplist, role_name); + tmp = merge_roles(merged_roles, roles); + pa_xfree(merged_roles); + merged_roles = tmp; + } + + if (context->ucm_modifiers) + PA_IDXSET_FOREACH(mod, context->ucm_modifiers, idx) { + tmp = merge_roles(merged_roles, mod->media_role); + pa_xfree(merged_roles); + merged_roles = tmp; + } + + if (merged_roles) + pa_proplist_sets(proplist, PA_PROP_DEVICE_INTENDED_ROLES, merged_roles); + + pa_log_info("ALSA device %s roles: %s", pa_proplist_gets(proplist, PA_PROP_DEVICE_STRING), pa_strnull(merged_roles)); + pa_xfree(merged_roles); +} + +/* Change UCM verb and device to match selected card profile */ +int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, pa_card *card, const char *new_profile, const char *old_profile) { + int ret = 0; + const char *profile; + pa_alsa_ucm_verb *verb; + pa_device_port *port; + pa_alsa_ucm_port_data *data; + void *state; + + if (new_profile == old_profile) + return ret; + else if (new_profile == NULL || old_profile == NULL) + profile = new_profile ? new_profile : SND_USE_CASE_VERB_INACTIVE; + else if (!pa_streq(new_profile, old_profile)) + profile = new_profile; + else + return ret; + + /* change verb */ + pa_log_info("Set UCM verb to %s", profile); + if ((snd_use_case_set(ucm->ucm_mgr, "_verb", profile)) < 0) { + pa_log("Failed to set verb %s", profile); + ret = -1; + } + + /* find active verb */ + ucm->active_verb = NULL; + PA_LLIST_FOREACH(verb, ucm->verbs) { + const char *verb_name; + verb_name = pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME); + if (pa_streq(verb_name, profile)) { + ucm->active_verb = verb; + break; + } + } + + /* select volume controls on ports */ + PA_HASHMAP_FOREACH(port, card->ports, state) { + data = PA_DEVICE_PORT_DATA(port); + data->path = pa_hashmap_get(data->paths, profile); + } + + return ret; +} + +int pa_alsa_ucm_set_port(pa_alsa_ucm_mapping_context *context, pa_device_port *port, bool is_sink) { + int i; + int ret = 0; + pa_alsa_ucm_config *ucm; + const char **enable_devs; + int enable_num = 0; + uint32_t idx; + pa_alsa_ucm_device *dev; + + pa_assert(context && context->ucm); + + ucm = context->ucm; + pa_assert(ucm->ucm_mgr); + + enable_devs = pa_xnew(const char *, pa_idxset_size(context->ucm_devices)); + + /* first disable then enable */ + PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) { + const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME); + + if (ucm_port_contains(port->name, dev_name, is_sink)) + enable_devs[enable_num++] = dev_name; + else { + pa_log_debug("Disable ucm device %s", dev_name); + if (snd_use_case_set(ucm->ucm_mgr, "_disdev", dev_name) > 0) { + pa_log("Failed to disable ucm device %s", dev_name); + ret = -1; + break; + } + } + } + + for (i = 0; i < enable_num; i++) { + pa_log_debug("Enable ucm device %s", enable_devs[i]); + if (snd_use_case_set(ucm->ucm_mgr, "_enadev", enable_devs[i]) < 0) { + pa_log("Failed to enable ucm device %s", enable_devs[i]); + ret = -1; + break; + } + } + + pa_xfree(enable_devs); + + return ret; +} + +static void ucm_add_mapping(pa_alsa_profile *p, pa_alsa_mapping *m) { + + pa_alsa_path_set *ps; + + /* create empty path set for the future path additions */ + ps = pa_xnew0(pa_alsa_path_set, 1); + ps->direction = m->direction; + ps->paths = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + + switch (m->direction) { + case PA_ALSA_DIRECTION_ANY: + pa_idxset_put(p->output_mappings, m, NULL); + pa_idxset_put(p->input_mappings, m, NULL); + m->output_path_set = ps; + m->input_path_set = ps; + break; + case PA_ALSA_DIRECTION_OUTPUT: + pa_idxset_put(p->output_mappings, m, NULL); + m->output_path_set = ps; + break; + case PA_ALSA_DIRECTION_INPUT: + pa_idxset_put(p->input_mappings, m, NULL); + m->input_path_set = ps; + break; + } +} + +static void alsa_mapping_add_ucm_device(pa_alsa_mapping *m, pa_alsa_ucm_device *device) { + char *cur_desc; + const char *new_desc, *mdev; + bool is_sink = m->direction == PA_ALSA_DIRECTION_OUTPUT; + + pa_idxset_put(m->ucm_context.ucm_devices, device, NULL); + + new_desc = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_DESCRIPTION); + cur_desc = m->description; + if (cur_desc) + m->description = pa_sprintf_malloc("%s + %s", cur_desc, new_desc); + else + m->description = pa_xstrdup(new_desc); + pa_xfree(cur_desc); + + /* walk around null case */ + m->description = m->description ? m->description : pa_xstrdup(""); + + /* save mapping to ucm device */ + if (is_sink) + device->playback_mapping = m; + else + device->capture_mapping = m; + + mdev = get_mixer_device(device, is_sink); + if (mdev) + pa_proplist_sets(m->proplist, "alsa.mixer_device", mdev); +} + +static void alsa_mapping_add_ucm_modifier(pa_alsa_mapping *m, pa_alsa_ucm_modifier *modifier) { + char *cur_desc; + const char *new_desc, *mod_name, *channel_str; + uint32_t channels = 0; + + pa_idxset_put(m->ucm_context.ucm_modifiers, modifier, NULL); + + new_desc = pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_DESCRIPTION); + cur_desc = m->description; + if (cur_desc) + m->description = pa_sprintf_malloc("%s + %s", cur_desc, new_desc); + else + m->description = pa_xstrdup(new_desc); + pa_xfree(cur_desc); + + m->description = m->description ? m->description : pa_xstrdup(""); + + /* Modifier sinks should not be routed to by default */ + m->priority = 0; + + mod_name = pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_NAME); + pa_proplist_sets(m->proplist, PA_ALSA_PROP_UCM_MODIFIER, mod_name); + + /* save mapping to ucm modifier */ + if (m->direction == PA_ALSA_DIRECTION_OUTPUT) { + modifier->playback_mapping = m; + channel_str = pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_PLAYBACK_CHANNELS); + } else { + modifier->capture_mapping = m; + channel_str = pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_CAPTURE_CHANNELS); + } + + if (channel_str) { + /* FIXME: channel_str is unsanitized input from the UCM configuration, + * we should do proper error handling instead of asserting. + * https://bugs.freedesktop.org/show_bug.cgi?id=71823 */ + pa_assert_se(pa_atou(channel_str, &channels) == 0 && pa_channels_valid(channels)); + pa_log_debug("Got channel count %" PRIu32 " for modifier", channels); + } + + if (channels) + pa_channel_map_init_extend(&m->channel_map, channels, PA_CHANNEL_MAP_ALSA); + else + pa_channel_map_init(&m->channel_map); +} + +static int ucm_create_mapping_direction( + pa_alsa_ucm_config *ucm, + pa_alsa_profile_set *ps, + pa_alsa_profile *p, + pa_alsa_ucm_device *device, + const char *verb_name, + const char *device_name, + const char *device_str, + bool is_sink) { + + pa_alsa_mapping *m; + char *mapping_name; + unsigned priority, rate, channels; + + mapping_name = pa_sprintf_malloc("Mapping %s: %s: %s", verb_name, device_str, is_sink ? "sink" : "source"); + + m = pa_alsa_mapping_get(ps, mapping_name); + if (!m) { + pa_log("No mapping for %s", mapping_name); + pa_xfree(mapping_name); + return -1; + } + pa_log_debug("UCM mapping: %s dev %s", mapping_name, device_name); + pa_xfree(mapping_name); + + priority = is_sink ? device->playback_priority : device->capture_priority; + rate = is_sink ? device->playback_rate : device->capture_rate; + channels = is_sink ? device->playback_channels : device->capture_channels; + + if (!m->ucm_context.ucm_devices) { /* new mapping */ + m->ucm_context.ucm_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + m->ucm_context.ucm = ucm; + m->ucm_context.direction = is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT; + + m->device_strings = pa_xnew0(char*, 2); + m->device_strings[0] = pa_xstrdup(device_str); + m->direction = is_sink ? PA_ALSA_DIRECTION_OUTPUT : PA_ALSA_DIRECTION_INPUT; + + ucm_add_mapping(p, m); + if (rate) + m->sample_spec.rate = rate; + pa_channel_map_init_extend(&m->channel_map, channels, PA_CHANNEL_MAP_ALSA); + } + + /* mapping priority is the highest one of ucm devices */ + if (priority > m->priority) + m->priority = priority; + + /* mapping channels is the lowest one of ucm devices */ + if (channels < m->channel_map.channels) + pa_channel_map_init_extend(&m->channel_map, channels, PA_CHANNEL_MAP_ALSA); + + alsa_mapping_add_ucm_device(m, device); + + return 0; +} + +static int ucm_create_mapping_for_modifier( + pa_alsa_ucm_config *ucm, + pa_alsa_profile_set *ps, + pa_alsa_profile *p, + pa_alsa_ucm_modifier *modifier, + const char *verb_name, + const char *mod_name, + const char *device_str, + bool is_sink) { + + pa_alsa_mapping *m; + char *mapping_name; + + mapping_name = pa_sprintf_malloc("Mapping %s: %s: %s", verb_name, device_str, is_sink ? "sink" : "source"); + + m = pa_alsa_mapping_get(ps, mapping_name); + if (!m) { + pa_log("no mapping for %s", mapping_name); + pa_xfree(mapping_name); + return -1; + } + pa_log_info("ucm mapping: %s modifier %s", mapping_name, mod_name); + pa_xfree(mapping_name); + + if (!m->ucm_context.ucm_devices && !m->ucm_context.ucm_modifiers) { /* new mapping */ + m->ucm_context.ucm_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + m->ucm_context.ucm_modifiers = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + m->ucm_context.ucm = ucm; + m->ucm_context.direction = is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT; + + m->device_strings = pa_xnew0(char*, 2); + m->device_strings[0] = pa_xstrdup(device_str); + m->direction = is_sink ? PA_ALSA_DIRECTION_OUTPUT : PA_ALSA_DIRECTION_INPUT; + /* Modifier sinks should not be routed to by default */ + m->priority = 0; + + ucm_add_mapping(p, m); + } else if (!m->ucm_context.ucm_modifiers) /* share pcm with device */ + m->ucm_context.ucm_modifiers = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + + alsa_mapping_add_ucm_modifier(m, modifier); + + return 0; +} + +static int ucm_create_mapping( + pa_alsa_ucm_config *ucm, + pa_alsa_profile_set *ps, + pa_alsa_profile *p, + pa_alsa_ucm_device *device, + const char *verb_name, + const char *device_name, + const char *sink, + const char *source) { + + int ret = 0; + + if (!sink && !source) { + pa_log("No sink and source at %s: %s", verb_name, device_name); + return -1; + } + + if (sink) + ret = ucm_create_mapping_direction(ucm, ps, p, device, verb_name, device_name, sink, true); + if (ret == 0 && source) + ret = ucm_create_mapping_direction(ucm, ps, p, device, verb_name, device_name, source, false); + + return ret; +} + +static pa_alsa_jack* ucm_get_jack(pa_alsa_ucm_config *ucm, pa_alsa_ucm_device *device) { + pa_alsa_jack *j; + const char *device_name; + const char *jack_control; + const char *mixer_device_name; + char *name; + + pa_assert(ucm); + pa_assert(device); + + device_name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_NAME); + + jack_control = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_JACK_CONTROL); + if (jack_control) { +#if SND_LIB_VERSION >= 0x10201 + snd_ctl_elem_id_t *ctl; + int err, index; + snd_ctl_elem_id_alloca(&ctl); + err = snd_use_case_parse_ctl_elem_id(ctl, "JackControl", jack_control); + if (err < 0) + return NULL; + jack_control = snd_ctl_elem_id_get_name(ctl); + index = snd_ctl_elem_id_get_index(ctl); + if (index > 0) { + pa_log("[%s] Invalid JackControl index value: \"%s\",%d", device_name, jack_control, index); + return NULL; + } +#else +#warning "Upgrade to alsa-lib 1.2.1!" +#endif + if (!pa_endswith(jack_control, " Jack")) { + pa_log("[%s] Invalid JackControl value: \"%s\"", device_name, jack_control); + return NULL; + } + + /* pa_alsa_jack_new() expects a jack name without " Jack" at the + * end, so drop the trailing " Jack". */ + name = pa_xstrndup(jack_control, strlen(jack_control) - 5); + } else { + /* The jack control hasn't been explicitly configured, fail. */ + return NULL; + } + + PA_LLIST_FOREACH(j, ucm->jacks) + if (pa_streq(j->name, name)) + goto finish; + + mixer_device_name = get_jack_mixer_device(device, true); + if (!mixer_device_name) + mixer_device_name = get_jack_mixer_device(device, false); + if (!mixer_device_name) { + pa_log("[%s] No mixer device name for JackControl \"%s\"", device_name, jack_control); + return NULL; + } + j = pa_alsa_jack_new(NULL, mixer_device_name, name, 0); + PA_LLIST_PREPEND(pa_alsa_jack, ucm->jacks, j); + +finish: + pa_xfree(name); + + return j; +} + +static int ucm_create_profile( + pa_alsa_ucm_config *ucm, + pa_alsa_profile_set *ps, + pa_alsa_ucm_verb *verb, + const char *verb_name, + const char *verb_desc) { + + pa_alsa_profile *p; + pa_alsa_ucm_device *dev; + pa_alsa_ucm_modifier *mod; + int i = 0; + const char *name, *sink, *source; + unsigned int priority; + + pa_assert(ps); + + if (pa_hashmap_get(ps->profiles, verb_name)) { + pa_log("Verb %s already exists", verb_name); + return -1; + } + + p = pa_xnew0(pa_alsa_profile, 1); + p->profile_set = ps; + p->name = pa_xstrdup(verb_name); + p->description = pa_xstrdup(verb_desc); + + p->output_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + p->input_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + + p->supported = true; + pa_hashmap_put(ps->profiles, p->name, p); + + /* TODO: get profile priority from policy management */ + priority = verb->priority; + + if (priority == 0) { + char *verb_cmp, *c; + c = verb_cmp = pa_xstrdup(verb_name); + while (*c) { + if (*c == '_') *c = ' '; + c++; + } + for (i = 0; verb_info[i].id; i++) { + if (strcasecmp(verb_info[i].id, verb_cmp) == 0) { + priority = verb_info[i].priority; + break; + } + } + pa_xfree(verb_cmp); + } + + p->priority = priority; + + PA_LLIST_FOREACH(dev, verb->devices) { + pa_alsa_jack *jack; + const char *jack_hw_mute; + + name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME); + + sink = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SINK); + source = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SOURCE); + + ucm_create_mapping(ucm, ps, p, dev, verb_name, name, sink, source); + + jack = ucm_get_jack(ucm, dev); + if (jack) + device_set_jack(dev, jack); + + /* JackHWMute contains a list of device names. Each listed device must + * be associated with the jack object that we just created. */ + jack_hw_mute = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_JACK_HW_MUTE); + if (jack_hw_mute && !jack) { + pa_log("[%s] JackHWMute set, but JackControl is missing", name); + jack_hw_mute = NULL; + } + if (jack_hw_mute) { + char *hw_mute_device_name; + const char *state = NULL; + + while ((hw_mute_device_name = ucm_split_devnames(jack_hw_mute, &state))) { + pa_alsa_ucm_verb *verb2; + bool device_found = false; + + /* Search the referenced device from all verbs. If there are + * multiple verbs that have a device with this name, we add the + * hw mute association to each of those devices. */ + PA_LLIST_FOREACH(verb2, ucm->verbs) { + pa_alsa_ucm_device *hw_mute_device; + + hw_mute_device = verb_find_device(verb2, hw_mute_device_name); + if (hw_mute_device) { + device_found = true; + device_add_hw_mute_jack(hw_mute_device, jack); + } + } + + if (!device_found) + pa_log("[%s] JackHWMute references an unknown device: %s", name, hw_mute_device_name); + + pa_xfree(hw_mute_device_name); + } + } + } + + /* Now find modifiers that have their own PlaybackPCM and create + * separate sinks for them. */ + PA_LLIST_FOREACH(mod, verb->modifiers) { + name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME); + + sink = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_SINK); + source = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_SOURCE); + + if (sink) + ucm_create_mapping_for_modifier(ucm, ps, p, mod, verb_name, name, sink, true); + else if (source) + ucm_create_mapping_for_modifier(ucm, ps, p, mod, verb_name, name, source, false); + } + + pa_alsa_profile_dump(p); + + return 0; +} + +static void mapping_init_eld(pa_alsa_mapping *m, snd_pcm_t *pcm) +{ + pa_alsa_ucm_mapping_context *context = &m->ucm_context; + pa_alsa_ucm_device *dev; + uint32_t idx; + char *mdev; + snd_pcm_info_t *info; + int pcm_card, pcm_device; + + snd_pcm_info_alloca(&info); + if (snd_pcm_info(pcm, info) < 0) + return; + + if ((pcm_card = snd_pcm_info_get_card(info)) < 0) + return; + if ((pcm_device = snd_pcm_info_get_device(info)) < 0) + return; + + PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) { + mdev = pa_sprintf_malloc("hw:%i", pcm_card); + if (mdev == NULL) + continue; + dev->eld_mixer_device_name = mdev; + dev->eld_device = pcm_device; + } +} + +static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m, int mode) { + snd_pcm_t* pcm; + pa_sample_spec try_ss = ucm->core->default_sample_spec; + pa_channel_map try_map; + snd_pcm_uframes_t try_period_size, try_buffer_size; + bool exact_channels = m->channel_map.channels > 0; + + if (exact_channels) { + try_map = m->channel_map; + try_ss.channels = try_map.channels; + } else + pa_channel_map_init_extend(&try_map, try_ss.channels, PA_CHANNEL_MAP_ALSA); + + try_period_size = + pa_usec_to_bytes(ucm->core->default_fragment_size_msec * PA_USEC_PER_MSEC, &try_ss) / + pa_frame_size(&try_ss); + try_buffer_size = ucm->core->default_n_fragments * try_period_size; + + pcm = pa_alsa_open_by_device_string(m->device_strings[0], NULL, &try_ss, + &try_map, mode, &try_period_size, &try_buffer_size, 0, NULL, NULL, exact_channels); + + if (pcm) { + if (!exact_channels) + m->channel_map = try_map; + mapping_init_eld(m, pcm); + } + + return pcm; +} + +static void profile_finalize_probing(pa_alsa_profile *p) { + pa_alsa_mapping *m; + uint32_t idx; + + PA_IDXSET_FOREACH(m, p->output_mappings, idx) { + if (p->supported) + m->supported++; + + if (!m->output_pcm) + continue; + + snd_pcm_close(m->output_pcm); + m->output_pcm = NULL; + } + + PA_IDXSET_FOREACH(m, p->input_mappings, idx) { + if (p->supported) + m->supported++; + + if (!m->input_pcm) + continue; + + snd_pcm_close(m->input_pcm); + m->input_pcm = NULL; + } +} + +static void ucm_mapping_jack_probe(pa_alsa_mapping *m, pa_hashmap *mixers) { + snd_mixer_t *mixer_handle; + pa_alsa_ucm_mapping_context *context = &m->ucm_context; + pa_alsa_ucm_device *dev; + uint32_t idx; + + PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) { + bool has_control; + + if (!dev->jack || !dev->jack->mixer_device_name) + continue; + + mixer_handle = pa_alsa_open_mixer_by_name(mixers, dev->jack->mixer_device_name, true); + if (!mixer_handle) { + pa_log_error("Unable to determine open mixer device '%s' for jack %s", dev->jack->mixer_device_name, dev->jack->name); + continue; + } + + has_control = pa_alsa_mixer_find_card(mixer_handle, &dev->jack->alsa_id, 0) != NULL; + pa_alsa_jack_set_has_control(dev->jack, has_control); + pa_log_info("UCM jack %s has_control=%d", dev->jack->name, dev->jack->has_control); + } +} + +static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set *ps) { + void *state; + pa_alsa_profile *p; + pa_alsa_mapping *m; + uint32_t idx; + + PA_HASHMAP_FOREACH(p, ps->profiles, state) { + /* change verb */ + pa_log_info("Set ucm verb to %s", p->name); + + if ((snd_use_case_set(ucm->ucm_mgr, "_verb", p->name)) < 0) { + pa_log("Failed to set verb %s", p->name); + p->supported = false; + continue; + } + + PA_IDXSET_FOREACH(m, p->output_mappings, idx) { + if (PA_UCM_IS_MODIFIER_MAPPING(m)) { + /* Skip jack probing on modifier PCMs since we expect this to + * only be controlled on the main device/verb PCM. */ + continue; + } + + m->output_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_PLAYBACK); + if (!m->output_pcm) { + p->supported = false; + break; + } + } + + if (p->supported) { + PA_IDXSET_FOREACH(m, p->input_mappings, idx) { + if (PA_UCM_IS_MODIFIER_MAPPING(m)) { + /* Skip jack probing on modifier PCMs since we expect this to + * only be controlled on the main device/verb PCM. */ + continue; + } + + m->input_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_CAPTURE); + if (!m->input_pcm) { + p->supported = false; + break; + } + } + } + + if (!p->supported) { + profile_finalize_probing(p); + continue; + } + + pa_log_debug("Profile %s supported.", p->name); + + PA_IDXSET_FOREACH(m, p->output_mappings, idx) + if (!PA_UCM_IS_MODIFIER_MAPPING(m)) + ucm_mapping_jack_probe(m, ucm->mixers); + + PA_IDXSET_FOREACH(m, p->input_mappings, idx) + if (!PA_UCM_IS_MODIFIER_MAPPING(m)) + ucm_mapping_jack_probe(m, ucm->mixers); + + profile_finalize_probing(p); + } + + /* restore ucm state */ + snd_use_case_set(ucm->ucm_mgr, "_verb", SND_USE_CASE_VERB_INACTIVE); + + pa_alsa_profile_set_drop_unsupported(ps); +} + +pa_alsa_profile_set* pa_alsa_ucm_add_profile_set(pa_alsa_ucm_config *ucm, pa_channel_map *default_channel_map) { + pa_alsa_ucm_verb *verb; + pa_alsa_profile_set *ps; + + ps = pa_xnew0(pa_alsa_profile_set, 1); + ps->mappings = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func); + ps->profiles = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func); + ps->decibel_fixes = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func); + + /* create a profile for each verb */ + PA_LLIST_FOREACH(verb, ucm->verbs) { + const char *verb_name; + const char *verb_desc; + + verb_name = pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME); + verb_desc = pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_DESCRIPTION); + if (verb_name == NULL) { + pa_log("Verb with no name"); + continue; + } + + ucm_create_profile(ucm, ps, verb, verb_name, verb_desc); + } + + ucm_probe_profile_set(ucm, ps); + ps->probed = true; + + return ps; +} + +static void free_verb(pa_alsa_ucm_verb *verb) { + pa_alsa_ucm_device *di, *dn; + pa_alsa_ucm_modifier *mi, *mn; + + PA_LLIST_FOREACH_SAFE(di, dn, verb->devices) { + PA_LLIST_REMOVE(pa_alsa_ucm_device, verb->devices, di); + + if (di->hw_mute_jacks) + pa_dynarray_free(di->hw_mute_jacks); + + if (di->ucm_ports) + pa_dynarray_free(di->ucm_ports); + + if (di->playback_volumes) + pa_hashmap_free(di->playback_volumes); + if (di->capture_volumes) + pa_hashmap_free(di->capture_volumes); + + pa_proplist_free(di->proplist); + + if (di->conflicting_devices) + pa_idxset_free(di->conflicting_devices, NULL); + if (di->supported_devices) + pa_idxset_free(di->supported_devices, NULL); + + pa_xfree(di->eld_mixer_device_name); + + pa_xfree(di); + } + + PA_LLIST_FOREACH_SAFE(mi, mn, verb->modifiers) { + PA_LLIST_REMOVE(pa_alsa_ucm_modifier, verb->modifiers, mi); + pa_proplist_free(mi->proplist); + if (mi->n_suppdev > 0) + snd_use_case_free_list(mi->supported_devices, mi->n_suppdev); + if (mi->n_confdev > 0) + snd_use_case_free_list(mi->conflicting_devices, mi->n_confdev); + pa_xfree(mi->media_role); + pa_xfree(mi); + } + pa_proplist_free(verb->proplist); + pa_xfree(verb); +} + +static pa_alsa_ucm_device *verb_find_device(pa_alsa_ucm_verb *verb, const char *device_name) { + pa_alsa_ucm_device *device; + + pa_assert(verb); + pa_assert(device_name); + + PA_LLIST_FOREACH(device, verb->devices) { + const char *name; + + name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_NAME); + if (pa_streq(name, device_name)) + return device; + } + + return NULL; +} + +void pa_alsa_ucm_free(pa_alsa_ucm_config *ucm) { + pa_alsa_ucm_verb *vi, *vn; + pa_alsa_jack *ji, *jn; + + PA_LLIST_FOREACH_SAFE(vi, vn, ucm->verbs) { + PA_LLIST_REMOVE(pa_alsa_ucm_verb, ucm->verbs, vi); + free_verb(vi); + } + PA_LLIST_FOREACH_SAFE(ji, jn, ucm->jacks) { + PA_LLIST_REMOVE(pa_alsa_jack, ucm->jacks, ji); + pa_alsa_jack_free(ji); + } + if (ucm->ucm_mgr) { + snd_use_case_mgr_close(ucm->ucm_mgr); + ucm->ucm_mgr = NULL; + } +} + +void pa_alsa_ucm_mapping_context_free(pa_alsa_ucm_mapping_context *context) { + pa_alsa_ucm_device *dev; + pa_alsa_ucm_modifier *mod; + uint32_t idx; + + if (context->ucm_devices) { + /* clear ucm device pointer to mapping */ + PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) { + if (context->direction == PA_DIRECTION_OUTPUT) + dev->playback_mapping = NULL; + else + dev->capture_mapping = NULL; + } + + pa_idxset_free(context->ucm_devices, NULL); + } + + if (context->ucm_modifiers) { + PA_IDXSET_FOREACH(mod, context->ucm_modifiers, idx) { + if (context->direction == PA_DIRECTION_OUTPUT) + mod->playback_mapping = NULL; + else + mod->capture_mapping = NULL; + } + + pa_idxset_free(context->ucm_modifiers, NULL); + } +} + +/* Enable the modifier when the first stream with matched role starts */ +void pa_alsa_ucm_roled_stream_begin(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir) { + pa_alsa_ucm_modifier *mod; + + if (!ucm->active_verb) + return; + + PA_LLIST_FOREACH(mod, ucm->active_verb->modifiers) { + if ((mod->action_direction == dir) && (pa_streq(mod->media_role, role))) { + if (mod->enabled_counter == 0) { + const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME); + + pa_log_info("Enable ucm modifier %s", mod_name); + if (snd_use_case_set(ucm->ucm_mgr, "_enamod", mod_name) < 0) { + pa_log("Failed to enable ucm modifier %s", mod_name); + } + } + + mod->enabled_counter++; + break; + } + } +} + +/* Disable the modifier when the last stream with matched role ends */ +void pa_alsa_ucm_roled_stream_end(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir) { + pa_alsa_ucm_modifier *mod; + + if (!ucm->active_verb) + return; + + PA_LLIST_FOREACH(mod, ucm->active_verb->modifiers) { + if ((mod->action_direction == dir) && (pa_streq(mod->media_role, role))) { + + mod->enabled_counter--; + if (mod->enabled_counter == 0) { + const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME); + + pa_log_info("Disable ucm modifier %s", mod_name); + if (snd_use_case_set(ucm->ucm_mgr, "_dismod", mod_name) < 0) { + pa_log("Failed to disable ucm modifier %s", mod_name); + } + } + + break; + } + } +} + +static void device_add_ucm_port(pa_alsa_ucm_device *device, pa_alsa_ucm_port_data *port) { + pa_assert(device); + pa_assert(port); + + pa_dynarray_append(device->ucm_ports, port); +} + +static void device_set_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack) { + pa_assert(device); + pa_assert(jack); + + device->jack = jack; + pa_alsa_jack_add_ucm_device(jack, device); + + pa_alsa_ucm_device_update_available(device); +} + +static void device_add_hw_mute_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack) { + pa_assert(device); + pa_assert(jack); + + pa_dynarray_append(device->hw_mute_jacks, jack); + pa_alsa_jack_add_ucm_hw_mute_device(jack, device); + + pa_alsa_ucm_device_update_available(device); +} + +static void device_set_available(pa_alsa_ucm_device *device, pa_available_t available) { + pa_alsa_ucm_port_data *port; + unsigned idx; + + pa_assert(device); + + if (available == device->available) + return; + + device->available = available; + + PA_DYNARRAY_FOREACH(port, device->ucm_ports, idx) + ucm_port_update_available(port); +} + +void pa_alsa_ucm_device_update_available(pa_alsa_ucm_device *device) { + pa_available_t available = PA_AVAILABLE_UNKNOWN; + pa_alsa_jack *jack; + unsigned idx; + + pa_assert(device); + + if (device->jack && device->jack->has_control) + available = device->jack->plugged_in ? PA_AVAILABLE_YES : PA_AVAILABLE_NO; + + PA_DYNARRAY_FOREACH(jack, device->hw_mute_jacks, idx) { + if (jack->plugged_in) { + available = PA_AVAILABLE_NO; + break; + } + } + + device_set_available(device, available); +} + +static void ucm_port_data_init(pa_alsa_ucm_port_data *port, pa_alsa_ucm_config *ucm, pa_device_port *core_port, + pa_alsa_ucm_device **devices, unsigned n_devices) { + unsigned i; + + pa_assert(ucm); + pa_assert(core_port); + pa_assert(devices); + + port->ucm = ucm; + port->core_port = core_port; + port->devices = pa_dynarray_new(NULL); + port->eld_device = -1; + + for (i = 0; i < n_devices; i++) { + pa_dynarray_append(port->devices, devices[i]); + device_add_ucm_port(devices[i], port); + } + + port->paths = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree, + (pa_free_cb_t) pa_alsa_path_free); + + ucm_port_update_available(port); +} + +static void ucm_port_data_free(pa_device_port *port) { + pa_alsa_ucm_port_data *ucm_port; + + pa_assert(port); + + ucm_port = PA_DEVICE_PORT_DATA(port); + + if (ucm_port->devices) + pa_dynarray_free(ucm_port->devices); + + if (ucm_port->paths) + pa_hashmap_free(ucm_port->paths); + + pa_xfree(ucm_port->eld_mixer_device_name); +} + +static void ucm_port_update_available(pa_alsa_ucm_port_data *port) { + pa_alsa_ucm_device *device; + unsigned idx; + pa_available_t available = PA_AVAILABLE_YES; + + pa_assert(port); + + PA_DYNARRAY_FOREACH(device, port->devices, idx) { + if (device->available == PA_AVAILABLE_UNKNOWN) + available = PA_AVAILABLE_UNKNOWN; + else if (device->available == PA_AVAILABLE_NO) { + available = PA_AVAILABLE_NO; + break; + } + } + + pa_device_port_set_available(port->core_port, available); +} + +#else /* HAVE_ALSA_UCM */ + +/* Dummy functions for systems without UCM support */ + +int pa_alsa_ucm_query_profiles(pa_alsa_ucm_config *ucm, int card_index) { + pa_log_info("UCM not available."); + return -1; +} + +pa_alsa_profile_set* pa_alsa_ucm_add_profile_set(pa_alsa_ucm_config *ucm, pa_channel_map *default_channel_map) { + return NULL; +} + +int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, pa_card *card, const char *new_profile, const char *old_profile) { + return -1; +} + +int pa_alsa_ucm_get_verb(snd_use_case_mgr_t *uc_mgr, const char *verb_name, const char *verb_desc, pa_alsa_ucm_verb **p_verb) { + return -1; +} + +void pa_alsa_ucm_add_ports( + pa_hashmap **hash, + pa_proplist *proplist, + pa_alsa_ucm_mapping_context *context, + bool is_sink, + pa_card *card, + snd_pcm_t *pcm_handle, + bool ignore_dB) { +} + +void pa_alsa_ucm_add_ports_combination( + pa_hashmap *hash, + pa_alsa_ucm_mapping_context *context, + bool is_sink, + pa_hashmap *ports, + pa_card_profile *cp, + pa_core *core) { +} + +int pa_alsa_ucm_set_port(pa_alsa_ucm_mapping_context *context, pa_device_port *port, bool is_sink) { + return -1; +} + +void pa_alsa_ucm_free(pa_alsa_ucm_config *ucm) { +} + +void pa_alsa_ucm_mapping_context_free(pa_alsa_ucm_mapping_context *context) { +} + +void pa_alsa_ucm_roled_stream_begin(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir) { +} + +void pa_alsa_ucm_roled_stream_end(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir) { +} + +#endif |