pipewire/spa/plugins/alsa/acp/alsa-ucm.c
Pauli Virtanen dcccfcab7f spa: acp: get and emit UCM information for SplitPCM devices
When api.alsa.split-enable=true for ACP device, instruct UCM to not
use alsa-lib plugins for SplitPCM devices.

Grab the information from UCM for the intended channel remapping, and
add the splitting information to the nodes emitted.

Session manager can then look at that, and load nodes to do the channel
splitting.
2024-12-21 16:03:19 +02:00

2911 lines
96 KiB
C

/***
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/>.
***/
#include "config.h"
#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 "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 *device);
static void ucm_port_data_free(pa_device_port *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 management */
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 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;
}
static PA_PRINTF_FUNC(2,3) const char *ucm_get_string(snd_use_case_mgr_t *uc_mgr, const char *fmt, ...)
{
char *id;
const char *value;
va_list args;
int err;
va_start(args, fmt);
id = pa_vsprintf_malloc(fmt, args);
va_end(args);
err = snd_use_case_get(uc_mgr, id, &value);
if (err >= 0)
pa_log_debug("Got %s: %s", id, value);
pa_xfree(id);
if (err < 0) {
errno = -err;
return NULL;
}
return value;
}
static pa_alsa_ucm_split *ucm_get_split_channels(pa_alsa_ucm_device *device, snd_use_case_mgr_t *uc_mgr, const char *prefix) {
pa_alsa_ucm_split *split;
const char *value;
const char *device_name;
int i;
uint32_t hw_channels;
device_name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_NAME);
if (!device_name)
return NULL;
value = ucm_get_string(uc_mgr, "%sChannels/%s", prefix, device_name);
if (pa_atou(value, &hw_channels) < 0)
return NULL;
split = pa_xnew0(pa_alsa_ucm_split, 1);
for (i = 0; i < PA_CHANNELS_MAX; i++) {
uint32_t idx;
snd_pcm_chmap_t *map;
value = ucm_get_string(uc_mgr, "%sChannel%d/%s", prefix, i, device_name);
if (pa_atou(value, &idx) < 0)
break;
if (idx >= hw_channels)
goto fail;
value = ucm_get_string(uc_mgr, "%sChannelPos%d/%s", prefix, i, device_name);
if (!value)
goto fail;
map = snd_pcm_chmap_parse_string(value);
if (!map)
goto fail;
if (map->channels == 1) {
pa_log_debug("Split %s channel %d -> device %s channel %d: %s (%d)",
prefix, (int)idx, device_name, i, value, map->pos[0]);
split->idx[i] = idx;
split->pos[i] = map->pos[0];
free(map);
} else {
free(map);
goto fail;
}
}
if (i == 0) {
pa_xfree(split);
return NULL;
}
split->channels = i;
split->hw_channels = hw_channels;
return split;
fail:
pa_log_warn("Invalid SplitPCM ALSA UCM rule for device %s", device_name);
pa_xfree(split);
return NULL;
}
/* 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);
}
device->playback_split = ucm_get_split_channels(device, uc_mgr, "Playback");
device->capture_split = ucm_get_split_channels(device, uc_mgr, "Capture");
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);
device->conflicting_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
if (n_confdev <= 0)
pa_log_debug("No %s for device %s", "_conflictingdevs", device_name);
else {
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);
device->supported_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
if (n_suppdev <= 0)
pa_log_debug("No %s for device %s", "_supporteddevs", device_name);
else {
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,
pa_alsa_ucm_verb *verb,
const char *modifier_name) {
const char *value;
char *id;
int i;
const char **devices;
int n_confdev, n_suppdev;
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);
n_confdev = snd_use_case_get_list(uc_mgr, id, &devices);
pa_xfree(id);
modifier->conflicting_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
if (n_confdev <= 0)
pa_log_debug("No %s for modifier %s", "_conflictingdevs", modifier_name);
else {
ucm_add_devices_to_idxset(modifier->conflicting_devices, NULL, verb->devices, devices, n_confdev);
snd_use_case_free_list(devices, n_confdev);
}
id = pa_sprintf_malloc("%s/%s", "_supporteddevs", modifier_name);
n_suppdev = snd_use_case_get_list(uc_mgr, id, &devices);
pa_xfree(id);
modifier->supported_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
if (n_suppdev <= 0)
pa_log_debug("No %s for modifier %s", "_supporteddevs", modifier_name);
else {
ucm_add_devices_to_idxset(modifier->supported_devices, NULL, verb->devices, devices, n_suppdev);
snd_use_case_free_list(devices, n_suppdev);
}
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 long ucm_device_status(pa_alsa_ucm_config *ucm, pa_alsa_ucm_device *dev) {
const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME);
char *devstatus;
long status = 0;
if (!ucm->active_verb) {
pa_log_error("Failed to get status for UCM device %s: no UCM verb set", dev_name);
return -1;
}
devstatus = pa_sprintf_malloc("_devstatus/%s", dev_name);
if (snd_use_case_geti(ucm->ucm_mgr, devstatus, &status) < 0) {
pa_log_debug("Failed to get status for UCM device %s", dev_name);
status = -1;
}
pa_xfree(devstatus);
return status;
}
static int ucm_device_disable(pa_alsa_ucm_config *ucm, pa_alsa_ucm_device *dev) {
const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME);
if (!ucm->active_verb) {
pa_log_error("Failed to disable UCM device %s: no UCM verb set", dev_name);
return -1;
}
/* If any of dev's conflicting devices is enabled, trying to disable
* dev gives an error despite the fact that it's already disabled.
* Check that dev is enabled to avoid this error. */
if (ucm_device_status(ucm, dev) == 0) {
pa_log_debug("UCM device %s is already disabled", dev_name);
return 0;
}
pa_log_debug("Disabling 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);
return -1;
}
return 0;
}
static int ucm_device_enable(pa_alsa_ucm_config *ucm, pa_alsa_ucm_device *dev) {
const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME);
if (!ucm->active_verb) {
pa_log_error("Failed to enable UCM device %s: no UCM verb set", dev_name);
return -1;
}
/* We don't need to enable devices that are already enabled */
if (ucm_device_status(ucm, dev) > 0) {
pa_log_debug("UCM device %s is already enabled", dev_name);
return 0;
}
pa_log_debug("Enabling UCM device %s", dev_name);
if (snd_use_case_set(ucm->ucm_mgr, "_enadev", dev_name) < 0) {
pa_log("Failed to enable UCM device %s", dev_name);
return -1;
}
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 long ucm_modifier_status(pa_alsa_ucm_config *ucm, pa_alsa_ucm_modifier *mod) {
const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME);
char *modstatus;
long status = 0;
if (!ucm->active_verb) {
pa_log_error("Failed to get status for UCM modifier %s: no UCM verb set", mod_name);
return -1;
}
modstatus = pa_sprintf_malloc("_modstatus/%s", mod_name);
if (snd_use_case_geti(ucm->ucm_mgr, modstatus, &status) < 0) {
pa_log_debug("Failed to get status for UCM modifier %s", mod_name);
status = -1;
}
pa_xfree(modstatus);
return status;
}
static int ucm_modifier_disable(pa_alsa_ucm_config *ucm, pa_alsa_ucm_modifier *mod) {
const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME);
if (!ucm->active_verb) {
pa_log_error("Failed to disable UCM modifier %s: no UCM verb set", mod_name);
return -1;
}
/* We don't need to disable modifiers that are already disabled */
if (ucm_modifier_status(ucm, mod) == 0) {
pa_log_debug("UCM modifier %s is already disabled", mod_name);
return 0;
}
pa_log_debug("Disabling 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);
return -1;
}
return 0;
}
static int ucm_modifier_enable(pa_alsa_ucm_config *ucm, pa_alsa_ucm_modifier *mod) {
const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME);
if (!ucm->active_verb) {
pa_log_error("Failed to enable UCM modifier %s: no UCM verb set", mod_name);
return -1;
}
/* We don't need to enable modifiers that are already enabled */
if (ucm_modifier_status(ucm, mod) > 0) {
pa_log_debug("UCM modifier %s is already enabled", mod_name);
return 0;
}
pa_log_debug("Enabling 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);
return -1;
}
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(pa_alsa_ucm_device *dev, const char *role_name, const char *role, bool is_sink) {
const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME);
const char *sink = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SINK);
const char *source = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SOURCE);
if (is_sink && sink)
add_role_to_device(dev, dev_name, role_name, role);
else if (!is_sink && source)
add_role_to_device(dev, dev_name, role_name, role);
}
static char *modifier_name_to_role(const char *mod_name, bool *is_sink) {
char *sub = NULL, *tmp, *pos;
*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);
pos = sub;
while (pos && *pos == ' ') pos++;
if (!pos || !*pos) {
pa_xfree(sub);
pa_log_warn("Can't match media roles for modifier %s", mod_name);
return NULL;
}
tmp = pos;
do {
*tmp = tolower(*tmp);
} while (*(++tmp));
tmp = pa_xstrdup(pos);
pa_xfree(sub);
return tmp;
}
static void ucm_set_media_roles(pa_alsa_ucm_modifier *modifier, const char *mod_name) {
pa_alsa_ucm_device *dev;
bool is_sink = false;
char *sub = NULL;
const char *role_name;
uint32_t idx;
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;
PA_IDXSET_FOREACH(dev, modifier->supported_devices, idx) {
/* 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(dev, role_name, sub, is_sink);
}
}
static void append_lost_relationship(pa_alsa_ucm_device *dev) {
uint32_t idx;
pa_alsa_ucm_device *d;
PA_IDXSET_FOREACH(d, dev->conflicting_devices, idx)
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));
PA_IDXSET_FOREACH(d, dev->supported_devices, idx)
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;
const char *split_prefix = ucm->split_enable ? "<<<SplitPCM=1>>>" : "";
/* support multiple card instances, address card directly by index */
card_name = pa_sprintf_malloc("%shw:%i", split_prefix, card_index);
if (card_name == NULL)
return -PA_ALSA_ERR_UNSPECIFIED;
err = snd_use_case_mgr_open(&ucm->ucm_mgr, card_name);
if (err < 0) {
char *ucm_card_name;
/* fallback longname: is UCM available for this card ? */
pa_xfree(card_name);
err = snd_card_get_name(card_index, &ucm_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;
}
card_name = pa_sprintf_malloc("%s%s", split_prefix, ucm_card_name);
free(ucm_card_name);
if (card_name == NULL) {
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);
if (snd_use_case_get(ucm->ucm_mgr, "_alibpref", &value) == 0) {
if (value[0]) {
ucm->alib_prefix = pa_xstrdup(value);
pa_log_debug("UCM _alibpref=%s", ucm->alib_prefix);
}
free((void *)value);
}
/* get a list of all UCM verbs 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;
}
static void ucm_verb_set_split_leaders(pa_alsa_ucm_verb *verb) {
pa_alsa_ucm_device *d, *d2;
/* Set first virtual device in each split HW PCM as the split leader */
PA_LLIST_FOREACH(d, verb->devices) {
if (d->playback_split)
d->playback_split->leader = true;
if (d->capture_split)
d->capture_split->leader = true;
}
PA_LLIST_FOREACH(d, verb->devices) {
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 (d->playback_split) {
if (!sink)
d->playback_split->leader = false;
if (d->playback_split->leader) {
PA_LLIST_FOREACH(d2, verb->devices) {
const char *sink2 = pa_proplist_gets(d2->proplist, PA_ALSA_PROP_UCM_SINK);
if (d == d2 || !d2->playback_split || !sink || !sink2 || !pa_streq(sink, sink2))
continue;
d2->playback_split->leader = false;
}
}
}
if (d->capture_split) {
if (!source)
d->capture_split->leader = false;
if (d->capture_split->leader) {
PA_LLIST_FOREACH(d2, verb->devices) {
const char *source2 = pa_proplist_gets(d2->proplist, PA_ALSA_PROP_UCM_SOURCE);
if (d == d2 || !d2->capture_split || !source || !source2 || !pa_streq(source, source2))
continue;
d2->capture_split->leader = false;
}
}
}
}
}
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);
}
ucm_verb_set_split_leaders(verb);
/* 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, verb, 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, 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;
void *state;
PA_HASHMAP_FOREACH(port, hash, state) {
data = PA_DEVICE_PORT_DATA(port);
dev = data->device;
data->eld_device = dev->eld_device;
if (data->eld_mixer_device_name)
pa_xfree(data->eld_mixer_device_name);
data->eld_mixer_device_name = pa_xstrdup(dev->eld_mixer_device_name);
}
}
static void update_mixer_paths(pa_hashmap *ports, const char *verb_name) {
pa_device_port *port;
pa_alsa_ucm_port_data *data;
void *state;
/* select volume controls on ports */
PA_HASHMAP_FOREACH(port, ports, state) {
pa_log_info("Updating mixer path for %s: %s", verb_name, port->name);
data = PA_DEVICE_PORT_DATA(port);
data->path = pa_hashmap_get(data->paths, verb_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 *verb_name, *mdev;
void *state, *state2;
PA_HASHMAP_FOREACH(port, hash, state) {
data = PA_DEVICE_PORT_DATA(port);
dev = data->device;
mdev = get_mixer_device(dev, is_sink);
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(verb_name, 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", path->name);
pa_hashmap_remove(data->paths, verb_name);
} else if (!path->has_volume && !path->has_mute) {
pa_log_warn("Path %s is not a volume or mute control", path->name);
pa_hashmap_remove(data->paths, verb_name);
} else
pa_log_debug("Set up h/w %s using '%s' for %s:%s", path->has_volume ? "volume" : "mute",
path->name, verb_name, 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 proplist_set_icon_name(
pa_proplist *proplist,
pa_device_port_type_t type,
bool is_sink) {
const char *icon;
if (is_sink) {
switch (type) {
case PA_DEVICE_PORT_TYPE_HEADPHONES:
icon = "audio-headphones";
break;
case PA_DEVICE_PORT_TYPE_HDMI:
icon = "video-display";
break;
case PA_DEVICE_PORT_TYPE_SPEAKER:
default:
icon = "audio-speakers";
break;
}
} else {
switch (type) {
case PA_DEVICE_PORT_TYPE_HEADSET:
icon = "audio-headset";
break;
case PA_DEVICE_PORT_TYPE_MIC:
default:
icon = "audio-input-microphone";
break;
}
}
pa_proplist_sets(proplist, "device.icon_name", icon);
}
static char *devset_name(pa_idxset *devices, const char *sep) {
int i = 0;
int num = pa_idxset_size(devices);
pa_alsa_ucm_device *sorted[num], *dev;
char *dev_names = NULL;
char *tmp = NULL;
uint32_t idx;
PA_IDXSET_FOREACH(dev, devices, idx) {
sorted[i] = dev;
i++;
}
/* Sort by alphabetical order so as to have a deterministic naming scheme */
qsort(&sorted[0], num, sizeof(pa_alsa_ucm_device *), pa_alsa_ucm_device_cmp);
for (i = 0; i < num; i++) {
dev = sorted[i];
const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME);
if (!dev_names) {
dev_names = pa_xstrdup(dev_name);
} else {
tmp = pa_sprintf_malloc("%s%s%s", dev_names, sep, dev_name);
pa_xfree(dev_names);
dev_names = tmp;
}
}
return dev_names;
}
PA_UNUSED static char *devset_description(pa_idxset *devices, const char *sep) {
int i = 0;
int num = pa_idxset_size(devices);
pa_alsa_ucm_device *sorted[num], *dev;
char *dev_descs = NULL;
char *tmp = NULL;
uint32_t idx;
PA_IDXSET_FOREACH(dev, devices, idx) {
sorted[i] = dev;
i++;
}
/* Sort by alphabetical order to match devset_name() */
qsort(&sorted[0], num, sizeof(pa_alsa_ucm_device *), pa_alsa_ucm_device_cmp);
for (i = 0; i < num; i++) {
dev = sorted[i];
const char *dev_desc = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_DESCRIPTION);
if (!dev_descs) {
dev_descs = pa_xstrdup(dev_desc);
} else {
tmp = pa_sprintf_malloc("%s%s%s", dev_descs, sep, dev_desc);
pa_xfree(dev_descs);
dev_descs = tmp;
}
}
return dev_descs;
}
/* If invert is true, uses 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. */
static unsigned devset_playback_priority(pa_idxset *devices, bool invert) {
pa_alsa_ucm_device *dev;
uint32_t idx;
double priority = 0;
PA_IDXSET_FOREACH(dev, devices, idx) {
if (dev->playback_priority > 0 && invert)
priority += 1.0 / dev->playback_priority;
else
priority += dev->playback_priority;
}
if (priority > 0 && invert)
return (unsigned)(1.0 / priority);
return (unsigned) priority;
}
static unsigned devset_capture_priority(pa_idxset *devices, bool invert) {
pa_alsa_ucm_device *dev;
uint32_t idx;
double priority = 0;
PA_IDXSET_FOREACH(dev, devices, idx) {
if (dev->capture_priority > 0 && invert)
priority += 1.0 / dev->capture_priority;
else
priority += dev->capture_priority;
}
if (priority > 0 && invert)
return (unsigned)(1.0 / priority);
return (unsigned) priority;
}
static void ucm_add_port_props(
pa_device_port *port,
bool is_sink)
{
proplist_set_icon_name(port->proplist, port->type, is_sink);
}
void pa_alsa_ucm_add_port(
pa_hashmap *hash,
pa_alsa_ucm_mapping_context *context,
bool is_sink,
pa_hashmap *ports,
pa_card_profile *cp,
pa_core *core) {
pa_device_port *port;
unsigned priority;
char *name, *desc;
const char *dev_name;
const char *direction;
const char *verb_name;
pa_alsa_ucm_device *dev;
pa_alsa_ucm_port_data *data;
pa_alsa_ucm_volume *vol;
pa_alsa_jack *jack;
pa_device_port_type_t type;
void *state;
dev = context->ucm_device;
if (!dev)
return;
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 = pa_xstrdup(pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_DESCRIPTION));
priority = is_sink ? dev->playback_priority : dev->capture_priority;
jack = ucm_get_jack(context->ucm, dev);
type = dev->type;
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, dev);
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);
ucm_add_port_props(port, is_sink);
}
data = PA_DEVICE_PORT_DATA(port);
PA_HASHMAP_FOREACH_KV(verb_name, vol, is_sink ? dev->playback_volumes : dev->capture_volumes, state) {
if (pa_hashmap_get(data->paths, verb_name))
continue;
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(verb_name), path);
/* Add path also to already created empty path set */
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);
}
/* ELD devices */
set_eld_devices(ports);
}
static bool devset_supports_device(pa_idxset *devices, pa_alsa_ucm_device *dev) {
const char *sink, *sink2, *source, *source2;
pa_alsa_ucm_device *d;
uint32_t idx;
pa_assert(devices);
pa_assert(dev);
/* Can add anything to empty group */
if (pa_idxset_isempty(devices))
return true;
/* Device already selected */
if (pa_idxset_contains(devices, dev))
return true;
/* No conflicting device must already be selected */
if (!pa_idxset_isdisjoint(devices, dev->conflicting_devices))
return false;
/* No already selected device must be unsupported */
if (!pa_idxset_isempty(dev->supported_devices))
if (!pa_idxset_issubset(devices, dev->supported_devices))
return false;
sink = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SINK);
source = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SOURCE);
PA_IDXSET_FOREACH(d, devices, idx) {
/* Must not be unsupported by any selected device */
if (!pa_idxset_isempty(d->supported_devices))
if (!pa_idxset_contains(d->supported_devices, dev))
return false;
/* PlaybackPCM must not be the same as any selected device, except when both split */
sink2 = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SINK);
if (sink && sink2 && pa_streq(sink, sink2)) {
if (!(dev->playback_split && d->playback_split))
return false;
}
/* CapturePCM must not be the same as any selected device, except when both split */
source2 = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SOURCE);
if (source && source2 && pa_streq(source, source2)) {
if (!(dev->capture_split && d->capture_split))
return false;
}
}
return true;
}
/* Iterates nonempty subsets of UCM devices that can be simultaneously
* used, including subsets of previously returned subsets. At start,
* *state should be NULL. It's not safe to modify the devices argument
* until iteration ends. The returned idxsets must be freed by the
* caller. */
static pa_idxset *iterate_device_subsets(pa_idxset *devices, void **state) {
uint32_t idx;
pa_alsa_ucm_device *dev;
pa_assert(devices);
pa_assert(state);
if (*state == NULL) {
/* First iteration, start adding from first device */
*state = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
dev = pa_idxset_first(devices, &idx);
} else {
/* Backtrack the most recent device we added and skip it */
dev = pa_idxset_steal_last(*state, NULL);
pa_idxset_get_by_data(devices, dev, &idx);
if (dev)
dev = pa_idxset_next(devices, &idx);
}
/* Try adding devices we haven't decided on yet */
for (; dev; dev = pa_idxset_next(devices, &idx)) {
if (devset_supports_device(*state, dev))
pa_idxset_put(*state, dev, NULL);
}
if (pa_idxset_isempty(*state)) {
/* No more choices to backtrack on, therefore no more subsets to
* return after this. Don't return the empty set, instead clean
* up and end iteration. */
pa_idxset_free(*state, NULL);
*state = NULL;
return NULL;
}
return pa_idxset_copy(*state, NULL);
}
/* This a wrapper around iterate_device_subsets() that only returns the
* biggest possible groups and not any of their subsets. */
static pa_idxset *iterate_maximal_device_subsets(pa_idxset *devices, void **state) {
uint32_t idx;
pa_alsa_ucm_device *dev;
pa_idxset *subset = NULL;
pa_assert(devices);
pa_assert(state);
while (subset == NULL && (subset = iterate_device_subsets(devices, state))) {
/* Skip this group if it's incomplete, by checking if we can add any
* other device. If we can, this iteration is a subset of another
* group that we already returned or eventually return. */
PA_IDXSET_FOREACH(dev, devices, idx) {
if (subset && !pa_idxset_contains(subset, dev) && devset_supports_device(subset, dev)) {
pa_idxset_free(subset, NULL);
subset = NULL;
}
}
}
return subset;
}
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(
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) {
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_port(*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);
/* probe_volumes() removes per-verb paths from ports if probing them
* fails. The path for the current verb is cached in
* pa_alsa_ucm_port_data.path, which is not cleared by probe_volumes() if
* the path gets removed, so we have to call update_mixer_paths() here to
* unset the cached path if needed. */
if (context->ucm->active_verb) {
const char *verb_name;
verb_name = pa_proplist_gets(context->ucm->active_verb->proplist, PA_ALSA_PROP_UCM_NAME);
update_mixer_paths(*p, verb_name);
}
/* then set property PA_PROP_DEVICE_INTENDED_ROLES */
merged_roles = pa_xstrdup(pa_proplist_gets(proplist, PA_PROP_DEVICE_INTENDED_ROLES));
dev = context->ucm_device;
if (dev) {
const char *roles = pa_proplist_gets(dev->proplist, role_name);
tmp = merge_roles(merged_roles, roles);
pa_xfree(merged_roles);
merged_roles = tmp;
}
mod = context->ucm_modifier;
if (mod) {
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, pa_alsa_profile *new_profile, pa_alsa_profile *old_profile) {
int ret = 0;
const char *verb_name, *profile_name;
pa_alsa_ucm_verb *verb;
pa_alsa_mapping *map;
uint32_t idx;
if (new_profile == old_profile)
return 0;
if (new_profile == NULL) {
verb = NULL;
profile_name = SND_USE_CASE_VERB_INACTIVE;
verb_name = SND_USE_CASE_VERB_INACTIVE;
} else {
verb = new_profile->ucm_context.verb;
profile_name = new_profile->name;
verb_name = pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME);
}
pa_log_info("Set profile to %s", profile_name);
if (ucm->active_verb != verb) {
/* change verb */
pa_log_info("Set UCM verb to %s", verb_name);
if ((snd_use_case_set(ucm->ucm_mgr, "_verb", verb_name)) < 0) {
pa_log("Failed to set verb %s", verb_name);
ret = -1;
}
} else if (ucm->active_verb) {
/* Disable modifiers not in new profile. Has to be done before
* devices, because _dismod fails if a modifier's supported
* devices are disabled. */
PA_IDXSET_FOREACH(map, old_profile->input_mappings, idx)
if (new_profile && !pa_idxset_contains(new_profile->input_mappings, map))
if (map->ucm_context.ucm_modifier && ucm_modifier_disable(ucm, map->ucm_context.ucm_modifier) < 0)
ret = -1;
PA_IDXSET_FOREACH(map, old_profile->output_mappings, idx)
if (new_profile && !pa_idxset_contains(new_profile->output_mappings, map))
if (map->ucm_context.ucm_modifier && ucm_modifier_disable(ucm, map->ucm_context.ucm_modifier) < 0)
ret = -1;
/* Disable devices not in new profile */
PA_IDXSET_FOREACH(map, old_profile->input_mappings, idx)
if (new_profile && !pa_idxset_contains(new_profile->input_mappings, map))
if (map->ucm_context.ucm_device && ucm_device_disable(ucm, map->ucm_context.ucm_device) < 0)
ret = -1;
PA_IDXSET_FOREACH(map, old_profile->output_mappings, idx)
if (new_profile && !pa_idxset_contains(new_profile->output_mappings, map))
if (map->ucm_context.ucm_device && ucm_device_disable(ucm, map->ucm_context.ucm_device) < 0)
ret = -1;
}
ucm->active_verb = verb;
update_mixer_paths(card->ports, verb_name);
return ret;
}
int pa_alsa_ucm_set_port(pa_alsa_ucm_mapping_context *context, pa_device_port *port) {
pa_alsa_ucm_config *ucm;
pa_alsa_ucm_device *dev;
pa_alsa_ucm_port_data *data;
const char *dev_name, *ucm_dev_name;
pa_assert(context && context->ucm);
ucm = context->ucm;
pa_assert(ucm->ucm_mgr);
data = PA_DEVICE_PORT_DATA(port);
dev = data->device;
pa_assert(dev);
if (context->ucm_device) {
dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME);
ucm_dev_name = pa_proplist_gets(context->ucm_device->proplist, PA_ALSA_PROP_UCM_NAME);
if (!pa_streq(dev_name, ucm_dev_name)) {
pa_log_error("Failed to set port %s with wrong UCM context: %s", dev_name, ucm_dev_name);
return -1;
}
}
return ucm_device_enable(ucm, dev);
}
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_full(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func, pa_xfree,
(pa_free_cb_t) pa_alsa_path_free);
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;
m->ucm_context.ucm_device = device;
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;
proplist_set_icon_name(m->proplist, device->type, is_sink);
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;
m->ucm_context.ucm_modifier = modifier;
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 pa_alsa_mapping* ucm_alsa_mapping_get(pa_alsa_ucm_config *ucm, pa_alsa_profile_set *ps, const char *verb_name, const char *ucm_name, bool is_sink) {
pa_alsa_mapping *m;
char *mapping_name;
mapping_name = pa_sprintf_malloc("Mapping %s: %s: %s", verb_name, ucm_name, 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 m;
}
static const struct {
enum snd_pcm_chmap_position pos;
pa_channel_position_t channel;
} chmap_info[] = {
[SND_CHMAP_MONO] = { SND_CHMAP_MONO, PA_CHANNEL_POSITION_MONO },
[SND_CHMAP_FL] = { SND_CHMAP_FL, PA_CHANNEL_POSITION_FRONT_LEFT },
[SND_CHMAP_FR] = { SND_CHMAP_FR, PA_CHANNEL_POSITION_FRONT_RIGHT },
[SND_CHMAP_RL] = { SND_CHMAP_RL, PA_CHANNEL_POSITION_REAR_LEFT },
[SND_CHMAP_RR] = { SND_CHMAP_RR, PA_CHANNEL_POSITION_REAR_RIGHT },
[SND_CHMAP_FC] = { SND_CHMAP_FC, PA_CHANNEL_POSITION_FRONT_CENTER },
[SND_CHMAP_LFE] = { SND_CHMAP_LFE, PA_CHANNEL_POSITION_LFE },
[SND_CHMAP_SL] = { SND_CHMAP_SL, PA_CHANNEL_POSITION_SIDE_LEFT },
[SND_CHMAP_SR] = { SND_CHMAP_SR, PA_CHANNEL_POSITION_SIDE_RIGHT },
[SND_CHMAP_RC] = { SND_CHMAP_RC, PA_CHANNEL_POSITION_REAR_CENTER },
[SND_CHMAP_FLC] = { SND_CHMAP_FLC, PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER },
[SND_CHMAP_FRC] = { SND_CHMAP_FRC, PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER },
/* XXX: missing channel positions, mapped to aux... */
/* [SND_CHMAP_RLC] = { SND_CHMAP_RLC, PA_CHANNEL_POSITION_REAR_LEFT_OF_CENTER }, */
/* [SND_CHMAP_RRC] = { SND_CHMAP_RRC, PA_CHANNEL_POSITION_REAR_RIGHT_OF_CENTER }, */
/* [SND_CHMAP_FLW] = { SND_CHMAP_FLW, PA_CHANNEL_POSITION_FRONT_LEFT_WIDE }, */
/* [SND_CHMAP_FRW] = { SND_CHMAP_FRW, PA_CHANNEL_POSITION_FRONT_RIGHT_WIDE }, */
/* [SND_CHMAP_FLH] = { SND_CHMAP_FLH, PA_CHANNEL_POSITION_FRONT_LEFT_HIGH }, */
/* [SND_CHMAP_FCH] = { SND_CHMAP_FCH, PA_CHANNEL_POSITION_FRONT_CENTER_HIGH }, */
/* [SND_CHMAP_FRH] = { SND_CHMAP_FRH, PA_CHANNEL_POSITION_FRONT_RIGHT_HIGH }, */
[SND_CHMAP_TC] = { SND_CHMAP_TC, PA_CHANNEL_POSITION_TOP_CENTER },
[SND_CHMAP_TFL] = { SND_CHMAP_TFL, PA_CHANNEL_POSITION_TOP_FRONT_LEFT },
[SND_CHMAP_TFR] = { SND_CHMAP_TFR, PA_CHANNEL_POSITION_TOP_FRONT_RIGHT },
[SND_CHMAP_TFC] = { SND_CHMAP_TFC, PA_CHANNEL_POSITION_TOP_FRONT_CENTER },
[SND_CHMAP_TRL] = { SND_CHMAP_TRL, PA_CHANNEL_POSITION_TOP_REAR_LEFT },
[SND_CHMAP_TRR] = { SND_CHMAP_TRR, PA_CHANNEL_POSITION_TOP_REAR_RIGHT },
[SND_CHMAP_TRC] = { SND_CHMAP_TRC, PA_CHANNEL_POSITION_TOP_REAR_CENTER },
/* [SND_CHMAP_TFLC] = { SND_CHMAP_TFLC, PA_CHANNEL_POSITION_TOP_FRONT_LEFT_OF_CENTER }, */
/* [SND_CHMAP_TFRC] = { SND_CHMAP_TFRC, PA_CHANNEL_POSITION_TOP_FRONT_RIGHT_OF_CENTER }, */
/* [SND_CHMAP_TSL] = { SND_CHMAP_TSL, PA_CHANNEL_POSITION_TOP_SIDE_LEFT }, */
/* [SND_CHMAP_TSR] = { SND_CHMAP_TSR, PA_CHANNEL_POSITION_TOP_SIDE_RIGHT }, */
/* [SND_CHMAP_LLFE] = { SND_CHMAP_LLFE, PA_CHANNEL_POSITION_LEFT_LFE }, */
/* [SND_CHMAP_RLFE] = { SND_CHMAP_RLFE, PA_CHANNEL_POSITION_RIGHT_LFE }, */
/* [SND_CHMAP_BC] = { SND_CHMAP_BC, PA_CHANNEL_POSITION_BOTTOM_CENTER }, */
/* [SND_CHMAP_BLC] = { SND_CHMAP_BLC, PA_CHANNEL_POSITION_BOTTOM_LEFT_OF_CENTER }, */
/* [SND_CHMAP_BRC] = { SND_CHMAP_BRC, PA_CHANNEL_POSITION_BOTTOM_RIGHT_OF_CENTER }, */
};
static void ucm_split_to_channel_map(pa_channel_map *m, const pa_alsa_ucm_split *s)
{
const int n = sizeof(chmap_info) / sizeof(chmap_info[0]);
int i;
int aux = 0;
for (i = 0; i < s->channels; ++i) {
int p = s->pos[i];
if (p >= 0 && p < n && (int)chmap_info[p].pos == p)
m->map[i] = chmap_info[p].channel;
else
m->map[i] = PA_CHANNEL_POSITION_AUX0 + aux++;
if (aux >= 32)
break;
}
m->channels = i;
}
static int ucm_create_mapping_direction(
pa_alsa_ucm_config *ucm,
pa_alsa_profile_set *ps,
pa_alsa_ucm_device *device,
const char *verb_name,
const char *device_name,
const char *device_str,
bool is_sink) {
pa_alsa_mapping *m;
unsigned priority, rate, channels;
m = ucm_alsa_mapping_get(ucm, ps, verb_name, device_name, is_sink);
if (!m)
return -1;
pa_log_debug("UCM mapping: %s dev %s", m->name, device_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_device) { /* new mapping */
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;
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);
if (is_sink && device->playback_split) {
m->split = pa_xmemdup(device->playback_split, sizeof(*m->split));
ucm_split_to_channel_map(&m->channel_map, m->split);
} else if (!is_sink && device->capture_split) {
m->split = pa_xmemdup(device->capture_split, sizeof(*m->split));
ucm_split_to_channel_map(&m->channel_map, m->split);
}
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_ucm_modifier *modifier,
const char *verb_name,
const char *mod_name,
const char *device_str,
bool is_sink) {
pa_alsa_mapping *m;
m = ucm_alsa_mapping_get(ucm, ps, verb_name, mod_name, is_sink);
if (!m)
return -1;
pa_log_info("UCM mapping: %s modifier %s", m->name, mod_name);
if (!m->ucm_context.ucm_device && !m->ucm_context.ucm_modifier) { /* new mapping */
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;
}
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_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, device, verb_name, device_name, sink, true);
if (ret == 0 && source)
ret = ucm_create_mapping_direction(ucm, ps, 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);
j = NULL;
goto finish;
}
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,
pa_idxset *mappings,
const char *profile_name,
const char *profile_desc,
unsigned int profile_priority) {
pa_alsa_profile *p;
pa_alsa_mapping *map;
uint32_t idx;
pa_assert(ps);
if (pa_hashmap_get(ps->profiles, profile_name)) {
pa_log("Profile %s already exists", profile_name);
return -1;
}
p = pa_xnew0(pa_alsa_profile, 1);
p->profile_set = ps;
p->name = pa_xstrdup(profile_name);
p->description = pa_xstrdup(profile_desc);
p->priority = profile_priority;
p->ucm_context.verb = verb;
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);
PA_IDXSET_FOREACH(map, mappings, idx)
ucm_add_mapping(p, map);
pa_alsa_profile_dump(p);
return 0;
}
static int ucm_create_verb_profiles(
pa_alsa_ucm_config *ucm,
pa_alsa_profile_set *ps,
pa_alsa_ucm_verb *verb,
const char *verb_name,
const char *verb_desc) {
pa_idxset *verb_devices, *p_devices, *p_mappings;
pa_alsa_ucm_device *dev;
pa_alsa_ucm_modifier *mod;
int i = 0;
int n_profiles = 0;
const char *name, *sink, *source;
char *p_name, *p_desc, *tmp;
unsigned int verb_priority, p_priority;
uint32_t idx;
void *state = NULL;
/* TODO: get profile priority from policy management */
verb_priority = verb->priority;
if (verb_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) {
verb_priority = verb_info[i].priority;
break;
}
}
pa_xfree(verb_cmp);
}
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, 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, mod, verb_name, name, sink, true);
else if (source)
ucm_create_mapping_for_modifier(ucm, ps, mod, verb_name, name, source, false);
}
verb_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
PA_LLIST_FOREACH(dev, verb->devices)
pa_idxset_put(verb_devices, dev, NULL);
while ((p_devices = iterate_maximal_device_subsets(verb_devices, &state))) {
p_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
/* Add the mappings that include our selected devices */
PA_IDXSET_FOREACH(dev, p_devices, idx) {
if (dev->playback_mapping)
pa_idxset_put(p_mappings, dev->playback_mapping, NULL);
if (dev->capture_mapping)
pa_idxset_put(p_mappings, dev->capture_mapping, NULL);
}
/* Add mappings only for the modifiers that can work with our
* device selection */
PA_LLIST_FOREACH(mod, verb->modifiers)
if (pa_idxset_isempty(mod->supported_devices) || pa_idxset_issubset(mod->supported_devices, p_devices))
if (pa_idxset_isdisjoint(mod->conflicting_devices, p_devices)) {
if (mod->playback_mapping)
pa_idxset_put(p_mappings, mod->playback_mapping, NULL);
if (mod->capture_mapping)
pa_idxset_put(p_mappings, mod->capture_mapping, NULL);
}
/* If we'll have multiple profiles for this verb, their names
* must be unique. Use a list of chosen devices to disambiguate
* them. If the profile contains all devices of a verb, we'll
* generate only onle profile whose name should be the verb
* name. GUIs usually show the profile description instead of
* the name, add the device names to those as well. */
tmp = devset_name(p_devices, ", ");
if (pa_idxset_equals(p_devices, verb_devices)) {
p_name = pa_xstrdup(verb_name);
p_desc = pa_xstrdup(verb_desc);
} else {
p_name = pa_sprintf_malloc("%s (%s)", verb_name, tmp);
p_desc = pa_sprintf_malloc("%s (%s)", verb_desc, tmp);
}
/* Make sure profiles with higher-priority devices are
* prioritized. */
p_priority = verb_priority + devset_playback_priority(p_devices, false) + devset_capture_priority(p_devices, false);
if (ucm_create_profile(ucm, ps, verb, p_mappings, p_name, p_desc, p_priority) == 0) {
pa_log_debug("Created profile %s for UCM verb %s", p_name, verb_name);
n_profiles++;
}
pa_xfree(tmp);
pa_xfree(p_name);
pa_xfree(p_desc);
pa_idxset_free(p_mappings, NULL);
pa_idxset_free(p_devices, NULL);
}
pa_idxset_free(verb_devices, NULL);
if (n_profiles == 0) {
pa_log("UCM verb %s created no profiles", verb_name);
return -1;
}
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;
char *mdev, *alib_prefix;
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;
alib_prefix = context->ucm->alib_prefix;
dev = context->ucm_device;
mdev = pa_sprintf_malloc("%shw:%i", alib_prefix ? alib_prefix : "", pcm_card);
if (mdev == NULL)
return;
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->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 (!m->split) {
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);
} else {
if (!m->split->leader) {
errno = EINVAL;
return NULL;
}
exact_channels = true;
try_ss.channels = m->split->hw_channels;
pa_channel_map_init_extend(&try_map, try_ss.channels, PA_CHANNEL_MAP_AUX);
}
try_period_size =
pa_usec_to_bytes(ucm->default_fragment_size_msec * PA_USEC_PER_MSEC, &try_ss) /
pa_frame_size(&try_ss);
try_buffer_size = ucm->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, NULL, NULL, exact_channels);
if (pcm) {
if (!exact_channels)
m->channel_map = try_map;
mapping_init_eld(m, pcm);
}
return pcm;
}
static void pa_alsa_init_proplist_split_pcm(pa_idxset *mappings, pa_alsa_mapping *leader, pa_direction_t direction)
{
pa_proplist *props = pa_proplist_new();
uint32_t idx;
pa_alsa_mapping *m;
if (direction == PA_DIRECTION_OUTPUT)
pa_alsa_init_proplist_pcm(NULL, props, leader->output_pcm);
else
pa_alsa_init_proplist_pcm(NULL, props, leader->input_pcm);
PA_IDXSET_FOREACH(m, mappings, idx) {
if (!m->split)
continue;
if (!pa_streq(m->device_strings[0], leader->device_strings[0]))
continue;
if (direction == PA_DIRECTION_OUTPUT)
pa_proplist_update(m->output_proplist, PA_UPDATE_REPLACE, props);
else
pa_proplist_update(m->input_proplist, PA_UPDATE_REPLACE, props);
}
pa_proplist_free(props);
}
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;
if (!m->split)
pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm);
else
pa_alsa_init_proplist_split_pcm(p->output_mappings, m, PA_DIRECTION_OUTPUT);
pa_alsa_close(&m->output_pcm);
}
PA_IDXSET_FOREACH(m, p->input_mappings, idx) {
if (p->supported)
m->supported++;
if (!m->input_pcm)
continue;
if (!m->split)
pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm);
else
pa_alsa_init_proplist_split_pcm(p->input_mappings, m, PA_DIRECTION_INPUT);
pa_alsa_close(&m->input_pcm);
}
}
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;
bool has_control;
dev = context->ucm_device;
if (!dev->jack || !dev->jack->mixer_device_name)
return;
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);
return;
}
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;
const char *verb_name;
uint32_t idx;
PA_HASHMAP_FOREACH(p, ps->profiles, state) {
pa_log_info("Probing profile %s", p->name);
/* change verb */
verb_name = pa_proplist_gets(p->ucm_context.verb->proplist, PA_ALSA_PROP_UCM_NAME);
pa_log_info("Set ucm verb to %s", verb_name);
if ((snd_use_case_set(ucm->ucm_mgr, "_verb", verb_name)) < 0) {
pa_log("Failed to set verb %s", verb_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;
}
if (m->split && !m->split->leader)
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;
}
if (m->split && !m->split->leader)
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_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL,
(pa_free_cb_t) pa_alsa_mapping_free);
ps->profiles = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL,
(pa_free_cb_t) pa_alsa_profile_free);
ps->decibel_fixes = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func);
/* create profiles 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_verb_profiles(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);
pa_idxset_free(di->conflicting_devices, NULL);
pa_idxset_free(di->supported_devices, NULL);
pa_xfree(di->eld_mixer_device_name);
pa_xfree(di->playback_split);
pa_xfree(di->capture_split);
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);
pa_idxset_free(mi->conflicting_devices, NULL);
pa_idxset_free(mi->supported_devices, NULL);
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;
}
pa_xfree(ucm->alib_prefix);
ucm->alib_prefix = NULL;
}
void pa_alsa_ucm_mapping_context_free(pa_alsa_ucm_mapping_context *context) {
pa_alsa_ucm_device *dev;
pa_alsa_ucm_modifier *mod;
dev = context->ucm_device;
if (dev) {
/* clear ucm device pointer to mapping */
if (context->direction == PA_DIRECTION_OUTPUT)
dev->playback_mapping = NULL;
else
dev->capture_mapping = NULL;
}
mod = context->ucm_modifier;
if (mod) {
if (context->direction == PA_DIRECTION_OUTPUT)
mod->playback_mapping = NULL;
else
mod->capture_mapping = 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) {
ucm_modifier_enable(ucm, mod);
}
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)
ucm_modifier_disable(ucm, mod);
break;
}
}
}
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)
pa_device_port_set_available(port->core_port, port->device->available);
}
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 *device) {
pa_assert(ucm);
pa_assert(core_port);
pa_assert(device);
port->ucm = ucm;
port->core_port = core_port;
port->eld_device = -1;
port->device = device;
pa_dynarray_append(device->ucm_ports, port);
port->paths = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree, NULL);
pa_device_port_set_available(port->core_port, port->device->available);
}
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->paths)
pa_hashmap_free(ucm_port->paths);
pa_xfree(ucm_port->eld_mixer_device_name);
}
long pa_alsa_ucm_port_device_status(pa_alsa_ucm_port_data *data) {
return ucm_device_status(data->ucm, data->device);
}
#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, pa_alsa_profile *new_profile, pa_alsa_profile *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_port(
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) {
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) {
}
long pa_alsa_ucm_port_device_status(pa_alsa_ucm_port_data *data) {
return -1;
}
#endif