pipewire/spa/plugins/alsa/acp/acp.c
Alper Nebi Yasak b8361cc568 alsa-ucm: Make ports store only one device
After previous patches, we should be generating no combination ports, so
we don't need to store multiple devices per port. Simplify the code
based on this.

Link: bf1708213b
Signed-off-by: Alper Nebi Yasak <alpernebiyasak@gmail.com>
2023-12-04 15:34:48 +00:00

2047 lines
60 KiB
C

/* ALSA Card Profile */
/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */
/* SPDX-License-Identifier: MIT */
#include "acp.h"
#include "alsa-mixer.h"
#include "alsa-ucm.h"
#include <spa/utils/string.h>
#include <spa/utils/json.h>
int _acp_log_level = 1;
acp_log_func _acp_log_func;
void *_acp_log_data;
struct spa_i18n *acp_i18n;
#define DEFAULT_RATE 48000
#define VOLUME_ACCURACY (PA_VOLUME_NORM/100) /* don't require volume adjustments to be perfectly correct. don't necessarily extend granularity in software unless the differences get greater than this level */
static const uint32_t channel_table[PA_CHANNEL_POSITION_MAX] = {
[PA_CHANNEL_POSITION_MONO] = ACP_CHANNEL_MONO,
[PA_CHANNEL_POSITION_FRONT_LEFT] = ACP_CHANNEL_FL,
[PA_CHANNEL_POSITION_FRONT_RIGHT] = ACP_CHANNEL_FR,
[PA_CHANNEL_POSITION_FRONT_CENTER] = ACP_CHANNEL_FC,
[PA_CHANNEL_POSITION_REAR_CENTER] = ACP_CHANNEL_RC,
[PA_CHANNEL_POSITION_REAR_LEFT] = ACP_CHANNEL_RL,
[PA_CHANNEL_POSITION_REAR_RIGHT] = ACP_CHANNEL_RR,
[PA_CHANNEL_POSITION_LFE] = ACP_CHANNEL_LFE,
[PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER] = ACP_CHANNEL_FLC,
[PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER] = ACP_CHANNEL_FRC,
[PA_CHANNEL_POSITION_SIDE_LEFT] = ACP_CHANNEL_SL,
[PA_CHANNEL_POSITION_SIDE_RIGHT] = ACP_CHANNEL_SR,
[PA_CHANNEL_POSITION_AUX0] = ACP_CHANNEL_START_Aux + 0,
[PA_CHANNEL_POSITION_AUX1] = ACP_CHANNEL_START_Aux + 1,
[PA_CHANNEL_POSITION_AUX2] = ACP_CHANNEL_START_Aux + 2,
[PA_CHANNEL_POSITION_AUX3] = ACP_CHANNEL_START_Aux + 3,
[PA_CHANNEL_POSITION_AUX4] = ACP_CHANNEL_START_Aux + 4,
[PA_CHANNEL_POSITION_AUX5] = ACP_CHANNEL_START_Aux + 5,
[PA_CHANNEL_POSITION_AUX6] = ACP_CHANNEL_START_Aux + 6,
[PA_CHANNEL_POSITION_AUX7] = ACP_CHANNEL_START_Aux + 7,
[PA_CHANNEL_POSITION_AUX8] = ACP_CHANNEL_START_Aux + 8,
[PA_CHANNEL_POSITION_AUX9] = ACP_CHANNEL_START_Aux + 9,
[PA_CHANNEL_POSITION_AUX10] = ACP_CHANNEL_START_Aux + 10,
[PA_CHANNEL_POSITION_AUX11] = ACP_CHANNEL_START_Aux + 11,
[PA_CHANNEL_POSITION_AUX12] = ACP_CHANNEL_START_Aux + 12,
[PA_CHANNEL_POSITION_AUX13] = ACP_CHANNEL_START_Aux + 13,
[PA_CHANNEL_POSITION_AUX14] = ACP_CHANNEL_START_Aux + 14,
[PA_CHANNEL_POSITION_AUX15] = ACP_CHANNEL_START_Aux + 15,
[PA_CHANNEL_POSITION_AUX16] = ACP_CHANNEL_START_Aux + 16,
[PA_CHANNEL_POSITION_AUX17] = ACP_CHANNEL_START_Aux + 17,
[PA_CHANNEL_POSITION_AUX18] = ACP_CHANNEL_START_Aux + 18,
[PA_CHANNEL_POSITION_AUX19] = ACP_CHANNEL_START_Aux + 19,
[PA_CHANNEL_POSITION_AUX20] = ACP_CHANNEL_START_Aux + 20,
[PA_CHANNEL_POSITION_AUX21] = ACP_CHANNEL_START_Aux + 21,
[PA_CHANNEL_POSITION_AUX22] = ACP_CHANNEL_START_Aux + 22,
[PA_CHANNEL_POSITION_AUX23] = ACP_CHANNEL_START_Aux + 23,
[PA_CHANNEL_POSITION_AUX24] = ACP_CHANNEL_START_Aux + 24,
[PA_CHANNEL_POSITION_AUX25] = ACP_CHANNEL_START_Aux + 25,
[PA_CHANNEL_POSITION_AUX26] = ACP_CHANNEL_START_Aux + 26,
[PA_CHANNEL_POSITION_AUX27] = ACP_CHANNEL_START_Aux + 27,
[PA_CHANNEL_POSITION_AUX28] = ACP_CHANNEL_START_Aux + 28,
[PA_CHANNEL_POSITION_AUX29] = ACP_CHANNEL_START_Aux + 29,
[PA_CHANNEL_POSITION_AUX30] = ACP_CHANNEL_START_Aux + 30,
[PA_CHANNEL_POSITION_AUX31] = ACP_CHANNEL_START_Aux + 31,
[PA_CHANNEL_POSITION_TOP_CENTER] = ACP_CHANNEL_TC,
[PA_CHANNEL_POSITION_TOP_FRONT_LEFT] = ACP_CHANNEL_TFL,
[PA_CHANNEL_POSITION_TOP_FRONT_RIGHT] = ACP_CHANNEL_TFR,
[PA_CHANNEL_POSITION_TOP_FRONT_CENTER] = ACP_CHANNEL_TFC,
[PA_CHANNEL_POSITION_TOP_REAR_LEFT] = ACP_CHANNEL_TRL,
[PA_CHANNEL_POSITION_TOP_REAR_RIGHT] = ACP_CHANNEL_TRR,
[PA_CHANNEL_POSITION_TOP_REAR_CENTER] = ACP_CHANNEL_TRC,
};
static const char *channel_names[] = {
[ACP_CHANNEL_UNKNOWN] = "UNK",
[ACP_CHANNEL_NA] = "NA",
[ACP_CHANNEL_MONO] = "MONO",
[ACP_CHANNEL_FL] = "FL",
[ACP_CHANNEL_FR] = "FR",
[ACP_CHANNEL_FC] = "FC",
[ACP_CHANNEL_LFE] = "LFE",
[ACP_CHANNEL_SL] = "SL",
[ACP_CHANNEL_SR] = "SR",
[ACP_CHANNEL_FLC] = "FLC",
[ACP_CHANNEL_FRC] = "FRC",
[ACP_CHANNEL_RC] = "RC",
[ACP_CHANNEL_RL] = "RL",
[ACP_CHANNEL_RR] = "RR",
[ACP_CHANNEL_TC] = "TC",
[ACP_CHANNEL_TFL] = "TFL",
[ACP_CHANNEL_TFC] = "TFC",
[ACP_CHANNEL_TFR] = "TFR",
[ACP_CHANNEL_TRL] = "TRL",
[ACP_CHANNEL_TRC] = "TRC",
[ACP_CHANNEL_TRR] = "TRR",
[ACP_CHANNEL_RLC] = "RLC",
[ACP_CHANNEL_RRC] = "RRC",
[ACP_CHANNEL_FLW] = "FLW",
[ACP_CHANNEL_FRW] = "FRW",
[ACP_CHANNEL_LFE2] = "LFE2",
[ACP_CHANNEL_FLH] = "FLH",
[ACP_CHANNEL_FCH] = "FCH",
[ACP_CHANNEL_FRH] = "FRH",
[ACP_CHANNEL_TFLC] = "TFLC",
[ACP_CHANNEL_TFRC] = "TFRC",
[ACP_CHANNEL_TSL] = "TSL",
[ACP_CHANNEL_TSR] = "TSR",
[ACP_CHANNEL_LLFE] = "LLFE",
[ACP_CHANNEL_RLFE] = "RLFE",
[ACP_CHANNEL_BC] = "BC",
[ACP_CHANNEL_BLC] = "BLC",
[ACP_CHANNEL_BRC] = "BRC",
};
#define ACP_N_ELEMENTS(arr) (sizeof(arr) / sizeof((arr)[0]))
static inline uint32_t channel_pa2acp(pa_channel_position_t channel)
{
if (channel < 0 || (size_t)channel >= ACP_N_ELEMENTS(channel_table))
return ACP_CHANNEL_UNKNOWN;
return channel_table[channel];
}
char *acp_channel_str(char *buf, size_t len, enum acp_channel ch)
{
if (ch >= ACP_CHANNEL_START_Aux && ch <= ACP_CHANNEL_LAST_Aux) {
snprintf(buf, len, "AUX%d", ch - ACP_CHANNEL_START_Aux);
} else if (ch >= ACP_CHANNEL_UNKNOWN && ch <= ACP_CHANNEL_BRC) {
snprintf(buf, len, "%s", channel_names[ch]);
} else {
snprintf(buf, len, "UNK");
}
return buf;
}
const char *acp_available_str(enum acp_available status)
{
switch (status) {
case ACP_AVAILABLE_UNKNOWN:
return "unknown";
case ACP_AVAILABLE_NO:
return "no";
case ACP_AVAILABLE_YES:
return "yes";
}
return "error";
}
const char *acp_direction_str(enum acp_direction direction)
{
switch (direction) {
case ACP_DIRECTION_CAPTURE:
return "capture";
case ACP_DIRECTION_PLAYBACK:
return "playback";
}
return "error";
}
static void port_free(void *data)
{
pa_device_port *dp = data;
pa_dynarray_clear(&dp->devices);
pa_dynarray_clear(&dp->prof);
pa_device_port_free(dp);
}
static void device_free(void *data)
{
pa_alsa_device *dev = data;
pa_dynarray_clear(&dev->port_array);
pa_proplist_free(dev->proplist);
pa_hashmap_free(dev->ports);
}
static inline void channelmap_to_acp(pa_channel_map *m, uint32_t *map)
{
uint32_t i, j;
for (i = 0; i < m->channels; i++) {
map[i] = channel_pa2acp(m->map[i]);
for (j = 0; j < i; j++) {
if (map[i] == map[j])
map[i] += 32;
}
}
}
static void init_device(pa_card *impl, pa_alsa_device *dev, pa_alsa_direction_t direction,
pa_alsa_mapping *m, uint32_t index)
{
char **d;
dev->card = impl;
dev->mapping = m;
dev->device.index = index;
dev->device.name = m->name;
dev->device.description = m->description;
dev->device.priority = m->priority;
dev->device.device_strings = (const char **)m->device_strings;
dev->device.format.format_mask = m->sample_spec.format;
dev->device.format.rate_mask = m->sample_spec.rate;
dev->device.format.channels = m->channel_map.channels;
pa_cvolume_reset(&dev->real_volume, dev->device.format.channels);
pa_cvolume_reset(&dev->soft_volume, dev->device.format.channels);
channelmap_to_acp(&m->channel_map, dev->device.format.map);
dev->direction = direction;
dev->proplist = pa_proplist_new();
pa_proplist_update(dev->proplist, PA_UPDATE_REPLACE, m->proplist);
if (direction == PA_ALSA_DIRECTION_OUTPUT) {
dev->mixer_path_set = m->output_path_set;
dev->pcm_handle = m->output_pcm;
dev->device.direction = ACP_DIRECTION_PLAYBACK;
pa_proplist_update(dev->proplist, PA_UPDATE_REPLACE, m->output_proplist);
} else {
dev->mixer_path_set = m->input_path_set;
dev->pcm_handle = m->input_pcm;
dev->device.direction = ACP_DIRECTION_CAPTURE;
pa_proplist_update(dev->proplist, PA_UPDATE_REPLACE, m->input_proplist);
}
pa_proplist_sets(dev->proplist, PA_PROP_DEVICE_PROFILE_NAME, m->name);
pa_proplist_sets(dev->proplist, PA_PROP_DEVICE_PROFILE_DESCRIPTION, m->description);
pa_proplist_setf(dev->proplist, "card.profile.device", "%u", index);
pa_proplist_as_dict(dev->proplist, &dev->device.props);
dev->ports = pa_hashmap_new(pa_idxset_string_hash_func,
pa_idxset_string_compare_func);
if (m->ucm_context.ucm) {
dev->ucm_context = &m->ucm_context;
if (impl->ucm.alib_prefix != NULL) {
for (d = m->device_strings; *d; d++) {
if (pa_startswith(*d, impl->ucm.alib_prefix)) {
size_t plen = strlen(impl->ucm.alib_prefix);
size_t len = strlen(*d);
memmove(*d, (*d) + plen, len - plen + 1);
dev->device.flags |= ACP_DEVICE_UCM_DEVICE;
}
}
}
}
for (d = m->device_strings; *d; d++) {
if (pa_startswith(*d, "iec958") ||
pa_startswith(*d, "hdmi"))
dev->device.flags |= ACP_DEVICE_IEC958;
}
pa_dynarray_init(&dev->port_array, NULL);
}
static int compare_profile(const void *a, const void *b)
{
const pa_hashmap_item *i1 = a;
const pa_hashmap_item *i2 = b;
const pa_alsa_profile *p1, *p2;
if (i1->key == NULL || i2->key == NULL)
return 0;
p1 = i1->value;
p2 = i2->value;
if (p1->profile.priority == 0 || p2->profile.priority == 0)
return 0;
return p2->profile.priority - p1->profile.priority;
}
static void profile_free(void *data)
{
pa_alsa_profile *ap = data;
pa_dynarray_clear(&ap->out.devices);
if (ap->profile.flags & ACP_PROFILE_OFF) {
free(ap->name);
free(ap->description);
free(ap);
}
}
static const char *find_best_verb(pa_card *impl)
{
const char *res = NULL;
unsigned prio = 0;
pa_alsa_ucm_verb *verb;
PA_LLIST_FOREACH(verb, impl->ucm.verbs) {
if (verb->priority >= prio)
res = pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME);
}
return res;
}
static int add_pro_profile(pa_card *impl, uint32_t index)
{
snd_ctl_t *ctl_hndl;
int err, dev, count = 0, n_capture = 0, n_playback = 0;
pa_alsa_profile *ap;
pa_alsa_profile_set *ps = impl->profile_set;
pa_alsa_mapping *m;
char *device;
snd_pcm_info_t *pcminfo;
pa_sample_spec ss;
snd_pcm_uframes_t try_period_size, try_buffer_size;
uint32_t idx;
if (impl->use_ucm) {
const char *verb = find_best_verb(impl);
if (verb == NULL)
return -ENOTSUP;
if ((err = snd_use_case_set(impl->ucm.ucm_mgr, "_verb", verb)) < 0) {
pa_log_error("error setting verb: %s", snd_strerror(err));
return err;
}
}
ss.format = PA_SAMPLE_S32LE;
ss.rate = impl->rate;
ss.channels = impl->pro_channels;
ap = pa_xnew0(pa_alsa_profile, 1);
ap->profile_set = ps;
ap->profile.name = ap->name = pa_xstrdup("pro-audio");
ap->profile.description = ap->description = pa_xstrdup(_("Pro Audio"));
ap->profile.available = ACP_AVAILABLE_YES;
ap->profile.flags = ACP_PROFILE_PRO;
ap->output_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
ap->input_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
pa_hashmap_put(ps->profiles, ap->name, ap);
ap->output_name = pa_xstrdup("pro-output");
ap->input_name = pa_xstrdup("pro-input");
ap->priority = 1;
pa_assert_se(asprintf(&device, "hw:%d", index) >= 0);
if ((err = snd_ctl_open(&ctl_hndl, device, 0)) < 0) {
pa_log_error("can't open control for card %s: %s",
device, snd_strerror(err));
free(device);
return err;
}
free(device);
snd_pcm_info_alloca(&pcminfo);
dev = -1;
while (1) {
char desc[128], devstr[128], *name;
if ((err = snd_ctl_pcm_next_device(ctl_hndl, &dev)) < 0) {
pa_log_error("error iterating devices: %s", snd_strerror(err));
break;
}
if (dev < 0)
break;
snd_pcm_info_set_device(pcminfo, dev);
snd_pcm_info_set_subdevice(pcminfo, 0);
snprintf(devstr, sizeof(devstr), "hw:%d,%d", index, dev);
if (count++ == 0)
snprintf(desc, sizeof(desc), "Pro");
else
snprintf(desc, sizeof(desc), "Pro %d", dev);
snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_PLAYBACK);
if ((err = snd_ctl_pcm_info(ctl_hndl, pcminfo)) < 0) {
if (err != -ENOENT)
pa_log_error("error pcm info: %s", snd_strerror(err));
}
if (err >= 0) {
pa_assert_se(asprintf(&name, "Mapping pro-output-%d", dev) >= 0);
m = pa_alsa_mapping_get(ps, name);
m->description = pa_xstrdup(desc);
m->device_strings = pa_split_spaces_strv(devstr);
try_period_size = 1024;
try_buffer_size = 1024 * 64;
m->sample_spec = ss;
if ((m->output_pcm = pa_alsa_open_by_template(m->device_strings,
devstr, NULL, &m->sample_spec,
&m->channel_map, SND_PCM_STREAM_PLAYBACK,
&try_period_size, &try_buffer_size,
0, NULL, NULL, NULL, NULL, false))) {
pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm);
pa_proplist_setf(m->output_proplist, "clock.name", "api.alsa.%u", index);
pa_proplist_setf(m->output_proplist, "device.profile.pro", "true");
pa_alsa_close(&m->output_pcm);
m->supported = true;
pa_channel_map_init_auto(&m->channel_map, m->sample_spec.channels, PA_CHANNEL_MAP_AUX);
n_playback++;
}
pa_idxset_put(ap->output_mappings, m, NULL);
free(name);
}
snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_CAPTURE);
if ((err = snd_ctl_pcm_info(ctl_hndl, pcminfo)) < 0) {
if (err != -ENOENT)
pa_log_error("error pcm info: %s", snd_strerror(err));
}
if (err >= 0) {
pa_assert_se(asprintf(&name, "Mapping pro-input-%d", dev) >= 0);
m = pa_alsa_mapping_get(ps, name);
m->description = pa_xstrdup(desc);
m->device_strings = pa_split_spaces_strv(devstr);
try_period_size = 1024;
try_buffer_size = 1024 * 64;
m->sample_spec = ss;
if ((m->input_pcm = pa_alsa_open_by_template(m->device_strings,
devstr, NULL, &m->sample_spec,
&m->channel_map, SND_PCM_STREAM_CAPTURE,
&try_period_size, &try_buffer_size,
0, NULL, NULL, NULL, NULL, false))) {
pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm);
pa_proplist_setf(m->input_proplist, "clock.name", "api.alsa.%u", index);
pa_proplist_setf(m->input_proplist, "device.profile.pro", "true");
pa_alsa_close(&m->input_pcm);
m->supported = true;
pa_channel_map_init_auto(&m->channel_map, m->sample_spec.channels, PA_CHANNEL_MAP_AUX);
n_capture++;
}
pa_idxset_put(ap->input_mappings, m, NULL);
free(name);
}
}
snd_ctl_close(ctl_hndl);
if (n_capture == 1 && n_playback == 1) {
PA_IDXSET_FOREACH(m, ap->output_mappings, idx) {
pa_proplist_setf(m->output_proplist, "node.group", "pro-audio-%u", index);
pa_proplist_setf(m->output_proplist, "node.link-group", "pro-audio-%u", index);
pa_proplist_setf(m->output_proplist, "api.alsa.auto-link", "true");
pa_proplist_setf(m->output_proplist, "api.alsa.disable-tsched", "true");
}
PA_IDXSET_FOREACH(m, ap->input_mappings, idx) {
pa_proplist_setf(m->input_proplist, "node.group", "pro-audio-%u", index);
pa_proplist_setf(m->input_proplist, "node.link-group", "pro-audio-%u", index);
pa_proplist_setf(m->input_proplist, "api.alsa.auto-link", "true");
pa_proplist_setf(m->input_proplist, "api.alsa.disable-tsched", "true");
}
}
return 0;
}
static bool contains_string(const char *arr, const char *str)
{
struct spa_json it[2];
char v[256];
if (arr == NULL || str == NULL)
return false;
spa_json_init(&it[0], arr, strlen(arr));
if (spa_json_enter_array(&it[0], &it[1]) <= 0)
spa_json_init(&it[1], arr, strlen(arr));
while (spa_json_get_string(&it[1], v, sizeof(v)) > 0) {
if (spa_streq(v, str))
return true;
}
return false;
}
static void add_profiles(pa_card *impl)
{
pa_alsa_profile *ap;
void *state;
struct acp_card_profile *cp;
pa_device_port *dp;
pa_alsa_device *dev;
int n_profiles, n_ports, n_devices;
uint32_t idx;
const char *arr;
n_devices = 0;
pa_dynarray_init(&impl->out.devices, device_free);
ap = pa_xnew0(pa_alsa_profile, 1);
ap->profile.name = ap->name = pa_xstrdup("off");
ap->profile.description = ap->description = pa_xstrdup(_("Off"));
ap->profile.available = ACP_AVAILABLE_YES;
ap->profile.flags = ACP_PROFILE_OFF;
pa_hashmap_put(impl->profiles, ap->name, ap);
add_pro_profile(impl, impl->card.index);
PA_HASHMAP_FOREACH(ap, impl->profile_set->profiles, state) {
pa_alsa_mapping *m;
cp = &ap->profile;
cp->name = ap->name;
cp->description = ap->description;
cp->priority = ap->priority ? ap->priority : 1;
pa_dynarray_init(&ap->out.devices, NULL);
if (ap->output_mappings) {
PA_IDXSET_FOREACH(m, ap->output_mappings, idx) {
dev = &m->output;
if (dev->mapping == NULL) {
init_device(impl, dev, PA_ALSA_DIRECTION_OUTPUT, m, n_devices++);
pa_dynarray_append(&impl->out.devices, dev);
}
if (impl->use_ucm) {
if (m->ucm_context.ucm_devices) {
pa_alsa_ucm_add_port(NULL, &m->ucm_context,
true, impl->ports, ap, NULL);
pa_alsa_ucm_add_ports(&dev->ports, m->proplist, &m->ucm_context,
true, impl, dev->pcm_handle, impl->profile_set->ignore_dB);
}
}
else
pa_alsa_path_set_add_ports(m->output_path_set, ap, impl->ports,
dev->ports, NULL);
pa_dynarray_append(&ap->out.devices, dev);
}
}
if (ap->input_mappings) {
PA_IDXSET_FOREACH(m, ap->input_mappings, idx) {
dev = &m->input;
if (dev->mapping == NULL) {
init_device(impl, dev, PA_ALSA_DIRECTION_INPUT, m, n_devices++);
pa_dynarray_append(&impl->out.devices, dev);
}
if (impl->use_ucm) {
if (m->ucm_context.ucm_devices) {
pa_alsa_ucm_add_port(NULL, &m->ucm_context,
false, impl->ports, ap, NULL);
pa_alsa_ucm_add_ports(&dev->ports, m->proplist, &m->ucm_context,
false, impl, dev->pcm_handle, impl->profile_set->ignore_dB);
}
} else
pa_alsa_path_set_add_ports(m->input_path_set, ap, impl->ports,
dev->ports, NULL);
pa_dynarray_append(&ap->out.devices, dev);
}
}
cp->n_devices = pa_dynarray_size(&ap->out.devices);
cp->devices = ap->out.devices.array.data;
pa_hashmap_put(impl->profiles, ap->name, cp);
}
pa_dynarray_init(&impl->out.ports, NULL);
n_ports = 0;
PA_HASHMAP_FOREACH(dp, impl->ports, state) {
void *state2;
dp->card = impl;
dp->port.index = n_ports++;
dp->port.priority = dp->priority;
pa_dynarray_init(&dp->prof, NULL);
pa_dynarray_init(&dp->devices, NULL);
n_profiles = 0;
PA_HASHMAP_FOREACH(cp, dp->profiles, state2) {
pa_dynarray_append(&dp->prof, cp);
n_profiles++;
}
dp->port.n_profiles = n_profiles;
dp->port.profiles = dp->prof.array.data;
pa_proplist_setf(dp->proplist, "card.profile.port", "%u", dp->port.index);
pa_proplist_as_dict(dp->proplist, &dp->port.props);
pa_dynarray_append(&impl->out.ports, dp);
}
PA_DYNARRAY_FOREACH(dev, &impl->out.devices, idx) {
PA_HASHMAP_FOREACH(dp, dev->ports, state) {
pa_dynarray_append(&dev->port_array, dp);
pa_dynarray_append(&dp->devices, dev);
}
dev->device.ports = dev->port_array.array.data;
dev->device.n_ports = pa_dynarray_size(&dev->port_array);
}
arr = pa_proplist_gets(impl->proplist, "api.acp.hidden-ports");
PA_HASHMAP_FOREACH(dp, impl->ports, state) {
if (contains_string(arr, dp->name))
dp->port.flags |= ACP_PORT_HIDDEN;
dp->port.devices = dp->devices.array.data;
dp->port.n_devices = pa_dynarray_size(&dp->devices);
}
pa_hashmap_sort(impl->profiles, compare_profile);
n_profiles = 0;
pa_dynarray_init(&impl->out.profiles, NULL);
arr = pa_proplist_gets(impl->proplist, "api.acp.hidden-profiles");
PA_HASHMAP_FOREACH(cp, impl->profiles, state) {
if (contains_string(arr, cp->name))
cp->flags |= ACP_PROFILE_HIDDEN;
cp->index = n_profiles++;
pa_dynarray_append(&impl->out.profiles, cp);
}
}
static pa_available_t calc_port_state(pa_device_port *p, pa_card *impl)
{
void *state;
pa_alsa_jack *jack;
pa_available_t pa = PA_AVAILABLE_UNKNOWN;
pa_device_port *port;
PA_HASHMAP_FOREACH(jack, impl->jacks, state) {
pa_available_t cpa;
if (impl->use_ucm)
port = pa_hashmap_get(impl->ports, jack->name);
else {
if (jack->path)
port = jack->path->port;
else
continue;
}
if (p != port)
continue;
cpa = jack->plugged_in ? jack->state_plugged : jack->state_unplugged;
if (cpa == PA_AVAILABLE_NO) {
/* If a plugged-in jack causes the availability to go to NO, it
* should override all other availability information (like a
* blacklist) so set and bail */
if (jack->plugged_in) {
pa = cpa;
break;
}
/* If the current availability is unknown go the more precise no,
* but otherwise don't change state */
if (pa == PA_AVAILABLE_UNKNOWN)
pa = cpa;
} else if (cpa == PA_AVAILABLE_YES) {
/* Output is available through at least one jack, so go to that
* level of availability. We still need to continue iterating through
* the jacks in case a jack is plugged in that forces the state to no
*/
pa = cpa;
}
}
return pa;
}
static void profile_set_available(pa_card *impl, uint32_t index,
enum acp_available status, bool emit)
{
struct acp_card_profile *p = impl->card.profiles[index];
enum acp_available old = p->available;
if (old != status)
pa_log_info("Profile %s available %s -> %s", p->name,
acp_available_str(old), acp_available_str(status));
p->available = status;
if (emit && impl->events && impl->events->profile_available)
impl->events->profile_available(impl->user_data, index,
old, status);
}
struct temp_port_avail {
pa_device_port *port;
pa_available_t avail;
};
static int report_jack_state(snd_mixer_elem_t *melem, unsigned int mask)
{
pa_card *impl = snd_mixer_elem_get_callback_private(melem);
snd_hctl_elem_t **_elem = snd_mixer_elem_get_private(melem), *elem;
snd_ctl_elem_value_t *elem_value;
bool plugged_in, any_input_port_available;
void *state;
pa_alsa_jack *jack;
struct temp_port_avail *tp, *tports;
pa_alsa_profile *profile;
enum acp_available active_available = ACP_AVAILABLE_UNKNOWN;
size_t size;
pa_assert(_elem);
elem = *_elem;
#if 0
/* Changing the jack state may cause a port change, and a port change will
* make the sink or source change the mixer settings. If there are multiple
* users having pulseaudio running, the mixer changes done by inactive
* users may mess up the volume settings for the active users, because when
* the inactive users change the mixer settings, those changes are picked
* up by the active user's pulseaudio instance and the changes are
* interpreted as if the active user changed the settings manually e.g.
* with alsamixer. Even single-user systems suffer from this, because gdm
* runs its own pulseaudio instance.
*
* We rerun this function when being unsuspended to catch up on jack state
* changes */
if (u->card->suspend_cause & PA_SUSPEND_SESSION)
return 0;
#endif
if (mask == SND_CTL_EVENT_MASK_REMOVE)
return 0;
snd_ctl_elem_value_alloca(&elem_value);
if (snd_hctl_elem_read(elem, elem_value) < 0) {
pa_log_warn("Failed to read jack detection from '%s'", pa_strnull(snd_hctl_elem_get_name(elem)));
return 0;
}
plugged_in = !!snd_ctl_elem_value_get_boolean(elem_value, 0);
pa_log_debug("Jack '%s' is now %s", pa_strnull(snd_hctl_elem_get_name(elem)),
plugged_in ? "plugged in" : "unplugged");
size = sizeof(struct temp_port_avail) * (pa_hashmap_size(impl->jacks)+1);
tports = tp = alloca(size);
memset(tports, 0, size);
PA_HASHMAP_FOREACH(jack, impl->jacks, state)
if (jack->melem == melem) {
pa_alsa_jack_set_plugged_in(jack, plugged_in);
if (impl->use_ucm) {
/* When using UCM, pa_alsa_jack_set_plugged_in() maps the jack
* state to port availability. */
continue;
}
/* When not using UCM, we have to do the jack state -> port
* availability mapping ourselves. */
pa_assert_se(tp->port = jack->path->port);
tp->avail = calc_port_state(tp->port, impl);
tp++;
}
/* Report available ports before unavailable ones: in case port 1
* becomes available when port 2 becomes unavailable,
* this prevents an unnecessary switch port 1 -> port 3 -> port 2 */
for (tp = tports; tp->port; tp++)
if (tp->avail != PA_AVAILABLE_NO)
pa_device_port_set_available(tp->port, tp->avail);
for (tp = tports; tp->port; tp++)
if (tp->avail == PA_AVAILABLE_NO)
pa_device_port_set_available(tp->port, tp->avail);
for (tp = tports; tp->port; tp++) {
pa_alsa_port_data *data;
data = PA_DEVICE_PORT_DATA(tp->port);
if (!data->suspend_when_unavailable)
continue;
#if 0
pa_sink *sink;
uint32_t idx;
PA_IDXSET_FOREACH(sink, u->core->sinks, idx) {
if (sink->active_port == tp->port)
pa_sink_suspend(sink, tp->avail == PA_AVAILABLE_NO, PA_SUSPEND_UNAVAILABLE);
}
#endif
}
/* Update profile availabilities. Ideally we would mark all profiles
* unavailable that contain unavailable devices. We can't currently do that
* in all cases, because if there are multiple sinks in a profile, and the
* profile contains a mix of available and unavailable ports, we don't know
* how the ports are distributed between the different sinks. It's possible
* that some sinks contain only unavailable ports, in which case we should
* mark the profile as unavailable, but it's also possible that all sinks
* contain at least one available port, in which case we should mark the
* profile as available. Until the data structures are improved so that we
* can distinguish between these two cases, we mark the problematic cases
* as available (well, "unknown" to be precise, but there's little
* practical difference).
*
* When all output ports are unavailable, we know that all sinks are
* unavailable, and therefore the profile is marked unavailable as well.
* The same applies to input ports as well, of course.
*
* If there are no output ports at all, but the profile contains at least
* one sink, then the output is considered to be available. */
if (impl->card.active_profile_index != ACP_INVALID_INDEX)
active_available = impl->card.profiles[impl->card.active_profile_index]->available;
/* First round - detect, if we have any input port available.
If the hardware can report the state for all I/O jacks, only speakers
may be plugged in. */
any_input_port_available = false;
PA_HASHMAP_FOREACH(profile, impl->profiles, state) {
pa_device_port *port;
void *state2;
if (profile->profile.flags & ACP_PROFILE_OFF)
continue;
PA_HASHMAP_FOREACH(port, impl->ports, state2) {
if (!pa_hashmap_get(port->profiles, profile->profile.name))
continue;
if (port->port.direction == ACP_DIRECTION_CAPTURE &&
port->port.available != ACP_AVAILABLE_NO) {
any_input_port_available = true;
goto input_port_found;
}
}
}
input_port_found:
/* Second round */
PA_HASHMAP_FOREACH(profile, impl->profiles, state) {
pa_device_port *port;
void *state2;
bool has_input_port = false;
bool has_output_port = false;
bool found_available_input_port = false;
bool found_available_output_port = false;
enum acp_available available = ACP_AVAILABLE_UNKNOWN;
if (profile->profile.flags & ACP_PROFILE_OFF)
continue;
PA_HASHMAP_FOREACH(port, impl->ports, state2) {
if (!pa_hashmap_get(port->profiles, profile->profile.name))
continue;
if (port->port.direction == ACP_DIRECTION_CAPTURE) {
has_input_port = true;
if (port->port.available != ACP_AVAILABLE_NO)
found_available_input_port = true;
} else {
has_output_port = true;
if (port->port.available != ACP_AVAILABLE_NO)
found_available_output_port = true;
}
}
if ((has_input_port && !found_available_input_port) ||
(has_output_port && !found_available_output_port))
available = ACP_AVAILABLE_NO;
if (has_input_port && !has_output_port && found_available_input_port)
available = ACP_AVAILABLE_YES;
if (has_output_port && (!has_input_port || !any_input_port_available) && found_available_output_port)
available = ACP_AVAILABLE_YES;
if (has_output_port && has_input_port && found_available_output_port && found_available_input_port)
available = ACP_AVAILABLE_YES;
/* We want to update the active profile's status last, so logic that
* may change the active profile based on profile availability status
* has an updated view of all profiles' availabilities. */
if (profile->profile.index == impl->card.active_profile_index)
active_available = available;
else
profile_set_available(impl, profile->profile.index, available, false);
}
if (impl->card.active_profile_index != ACP_INVALID_INDEX)
profile_set_available(impl, impl->card.active_profile_index, active_available, true);
return 0;
}
static void init_jacks(pa_card *impl)
{
void *state;
pa_alsa_path* path;
pa_alsa_jack* jack;
char buf[64];
impl->jacks = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
if (impl->use_ucm) {
PA_LLIST_FOREACH(jack, impl->ucm.jacks)
if (jack->has_control)
pa_hashmap_put(impl->jacks, jack, jack);
} else {
/* See if we have any jacks */
if (impl->profile_set->output_paths)
PA_HASHMAP_FOREACH(path, impl->profile_set->output_paths, state)
PA_LLIST_FOREACH(jack, path->jacks)
if (jack->has_control)
pa_hashmap_put(impl->jacks, jack, jack);
if (impl->profile_set->input_paths)
PA_HASHMAP_FOREACH(path, impl->profile_set->input_paths, state)
PA_LLIST_FOREACH(jack, path->jacks)
if (jack->has_control)
pa_hashmap_put(impl->jacks, jack, jack);
}
pa_log_debug("Found %d jacks.", pa_hashmap_size(impl->jacks));
if (pa_hashmap_size(impl->jacks) == 0)
return;
PA_HASHMAP_FOREACH(jack, impl->jacks, state) {
if (!jack->mixer_device_name) {
jack->mixer_handle = pa_alsa_open_mixer(impl->ucm.mixers, impl->card.index, false);
if (!jack->mixer_handle) {
pa_log("Failed to open mixer for card %d for jack detection", impl->card.index);
continue;
}
} else {
jack->mixer_handle = pa_alsa_open_mixer_by_name(impl->ucm.mixers, jack->mixer_device_name, false);
if (!jack->mixer_handle) {
pa_log("Failed to open mixer '%s' for jack detection", jack->mixer_device_name);
continue;
}
}
pa_alsa_mixer_use_for_poll(impl->ucm.mixers, jack->mixer_handle);
jack->melem = pa_alsa_mixer_find_card(jack->mixer_handle, &jack->alsa_id, 0);
if (!jack->melem) {
pa_alsa_mixer_id_to_string(buf, sizeof(buf), &jack->alsa_id);
pa_log_warn("Jack '%s' seems to have disappeared.", buf);
pa_alsa_jack_set_has_control(jack, false);
continue;
}
snd_mixer_elem_set_callback(jack->melem, report_jack_state);
snd_mixer_elem_set_callback_private(jack->melem, impl);
report_jack_state(jack->melem, 0);
}
}
static pa_device_port* find_port_with_eld_device(pa_card *impl, int device)
{
void *state;
pa_device_port *p;
if (impl->use_ucm) {
PA_HASHMAP_FOREACH(p, impl->ports, state) {
pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(p);
pa_assert(data->eld_mixer_device_name);
if (device == data->eld_device)
return p;
}
} else {
PA_HASHMAP_FOREACH(p, impl->ports, state) {
pa_alsa_port_data *data = PA_DEVICE_PORT_DATA(p);
pa_assert(data->path);
if (device == data->path->eld_device)
return p;
}
}
return NULL;
}
static int hdmi_eld_changed(snd_mixer_elem_t *melem, unsigned int mask)
{
pa_card *impl = snd_mixer_elem_get_callback_private(melem);
snd_hctl_elem_t **_elem = snd_mixer_elem_get_private(melem), *elem;
int device;
const char *old_monitor_name;
pa_device_port *p;
pa_hdmi_eld eld;
bool changed = false;
pa_assert(_elem);
elem = *_elem;
device = snd_hctl_elem_get_device(elem);
if (mask == SND_CTL_EVENT_MASK_REMOVE)
return 0;
p = find_port_with_eld_device(impl, device);
if (p == NULL) {
pa_log_error("Invalid device changed in ALSA: %d", device);
return 0;
}
if (pa_alsa_get_hdmi_eld(elem, &eld) < 0)
memset(&eld, 0, sizeof(eld));
old_monitor_name = pa_proplist_gets(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME);
if (eld.monitor_name[0] == '\0') {
changed |= old_monitor_name != NULL;
pa_proplist_unset(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME);
} else {
changed |= (old_monitor_name == NULL) || (!spa_streq(old_monitor_name, eld.monitor_name));
pa_proplist_sets(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME, eld.monitor_name);
}
pa_proplist_as_dict(p->proplist, &p->port.props);
if (changed && mask != 0 && impl->events && impl->events->props_changed)
impl->events->props_changed(impl->user_data);
return 0;
}
static void init_eld_ctls(pa_card *impl)
{
void *state;
pa_device_port *port;
/* The code in this function expects ports to have a pa_alsa_port_data
* struct as their data, but in UCM mode ports don't have any data. Hence,
* the ELD controls can't currently be used in UCM mode. */
PA_HASHMAP_FOREACH(port, impl->ports, state) {
snd_mixer_t *mixer_handle;
snd_mixer_elem_t* melem;
int device;
if (impl->use_ucm) {
pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(port);
device = data->eld_device;
if (device < 0 || !data->eld_mixer_device_name)
continue;
mixer_handle = pa_alsa_open_mixer_by_name(impl->ucm.mixers, data->eld_mixer_device_name, true);
} else {
pa_alsa_port_data *data = PA_DEVICE_PORT_DATA(port);
pa_assert(data->path);
device = data->path->eld_device;
if (device < 0)
continue;
mixer_handle = pa_alsa_open_mixer(impl->ucm.mixers, impl->card.index, true);
}
if (!mixer_handle)
continue;
melem = pa_alsa_mixer_find_pcm(mixer_handle, "ELD", device);
if (melem) {
pa_alsa_mixer_use_for_poll(impl->ucm.mixers, mixer_handle);
snd_mixer_elem_set_callback(melem, hdmi_eld_changed);
snd_mixer_elem_set_callback_private(melem, impl);
hdmi_eld_changed(melem, 0);
pa_log_info("ELD device found for port %s (%d).", port->port.name, device);
}
else
pa_log_debug("No ELD device found for port %s (%d).", port->port.name, device);
}
}
uint32_t acp_card_find_best_profile_index(struct acp_card *card, const char *name)
{
uint32_t i;
uint32_t best, best2, off;
struct acp_card_profile **profiles = card->profiles;
best = best2 = ACP_INVALID_INDEX;
off = 0;
for (i = 0; i < card->n_profiles; i++) {
struct acp_card_profile *p = profiles[i];
if (SPA_FLAG_IS_SET(p->flags, ACP_PROFILE_HIDDEN))
continue;
if (name) {
if (spa_streq(name, p->name))
best = i;
} else if (p->flags & ACP_PROFILE_OFF) {
off = i;
} else if (p->available == ACP_AVAILABLE_YES) {
if (best == ACP_INVALID_INDEX || p->priority > profiles[best]->priority)
best = i;
} else if (p->available != ACP_AVAILABLE_NO) {
if (best2 == ACP_INVALID_INDEX || p->priority > profiles[best2]->priority)
best2 = i;
}
}
if (best == ACP_INVALID_INDEX)
best = best2;
if (best == ACP_INVALID_INDEX)
best = off;
return best;
}
static void find_mixer(pa_card *impl, pa_alsa_device *dev, const char *element, bool ignore_dB)
{
const char *mdev;
pa_alsa_mapping *mapping = dev->mapping;
if (!mapping && !element)
return;
if (!element && mapping && pa_alsa_path_set_is_empty(dev->mixer_path_set))
return;
mdev = pa_proplist_gets(mapping->proplist, "alsa.mixer_device");
if (mdev) {
dev->mixer_handle = pa_alsa_open_mixer_by_name(impl->ucm.mixers, mdev, true);
} else {
dev->mixer_handle = pa_alsa_open_mixer(impl->ucm.mixers, impl->card.index, true);
}
if (!dev->mixer_handle) {
pa_log_info("Failed to find a working mixer device.");
return;
}
if (element) {
if (!(dev->mixer_path = pa_alsa_path_synthesize(element, dev->direction)))
goto fail;
if (pa_alsa_path_probe(dev->mixer_path, NULL, dev->mixer_handle, ignore_dB) < 0)
goto fail;
pa_log_debug("Probed mixer path %s:", dev->mixer_path->name);
pa_alsa_path_dump(dev->mixer_path);
}
return;
fail:
if (dev->mixer_path) {
pa_alsa_path_free(dev->mixer_path);
dev->mixer_path = NULL;
}
dev->mixer_handle = NULL;
}
static int mixer_callback(snd_mixer_elem_t *elem, unsigned int mask)
{
pa_alsa_device *dev = snd_mixer_elem_get_callback_private(elem);
if (mask == SND_CTL_EVENT_MASK_REMOVE)
return 0;
pa_log_info("%p mixer changed %d", dev, mask);
if (mask & SND_CTL_EVENT_MASK_VALUE) {
if (dev->read_volume)
dev->read_volume(dev);
if (dev->read_mute)
dev->read_mute(dev);
}
return 0;
}
static int read_volume(pa_alsa_device *dev)
{
pa_card *impl = dev->card;
pa_cvolume r;
uint32_t i;
int res;
if (!dev->mixer_handle)
return 0;
if ((res = pa_alsa_path_get_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, &r)) < 0)
return res;
/* Shift down by the base volume, so that 0dB becomes maximum volume */
pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume);
if (pa_cvolume_equal(&dev->hardware_volume, &r))
return 0;
dev->real_volume = dev->hardware_volume = r;
pa_log_info("New hardware volume: min:%d max:%d",
pa_cvolume_min(&r), pa_cvolume_max(&r));
for (i = 0; i < r.channels; i++)
pa_log_debug(" %d: %d", i, r.values[i]);
pa_cvolume_reset(&dev->soft_volume, r.channels);
if (impl->events && impl->events->volume_changed)
impl->events->volume_changed(impl->user_data, &dev->device);
return 0;
}
static void set_volume(pa_alsa_device *dev, const pa_cvolume *v)
{
pa_cvolume r;
if (v != &dev->real_volume)
dev->real_volume = *v;
if (!dev->mixer_handle)
return;
/* Shift up by the base volume */
pa_sw_cvolume_divide_scalar(&r, &dev->real_volume, dev->base_volume);
if (pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map,
&r, false, true) < 0)
return;
/* Shift down by the base volume, so that 0dB becomes maximum volume */
pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume);
dev->hardware_volume = r;
if (dev->mixer_path->has_dB) {
pa_cvolume new_soft_volume;
bool accurate_enough;
/* Match exactly what the user requested by software */
pa_sw_cvolume_divide(&new_soft_volume, &dev->real_volume, &dev->hardware_volume);
/* If the adjustment to do in software is only minimal we
* can skip it. That saves us CPU at the expense of a bit of
* accuracy */
accurate_enough =
(pa_cvolume_min(&new_soft_volume) >= (PA_VOLUME_NORM - VOLUME_ACCURACY)) &&
(pa_cvolume_max(&new_soft_volume) <= (PA_VOLUME_NORM + VOLUME_ACCURACY));
pa_log_debug("Requested volume: %d", pa_cvolume_max(&dev->real_volume));
pa_log_debug("Got hardware volume: %d", pa_cvolume_max(&dev->hardware_volume));
pa_log_debug("Calculated software volume: %d (accurate-enough=%s)",
pa_cvolume_max(&new_soft_volume),
pa_yes_no(accurate_enough));
if (accurate_enough)
pa_cvolume_reset(&new_soft_volume, new_soft_volume.channels);
dev->soft_volume = new_soft_volume;
} else {
pa_log_debug("Wrote hardware volume: %d", pa_cvolume_max(&r));
/* We can't match exactly what the user requested, hence let's
* at least tell the user about it */
dev->real_volume = r;
}
}
static int read_mute(pa_alsa_device *dev)
{
pa_card *impl = dev->card;
bool mute;
int res;
if (!dev->mixer_handle)
return 0;
if ((res = pa_alsa_path_get_mute(dev->mixer_path, dev->mixer_handle, &mute)) < 0)
return res;
if (mute == dev->muted)
return 0;
dev->muted = mute;
pa_log_info("New hardware muted: %d", mute);
if (impl->events && impl->events->mute_changed)
impl->events->mute_changed(impl->user_data, &dev->device);
return 0;
}
static void set_mute(pa_alsa_device *dev, bool mute)
{
dev->muted = mute;
if (!dev->mixer_handle)
return;
pa_alsa_path_set_mute(dev->mixer_path, dev->mixer_handle, mute);
}
static void mixer_volume_init(pa_card *impl, pa_alsa_device *dev)
{
pa_assert(dev);
if (impl->soft_mixer || !dev->mixer_path || !dev->mixer_path->has_volume) {
dev->read_volume = NULL;
dev->set_volume = NULL;
pa_log_info("Driver does not support hardware volume control, "
"falling back to software volume control.");
dev->base_volume = PA_VOLUME_NORM;
dev->n_volume_steps = PA_VOLUME_NORM+1;
dev->device.flags &= ~ACP_DEVICE_HW_VOLUME;
} else {
dev->read_volume = read_volume;
dev->set_volume = set_volume;
dev->device.flags |= ACP_DEVICE_HW_VOLUME;
#if 0
if (u->mixer_path->has_dB && u->deferred_volume) {
pa_sink_set_write_volume_callback(u->sink, sink_write_volume_cb);
pa_log_info("Successfully enabled deferred volume.");
} else
pa_sink_set_write_volume_callback(u->sink, NULL);
#endif
if (dev->mixer_path->has_dB) {
dev->decibel_volume = true;
pa_log_info("Hardware volume ranges from %0.2f dB to %0.2f dB.",
dev->mixer_path->min_dB, dev->mixer_path->max_dB);
dev->base_volume = pa_sw_volume_from_dB(-dev->mixer_path->max_dB);
dev->n_volume_steps = PA_VOLUME_NORM+1;
pa_log_info("Fixing base volume to %0.2f dB", pa_sw_volume_to_dB(dev->base_volume));
} else {
dev->decibel_volume = false;
pa_log_info("Hardware volume ranges from %li to %li.",
dev->mixer_path->min_volume, dev->mixer_path->max_volume);
dev->base_volume = PA_VOLUME_NORM;
dev->n_volume_steps = dev->mixer_path->max_volume - dev->mixer_path->min_volume + 1;
}
pa_log_info("Using hardware volume control. Hardware dB scale %s.",
dev->mixer_path->has_dB ? "supported" : "not supported");
}
dev->device.base_volume = pa_sw_volume_to_linear(dev->base_volume);
dev->device.volume_step = 1.0f / dev->n_volume_steps;
if (impl->soft_mixer || !dev->mixer_path || !dev->mixer_path->has_mute) {
dev->read_mute = NULL;
dev->set_mute = NULL;
pa_log_info("Driver does not support hardware mute control, falling back to software mute control.");
dev->device.flags &= ~ACP_DEVICE_HW_MUTE;
} else {
dev->read_mute = read_mute;
dev->set_mute = set_mute;
pa_log_info("Using hardware mute control.");
dev->device.flags |= ACP_DEVICE_HW_MUTE;
}
}
static int setup_mixer(pa_card *impl, pa_alsa_device *dev, bool ignore_dB)
{
int res;
bool need_mixer_callback = false;
/* This code is before the u->mixer_handle check, because if the UCM
* configuration doesn't specify volume or mute controls, u->mixer_handle
* will be NULL, but the UCM device enable sequence will still need to be
* executed. */
if (dev->active_port && dev->ucm_context) {
if ((res = pa_alsa_ucm_set_port(dev->ucm_context, dev->active_port)) < 0)
return res;
}
if (!dev->mixer_handle)
return 0;
if (dev->active_port) {
if (!impl->use_ucm) {
pa_alsa_port_data *data;
/* We have a list of supported paths, so let's activate the
* one that has been chosen as active */
data = PA_DEVICE_PORT_DATA(dev->active_port);
dev->mixer_path = data->path;
pa_alsa_path_select(data->path, data->setting, dev->mixer_handle, dev->muted);
} else {
pa_alsa_ucm_port_data *data;
data = PA_DEVICE_PORT_DATA(dev->active_port);
/* Now activate volume controls, if any */
if (data->path) {
dev->mixer_path = data->path;
pa_alsa_path_select(dev->mixer_path, NULL, dev->mixer_handle, dev->muted);
}
}
} else {
if (!dev->mixer_path && dev->mixer_path_set)
dev->mixer_path = pa_hashmap_first(dev->mixer_path_set->paths);
if (dev->mixer_path) {
/* Hmm, we have only a single path, then let's activate it */
pa_alsa_path_select(dev->mixer_path, dev->mixer_path->settings,
dev->mixer_handle, dev->muted);
} else
return 0;
}
mixer_volume_init(impl, dev);
/* Will we need to register callbacks? */
if (dev->mixer_path_set && dev->mixer_path_set->paths) {
pa_alsa_path *p;
void *state;
PA_HASHMAP_FOREACH(p, dev->mixer_path_set->paths, state) {
if (p->has_volume || p->has_mute)
need_mixer_callback = true;
}
}
else if (dev->mixer_path)
need_mixer_callback = dev->mixer_path->has_volume || dev->mixer_path->has_mute;
if (!impl->soft_mixer && need_mixer_callback) {
pa_alsa_mixer_use_for_poll(impl->ucm.mixers, dev->mixer_handle);
if (dev->mixer_path_set)
pa_alsa_path_set_set_callback(dev->mixer_path_set, dev->mixer_handle, mixer_callback, dev);
else
pa_alsa_path_set_callback(dev->mixer_path, dev->mixer_handle, mixer_callback, dev);
}
return 0;
}
static int device_disable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device *dev)
{
dev->device.flags &= ~ACP_DEVICE_ACTIVE;
if (dev->active_port) {
dev->active_port->port.flags &= ~ACP_PORT_ACTIVE;
dev->active_port = NULL;
}
return 0;
}
static int device_enable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device *dev)
{
const char *mod_name;
uint32_t i, port_index;
int res;
if (impl->use_ucm &&
(mod_name = pa_proplist_gets(mapping->proplist, PA_ALSA_PROP_UCM_MODIFIER))) {
if (snd_use_case_set(impl->ucm.ucm_mgr, "_enamod", mod_name) < 0)
pa_log("Failed to enable ucm modifier %s", mod_name);
else
pa_log_debug("Enabled ucm modifier %s", mod_name);
}
pa_log_info("Device: %s mapping '%s' (%s).", dev->device.description,
mapping->description, mapping->name);
dev->device.flags |= ACP_DEVICE_ACTIVE;
find_mixer(impl, dev, NULL, impl->ignore_dB);
/* Synchronize priority values, as it may have changed when setting the profile */
for (i = 0; i < impl->card.n_ports; i++) {
pa_device_port *p = (pa_device_port *)impl->card.ports[i];
p->port.priority = p->priority;
}
if (impl->auto_port)
port_index = acp_device_find_best_port_index(&dev->device, NULL);
else
port_index = ACP_INVALID_INDEX;
if (port_index == ACP_INVALID_INDEX)
dev->active_port = NULL;
else
dev->active_port = (pa_device_port*)impl->card.ports[port_index];
if (dev->active_port)
dev->active_port->port.flags |= ACP_PORT_ACTIVE;
if ((res = setup_mixer(impl, dev, impl->ignore_dB)) < 0)
return res;
if (dev->read_volume)
dev->read_volume(dev);
else {
pa_cvolume_reset(&dev->real_volume, dev->device.format.channels);
pa_cvolume_reset(&dev->soft_volume, dev->device.format.channels);
}
if (dev->read_mute)
dev->read_mute(dev);
else
dev->muted = false;
return 0;
}
int acp_card_set_profile(struct acp_card *card, uint32_t new_index, uint32_t flags)
{
pa_card *impl = (pa_card *)card;
pa_alsa_mapping *am;
uint32_t old_index = impl->card.active_profile_index;
struct acp_card_profile **profiles = card->profiles;
pa_alsa_profile *op, *np;
uint32_t idx;
int res;
if (new_index >= card->n_profiles)
return -EINVAL;
np = (pa_alsa_profile*)profiles[new_index];
if (SPA_FLAG_IS_SET(np->profile.flags, ACP_PROFILE_HIDDEN))
return -EINVAL;
op = old_index != ACP_INVALID_INDEX ? (pa_alsa_profile*)profiles[old_index] : NULL;
if (op == np)
return 0;
pa_log_info("activate profile: %s (%d)", np->profile.name, new_index);
if (op && op->output_mappings) {
PA_IDXSET_FOREACH(am, op->output_mappings, idx) {
if (np->output_mappings &&
pa_idxset_get_by_data(np->output_mappings, am, NULL))
continue;
device_disable(impl, am, &am->output);
}
}
if (op && op->input_mappings) {
PA_IDXSET_FOREACH(am, op->input_mappings, idx) {
if (np->input_mappings &&
pa_idxset_get_by_data(np->input_mappings, am, NULL))
continue;
device_disable(impl, am, &am->input);
}
}
/* if UCM is available for this card then update the verb */
if (impl->use_ucm && !(np->profile.flags & ACP_PROFILE_PRO)) {
if ((res = pa_alsa_ucm_set_profile(&impl->ucm, impl,
np->profile.flags & ACP_PROFILE_OFF ? NULL : np, op)) < 0) {
return res;
}
}
if (np->output_mappings) {
PA_IDXSET_FOREACH(am, np->output_mappings, idx) {
if (impl->use_ucm) {
/* Update ports priorities */
if (am->ucm_context.ucm_devices) {
pa_alsa_ucm_add_port(am->output.ports, &am->ucm_context,
true, impl->ports, np, NULL);
}
}
device_enable(impl, am, &am->output);
}
}
if (np->input_mappings) {
PA_IDXSET_FOREACH(am, np->input_mappings, idx) {
if (impl->use_ucm) {
/* Update ports priorities */
if (am->ucm_context.ucm_devices) {
pa_alsa_ucm_add_port(am->input.ports, &am->ucm_context,
false, impl->ports, np, NULL);
}
}
device_enable(impl, am, &am->input);
}
}
if (op)
op->profile.flags &= ~(ACP_PROFILE_ACTIVE | ACP_PROFILE_SAVE);
np->profile.flags |= ACP_PROFILE_ACTIVE | flags;
impl->card.active_profile_index = new_index;
if (impl->events && impl->events->profile_changed)
impl->events->profile_changed(impl->user_data, old_index,
new_index);
return 0;
}
static void prune_singleton_availability_groups(pa_hashmap *ports) {
pa_device_port *p;
pa_hashmap *group_counts;
void *state, *count;
const char *group;
/* Collect groups and erase those that don't have more than 1 path */
group_counts = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func);
PA_HASHMAP_FOREACH(p, ports, state) {
if (p->availability_group) {
count = pa_hashmap_get(group_counts, p->availability_group);
pa_hashmap_remove(group_counts, p->availability_group);
pa_hashmap_put(group_counts, p->availability_group, PA_UINT_TO_PTR(PA_PTR_TO_UINT(count) + 1));
}
}
/* Now we have an availability_group -> count map, let's drop all groups
* that have only one member */
PA_HASHMAP_FOREACH_KV(group, count, group_counts, state) {
if (count == PA_UINT_TO_PTR(1))
pa_hashmap_remove(group_counts, group);
}
PA_HASHMAP_FOREACH(p, ports, state) {
if (p->availability_group && !pa_hashmap_get(group_counts, p->availability_group)) {
pa_log_debug("Pruned singleton availability group %s from port %s", p->availability_group, p->name);
pa_xfree(p->availability_group);
p->availability_group = NULL;
}
}
pa_hashmap_free(group_counts);
}
static const char *acp_dict_lookup(const struct acp_dict *dict, const char *key)
{
const struct acp_dict_item *it;
acp_dict_for_each(it, dict) {
if (spa_streq(key, it->key))
return it->value;
}
return NULL;
}
struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props)
{
pa_card *impl;
struct acp_card *card;
const char *s, *profile_set = NULL, *profile = NULL;
char device_id[16];
uint32_t profile_index;
int res;
impl = calloc(1, sizeof(*impl));
if (impl == NULL)
return NULL;
pa_alsa_refcnt_inc();
snprintf(device_id, sizeof(device_id), "%d", index);
impl->proplist = pa_proplist_new_dict(props);
card = &impl->card;
card->index = index;
card->active_profile_index = ACP_INVALID_INDEX;
impl->use_ucm = true;
impl->auto_profile = true;
impl->auto_port = true;
impl->ignore_dB = false;
impl->rate = DEFAULT_RATE;
impl->pro_channels = 64;
if (props) {
if ((s = acp_dict_lookup(props, "api.alsa.use-ucm")) != NULL)
impl->use_ucm = spa_atob(s);
if ((s = acp_dict_lookup(props, "api.alsa.soft-mixer")) != NULL)
impl->soft_mixer = spa_atob(s);
if ((s = acp_dict_lookup(props, "api.alsa.ignore-dB")) != NULL)
impl->ignore_dB = spa_atob(s);
if ((s = acp_dict_lookup(props, "device.profile-set")) != NULL)
profile_set = s;
if ((s = acp_dict_lookup(props, "device.profile")) != NULL)
profile = s;
if ((s = acp_dict_lookup(props, "api.acp.auto-profile")) != NULL)
impl->auto_profile = spa_atob(s);
if ((s = acp_dict_lookup(props, "api.acp.auto-port")) != NULL)
impl->auto_port = spa_atob(s);
if ((s = acp_dict_lookup(props, "api.acp.probe-rate")) != NULL)
impl->rate = atoi(s);
if ((s = acp_dict_lookup(props, "api.acp.pro-channels")) != NULL)
impl->pro_channels = atoi(s);
}
impl->ucm.default_sample_spec.format = PA_SAMPLE_S16NE;
impl->ucm.default_sample_spec.rate = impl->rate;
impl->ucm.default_sample_spec.channels = 2;
pa_channel_map_init_extend(&impl->ucm.default_channel_map,
impl->ucm.default_sample_spec.channels, PA_CHANNEL_MAP_ALSA);
impl->ucm.default_n_fragments = 4;
impl->ucm.default_fragment_size_msec = 25;
impl->ucm.mixers = pa_hashmap_new_full(pa_idxset_string_hash_func,
pa_idxset_string_compare_func,
pa_xfree, (pa_free_cb_t) pa_alsa_mixer_free);
impl->profiles = pa_hashmap_new_full(pa_idxset_string_hash_func,
pa_idxset_string_compare_func, NULL,
(pa_free_cb_t) profile_free);
impl->ports = pa_hashmap_new_full(pa_idxset_string_hash_func,
pa_idxset_string_compare_func, NULL,
(pa_free_cb_t) port_free);
snd_config_update_free_global();
res = impl->use_ucm ? pa_alsa_ucm_query_profiles(&impl->ucm, card->index) : -1;
if (res == -PA_ALSA_ERR_UCM_LINKED) {
res = -EEXIST;
goto error;
}
if (res == 0) {
pa_log_info("Found UCM profiles");
impl->profile_set = pa_alsa_ucm_add_profile_set(&impl->ucm, &impl->ucm.default_channel_map);
} else {
impl->use_ucm = false;
impl->profile_set = pa_alsa_profile_set_new(profile_set, &impl->ucm.default_channel_map);
}
if (impl->profile_set == NULL) {
res = -ENOTSUP;
goto error;
}
impl->profile_set->ignore_dB = impl->ignore_dB;
pa_alsa_profile_set_probe(impl->profile_set, impl->ucm.mixers,
device_id,
&impl->ucm.default_sample_spec,
impl->ucm.default_n_fragments,
impl->ucm.default_fragment_size_msec);
pa_alsa_init_proplist_card(NULL, impl->proplist, impl->card.index);
pa_proplist_sets(impl->proplist, PA_PROP_DEVICE_STRING, device_id);
pa_alsa_init_description(impl->proplist, NULL);
add_profiles(impl);
prune_singleton_availability_groups(impl->ports);
card->n_profiles = pa_dynarray_size(&impl->out.profiles);
card->profiles = impl->out.profiles.array.data;
card->n_ports = pa_dynarray_size(&impl->out.ports);
card->ports = impl->out.ports.array.data;
card->n_devices = pa_dynarray_size(&impl->out.devices);
card->devices = impl->out.devices.array.data;
pa_proplist_as_dict(impl->proplist, &card->props);
init_jacks(impl);
if (!impl->auto_profile && profile == NULL)
profile = "off";
profile_index = acp_card_find_best_profile_index(&impl->card, profile);
acp_card_set_profile(&impl->card, profile_index, 0);
init_eld_ctls(impl);
return &impl->card;
error:
pa_alsa_refcnt_dec();
free(impl);
errno = -res;
return NULL;
}
void acp_card_add_listener(struct acp_card *card,
const struct acp_card_events *events, void *user_data)
{
pa_card *impl = (pa_card *)card;
impl->events = events;
impl->user_data = user_data;
}
void acp_card_destroy(struct acp_card *card)
{
pa_card *impl = (pa_card *)card;
if (impl->profiles)
pa_hashmap_free(impl->profiles);
if (impl->ports)
pa_hashmap_free(impl->ports);
pa_dynarray_clear(&impl->out.devices);
pa_dynarray_clear(&impl->out.profiles);
pa_dynarray_clear(&impl->out.ports);
if (impl->ucm.mixers)
pa_hashmap_free(impl->ucm.mixers);
if (impl->jacks)
pa_hashmap_free(impl->jacks);
if (impl->profile_set)
pa_alsa_profile_set_free(impl->profile_set);
pa_alsa_ucm_free(&impl->ucm);
pa_proplist_free(impl->proplist);
pa_alsa_refcnt_dec();
free(impl);
}
int acp_card_poll_descriptors_count(struct acp_card *card)
{
pa_card *impl = (pa_card *)card;
void *state;
pa_alsa_mixer *pm;
int n, count = 0;
PA_HASHMAP_FOREACH(pm, impl->ucm.mixers, state) {
if (!pm->used_for_poll)
continue;
n = snd_mixer_poll_descriptors_count(pm->mixer_handle);
if (n < 0)
return n;
count += n;
}
return count;
}
int acp_card_poll_descriptors(struct acp_card *card, struct pollfd *pfds, unsigned int space)
{
pa_card *impl = (pa_card *)card;
void *state;
pa_alsa_mixer *pm;
int n, count = 0;
PA_HASHMAP_FOREACH(pm, impl->ucm.mixers, state) {
if (!pm->used_for_poll)
continue;
n = snd_mixer_poll_descriptors(pm->mixer_handle, pfds, space);
if (n < 0)
return n;
if (space >= (unsigned int) n) {
count += n;
space -= n;
pfds += n;
} else
space = 0;
}
return count;
}
int acp_card_poll_descriptors_revents(struct acp_card *card, struct pollfd *pfds,
unsigned int nfds, unsigned short *revents)
{
unsigned int idx;
unsigned short res;
if (nfds == 0)
return -EINVAL;
res = 0;
for (idx = 0; idx < nfds; idx++, pfds++)
res |= pfds->revents & (POLLIN|POLLERR|POLLNVAL);
*revents = res;
return 0;
}
int acp_card_handle_events(struct acp_card *card)
{
pa_card *impl = (pa_card *)card;
void *state;
pa_alsa_mixer *pm;
int n, count = 0;
PA_HASHMAP_FOREACH(pm, impl->ucm.mixers, state) {
if (!pm->used_for_poll)
continue;
n = snd_mixer_handle_events(pm->mixer_handle);
if (n < 0)
return n;
count += n;
}
return count;
}
static void sync_mixer(pa_alsa_device *d, pa_device_port *port)
{
pa_alsa_setting *setting = NULL;
if (!d->mixer_path)
return;
/* port may be NULL, because if we use a synthesized mixer path, then the
* sink has no ports. */
if (port && !d->ucm_context) {
pa_alsa_port_data *data;
data = PA_DEVICE_PORT_DATA(port);
setting = data->setting;
}
if (d->mixer_handle)
pa_alsa_path_select(d->mixer_path, setting, d->mixer_handle, d->muted);
if (d->set_mute)
d->set_mute(d, d->muted);
if (d->set_volume)
d->set_volume(d, &d->real_volume);
}
uint32_t acp_device_find_best_port_index(struct acp_device *dev, const char *name)
{
uint32_t i;
uint32_t best, best2, best3;
struct acp_port **ports = dev->ports;
best = best2 = best3 = ACP_INVALID_INDEX;
for (i = 0; i < dev->n_ports; i++) {
struct acp_port *p = ports[i];
if (SPA_FLAG_IS_SET(p->flags, ACP_PORT_HIDDEN))
continue;
if (name) {
if (spa_streq(name, p->name))
best = i;
} else if (p->available == ACP_AVAILABLE_YES) {
if (best == ACP_INVALID_INDEX || p->priority > ports[best]->priority)
best = i;
} else if (p->available != ACP_AVAILABLE_NO) {
if (best2 == ACP_INVALID_INDEX || p->priority > ports[best2]->priority)
best2 = i;
} else {
if (best3 == ACP_INVALID_INDEX || p->priority > ports[best3]->priority)
best3 = i;
}
}
if (best == ACP_INVALID_INDEX)
best = best2;
if (best == ACP_INVALID_INDEX)
best = best3;
if (best == ACP_INVALID_INDEX)
best = 0;
if (best < dev->n_ports)
return ports[best]->index;
else
return ACP_INVALID_INDEX;
}
int acp_device_set_port(struct acp_device *dev, uint32_t port_index, uint32_t flags)
{
pa_alsa_device *d = (pa_alsa_device*)dev;
pa_card *impl = d->card;
pa_device_port *p, *old = d->active_port;
int res;
if (port_index >= impl->card.n_ports)
return -EINVAL;
p = (pa_device_port*)impl->card.ports[port_index];
if (!pa_hashmap_get(d->ports, p->name))
return -EINVAL;
if (SPA_FLAG_IS_SET(p->port.flags, ACP_PORT_HIDDEN))
return -EINVAL;
p->port.flags = ACP_PORT_ACTIVE | flags;
if (p == old)
return 0;
if (old)
old->port.flags &= ~(ACP_PORT_ACTIVE | ACP_PORT_SAVE);
d->active_port = p;
if (impl->use_ucm) {
pa_alsa_ucm_port_data *data;
data = PA_DEVICE_PORT_DATA(p);
d->mixer_path = data->path;
mixer_volume_init(impl, d);
sync_mixer(d, p);
res = pa_alsa_ucm_set_port(d->ucm_context, p);
} else {
pa_alsa_port_data *data;
data = PA_DEVICE_PORT_DATA(p);
d->mixer_path = data->path;
mixer_volume_init(impl, d);
sync_mixer(d, p);
res = 0;
#if 0
if (data->suspend_when_unavailable && p->available == PA_AVAILABLE_NO)
pa_sink_suspend(s, true, PA_SUSPEND_UNAVAILABLE);
else
pa_sink_suspend(s, false, PA_SUSPEND_UNAVAILABLE);
#endif
}
if (impl->events && impl->events->port_changed)
impl->events->port_changed(impl->user_data,
old ? old->port.index : 0, p->port.index);
return res;
}
int acp_device_set_volume(struct acp_device *dev, const float *volume, uint32_t n_volume)
{
pa_alsa_device *d = (pa_alsa_device*)dev;
pa_card *impl = d->card;
uint32_t i;
pa_cvolume v, old_volume;
if (n_volume == 0)
return -EINVAL;
old_volume = d->real_volume;
v.channels = d->mapping->channel_map.channels;
for (i = 0; i < v.channels; i++)
v.values[i] = pa_sw_volume_from_linear(volume[i % n_volume]);
pa_log_info("Set %s volume: min:%d max:%d",
d->set_volume ? "hardware" : "software",
pa_cvolume_min(&v), pa_cvolume_max(&v));
for (i = 0; i < v.channels; i++)
pa_log_debug(" %d: %d", i, v.values[i]);
if (d->set_volume) {
d->set_volume(d, &v);
} else {
d->real_volume = v;
d->soft_volume = v;
}
if (!pa_cvolume_equal(&d->real_volume, &old_volume))
if (impl->events && impl->events->volume_changed)
impl->events->volume_changed(impl->user_data, dev);
return 0;
}
static int get_volume(pa_cvolume *v, float *volume, uint32_t n_volume)
{
uint32_t i;
if (v->channels == 0)
return -EIO;
for (i = 0; i < n_volume; i++)
volume[i] = pa_sw_volume_to_linear(v->values[i % v->channels]);
return 0;
}
int acp_device_get_soft_volume(struct acp_device *dev, float *volume, uint32_t n_volume)
{
pa_alsa_device *d = (pa_alsa_device*)dev;
return get_volume(&d->soft_volume, volume, n_volume);
}
int acp_device_get_volume(struct acp_device *dev, float *volume, uint32_t n_volume)
{
pa_alsa_device *d = (pa_alsa_device*)dev;
return get_volume(&d->real_volume, volume, n_volume);
}
int acp_device_set_mute(struct acp_device *dev, bool mute)
{
pa_alsa_device *d = (pa_alsa_device*)dev;
pa_card *impl = d->card;
bool old_muted = d->muted;
if (old_muted == mute)
return 0;
pa_log_info("Set %s mute: %d", d->set_mute ? "hardware" : "software", mute);
if (d->set_mute) {
d->set_mute(d, mute);
} else {
d->muted = mute;
}
if (old_muted != mute)
if (impl->events && impl->events->mute_changed)
impl->events->mute_changed(impl->user_data, dev);
return 0;
}
int acp_device_get_mute(struct acp_device *dev, bool *mute)
{
pa_alsa_device *d = (pa_alsa_device*)dev;
*mute = d->muted;
return 0;
}
void acp_set_log_func(acp_log_func func, void *data)
{
_acp_log_func = func;
_acp_log_data = data;
}
void acp_set_log_level(int level)
{
_acp_log_level = level;
}