/* 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 #include #include 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); } if (m->split) { char pos[2048]; struct spa_strbuf b; int i; spa_strbuf_init(&b, pos, sizeof(pos)); spa_strbuf_append(&b, "["); for (i = 0; i < m->split->channels; ++i) spa_strbuf_append(&b, "%sAUX%d", ((i == 0) ? "" : ","), m->split->idx[i]); spa_strbuf_append(&b, "]"); pa_proplist_sets(dev->proplist, "api.alsa.split.position", pos); spa_strbuf_init(&b, pos, sizeof(pos)); spa_strbuf_append(&b, "["); for (i = 0; i < m->split->hw_channels; ++i) spa_strbuf_append(&b, "%sAUX%d", ((i == 0) ? "" : ","), i); spa_strbuf_append(&b, "]"); pa_proplist_sets(dev->proplist, "api.alsa.split.hw-position", pos); } 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[1]; char v[256]; if (arr == NULL || str == NULL) return false; if (spa_json_begin_array_relax(&it[0], arr, strlen(arr)) <= 0) return false; while (spa_json_get_string(&it[0], 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; bool broken_ucm = false; 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_device) { 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 (m->split && m->split->broken) broken_ucm = true; } } 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_device) { 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); if (m->split && m->split->broken) broken_ucm = true; } } cp->n_devices = pa_dynarray_size(&ap->out.devices); cp->devices = ap->out.devices.array.data; pa_hashmap_put(impl->profiles, ap->name, cp); } /* Add a conspicuous notice if there are errors in the UCM profile */ if (broken_ucm) { const char *desc; char *new_desc = NULL; desc = pa_proplist_gets(impl->proplist, PA_PROP_DEVICE_DESCRIPTION); if (!desc) desc = ""; new_desc = spa_aprintf(_("%s [ALSA UCM error]"), desc); pa_log_notice("Errors in ALSA UCM profile for card %s", desc); if (new_desc) pa_proplist_sets(impl->proplist, PA_PROP_DEVICE_DESCRIPTION, new_desc); free(new_desc); } 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 void acp_iec958_codec_mask_to_json(uint64_t codecs, char *buf, size_t maxsize) { struct spa_strbuf b; const struct spa_type_info *info; spa_strbuf_init(&b, buf, maxsize); for (info = spa_type_audio_iec958_codec; info->name; ++info) if ((codecs & (1ULL << info->type)) && info->type != SPA_AUDIO_IEC958_CODEC_UNKNOWN) spa_strbuf_append(&b, "%s\"%s\"", (b.pos ? "," : "["), spa_type_audio_iec958_codec_to_short_name(info->type)); if (b.pos) spa_strbuf_append(&b, "]"); } void acp_iec958_codecs_to_json(const uint32_t *codecs, size_t n_codecs, char *buf, size_t maxsize) { struct spa_strbuf b; spa_strbuf_init(&b, buf, maxsize); spa_strbuf_append(&b, "["); for (size_t i = 0; i < n_codecs; ++i) spa_strbuf_append(&b, "%s\"%s\"", (i ? "," : ""), spa_type_audio_iec958_codec_to_short_name(codecs[i])); spa_strbuf_append(&b, "]"); } size_t acp_iec958_codecs_from_json(const char *str, uint32_t *codecs, size_t max_codecs) { struct spa_json it; char v[256]; size_t n_codecs = 0; if (spa_json_begin_array_relax(&it, str, strlen(str)) <= 0) return 0; while (spa_json_get_string(&it, v, sizeof(v)) > 0) { uint32_t type = spa_type_audio_iec958_codec_from_short_name(v); if (type != SPA_AUDIO_IEC958_CODEC_UNKNOWN) codecs[n_codecs++] = type; if (n_codecs >= max_codecs) break; } return n_codecs; } 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, i; const char *old_monitor_name, *old_iec958_codec_list; 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)); // Strip trailing whitespace from monitor_name (primarily an NVidia driver bug for now) for (i = strlen(eld.monitor_name) - 1; i >= 0; i--) { if (eld.monitor_name[i] == '\n' || eld.monitor_name[i] == '\r' || eld.monitor_name[i] == '\t' || eld.monitor_name[i] == ' ') eld.monitor_name[i] = 0; else break; } 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); } old_iec958_codec_list = pa_proplist_gets(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED); if (eld.iec958_codecs == 0) { changed |= old_iec958_codec_list != NULL; pa_proplist_unset(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED); } else { char codecs[512]; acp_iec958_codec_mask_to_json(eld.iec958_codecs, codecs, sizeof(codecs)); changed |= (old_iec958_codec_list == NULL) || (!spa_streq(old_iec958_codec_list, codecs)); pa_proplist_sets(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED, codecs); } 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->ucm_context) { if (!dev->active_port) return 0; pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(dev->active_port); if (pa_alsa_ucm_port_device_status(data) <= 0) return 0; } 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->ucm_context) { if (!dev->active_port) return; pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(dev->active_port); if (pa_alsa_ucm_port_device_status(data) <= 0) return; } 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->ucm_context) { if (!dev->active_port) return 0; pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(dev->active_port); if (pa_alsa_ucm_port_device_status(data) <= 0) return 0; } 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->ucm_context) { if (!dev->active_port) return; pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(dev->active_port); if (pa_alsa_ucm_port_device_status(data) <= 0) return; } 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 = (float)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; if (!impl->disable_mixer_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; if (!impl->disable_mixer_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 */ if (!impl->disable_mixer_path) 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; const char *codecs; pa_device_port *p; void *state = NULL; 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++) { 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; while ((p = pa_hashmap_iterate(dev->ports, &state, NULL))) { codecs = pa_proplist_gets(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED); if (codecs) { dev->device.n_codecs = acp_iec958_codecs_from_json(codecs, dev->device.codecs, ACP_N_ELEMENTS(dev->device.codecs)); break; } } 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) { if (np->profile.flags & ACP_PROFILE_OFF) { if ((res = pa_alsa_ucm_set_profile(&impl->ucm, impl, NULL, op)) < 0) return res; } else if (np->profile.flags & ACP_PROFILE_PRO) { const char *verb = find_best_verb(impl); if ((res = pa_alsa_ucm_set_profile(&impl->ucm, impl, NULL, op)) < 0) return res; if ((res = snd_use_case_set(impl->ucm.ucm_mgr, "_verb", verb)) < 0) { pa_log_error("error setting verb: %s", snd_strerror(res)); return res; } } else if ((res = pa_alsa_ucm_set_profile(&impl->ucm, impl, 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_device) { 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_device) { 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); } 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.disable-mixer-path")) != NULL) impl->disable_mixer_path = 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); if ((s = acp_dict_lookup(props, "api.alsa.split-enable")) != NULL) impl->ucm.split_enable = spa_atob(s); } #if SND_LIB_VERSION < 0x10207 if (impl->ucm.split_enable) pa_log_info("alsa-lib too old for PipeWire-side UCM SplitPCM"); impl->ucm.split_enable = false; /* API addition in 1.2.7 */ #endif 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"; init_eld_ctls(impl); profile_index = acp_card_find_best_profile_index(&impl->card, profile); acp_card_set_profile(&impl->card, profile_index, 0); 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; pa_card *impl = d->card; 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 && !impl->disable_mixer_path) 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); res = pa_alsa_ucm_set_port(d->ucm_context, p); sync_mixer(d, 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] = (float)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; }