[PATCH 3/3] alsa-ucm: Support Playback/CaptureVolume

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



This allows us to support the PlaybackVolume and CaptureVolume commands
in UCM, specifying a mixer control to use for hardware volume control.

This only works with ports corresponding to single devices at the
moment, and doesn't support stacking controls for combination ports.

On the UCM side, this also requires that when disabling the device for
the port, the volume should be reset to some default.

When enabling/disabling combination devices, things are a bit iffy since
we have no way to reset the volume before switching to a combination
device. It would be nice to have a combination-transition-sequence
command in UCM to handle this and other similar cases.
---
 src/modules/alsa/alsa-sink.c        |  69 +++++++++++++++-----
 src/modules/alsa/alsa-source.c      |  34 ++++++----
 src/modules/alsa/alsa-ucm.c         | 126 ++++++++++++++++++++++++++++++++++--
 src/modules/alsa/alsa-ucm.h         |  19 +++++-
 src/modules/alsa/module-alsa-card.c |   4 +-
 5 files changed, 215 insertions(+), 37 deletions(-)

diff --git a/src/modules/alsa/alsa-sink.c b/src/modules/alsa/alsa-sink.c
index d3d5b19..3b64448 100644
--- a/src/modules/alsa/alsa-sink.c
+++ b/src/modules/alsa/alsa-sink.c
@@ -1422,7 +1422,7 @@ static void sink_set_mute_cb(pa_sink *s) {
 static void mixer_volume_init(struct userdata *u) {
     pa_assert(u);
 
-    if (!u->mixer_path->has_volume) {
+    if (!u->mixer_path || !u->mixer_path->has_volume) {
         pa_sink_set_write_volume_callback(u->sink, NULL);
         pa_sink_set_get_volume_callback(u->sink, NULL);
         pa_sink_set_set_volume_callback(u->sink, NULL);
@@ -1457,7 +1457,7 @@ static void mixer_volume_init(struct userdata *u) {
         pa_log_info("Using hardware volume control. Hardware dB scale %s.", u->mixer_path->has_dB ? "supported" : "not supported");
     }
 
-    if (!u->mixer_path->has_mute) {
+    if (!u->mixer_path || !u->mixer_path->has_mute) {
         pa_sink_set_get_mute_callback(u->sink, NULL);
         pa_sink_set_set_mute_callback(u->sink, NULL);
         pa_log_info("Driver does not support hardware mute control, falling back to software mute control.");
@@ -1470,10 +1470,31 @@ static void mixer_volume_init(struct userdata *u) {
 
 static int sink_set_port_ucm_cb(pa_sink *s, pa_device_port *p) {
     struct userdata *u = s->userdata;
+    pa_alsa_ucm_port_data *data;
+
+    data = PA_DEVICE_PORT_DATA(p);
 
     pa_assert(u);
     pa_assert(p);
     pa_assert(u->ucm_context);
+    pa_assert(u->mixer_handle);
+
+    u->mixer_path = data->path;
+    mixer_volume_init(u);
+
+    if (data->path) {
+        pa_alsa_path_select(u->mixer_path, NULL, u->mixer_handle, s->muted);
+
+        if (s->set_mute)
+            s->set_mute(s);
+        if (s->flags & PA_SINK_DEFERRED_VOLUME) {
+            if (s->write_volume)
+                s->write_volume(s);
+        } else {
+            if (s->set_volume)
+                s->set_volume(s);
+        }
+    }
 
     return pa_alsa_ucm_set_port(u->ucm_context, p, true);
 }
@@ -1896,7 +1917,7 @@ static void set_sink_name(pa_sink_new_data *data, pa_modargs *ma, const char *de
 static void find_mixer(struct userdata *u, pa_alsa_mapping *mapping, const char *element, bool ignore_dB) {
     snd_hctl_t *hctl;
 
-    if (!mapping && !element)
+    if (!u->ucm_context && (!mapping && !element))
         return;
 
     if (!(u->mixer_handle = pa_alsa_open_mixer_for_pcm(u->pcm_handle, &u->control_device, &hctl))) {
@@ -1904,6 +1925,11 @@ static void find_mixer(struct userdata *u, pa_alsa_mapping *mapping, const char
         return;
     }
 
+    if (u->ucm_context) {
+        /* We just want to open the device */
+        return;
+    }
+
     if (element) {
 
         if (!(u->mixer_path = pa_alsa_path_synthesize(element, PA_ALSA_DIRECTION_OUTPUT)))
@@ -1941,16 +1967,31 @@ static int setup_mixer(struct userdata *u, bool ignore_dB) {
         return 0;
 
     if (u->sink->active_port) {
-        pa_alsa_port_data *data;
+        if (!u->ucm_context) {
+            pa_alsa_port_data *data;
 
-        /* We have a list of supported paths, so let's activate the
-         * one that has been chosen as active */
+            /* We have a list of supported paths, so let's activate the
+             * one that has been chosen as active */
 
-        data = PA_DEVICE_PORT_DATA(u->sink->active_port);
-        u->mixer_path = data->path;
+            data = PA_DEVICE_PORT_DATA(u->sink->active_port);
+            u->mixer_path = data->path;
 
-        pa_alsa_path_select(data->path, data->setting, u->mixer_handle, u->sink->muted);
+            pa_alsa_path_select(data->path, data->setting, u->mixer_handle, u->sink->muted);
+        } else {
+            pa_alsa_ucm_port_data *data;
+
+            /* First activate the port on the UCM side */
+            if (pa_alsa_ucm_set_port(u->ucm_context, u->sink->active_port, true) < 0)
+                return -1;
 
+            data = PA_DEVICE_PORT_DATA(u->sink->active_port);
+
+            /* Now activate volume controls, if any */
+            if (data->path) {
+                u->mixer_path = data->path;
+                pa_alsa_path_select(data->path, NULL, u->mixer_handle, u->sink->muted);
+            }
+        }
     } else {
 
         if (!u->mixer_path && u->mixer_path_set)
@@ -2244,8 +2285,7 @@ pa_sink *pa_alsa_sink_new(pa_module *m, pa_modargs *ma, const char*driver, pa_ca
     /* ALSA might tweak the sample spec, so recalculate the frame size */
     frame_size = pa_frame_size(&ss);
 
-    if (!u->ucm_context)
-        find_mixer(u, mapping, pa_modargs_get_value(ma, "control", NULL), ignore_dB);
+    find_mixer(u, mapping, pa_modargs_get_value(ma, "control", NULL), ignore_dB);
 
     pa_sink_new_data_init(&data);
     data.driver = driver;
@@ -2295,7 +2335,7 @@ pa_sink *pa_alsa_sink_new(pa_module *m, pa_modargs *ma, const char*driver, pa_ca
     }
 
     if (u->ucm_context)
-        pa_alsa_ucm_add_ports(&data.ports, data.proplist, u->ucm_context, true, card);
+        pa_alsa_ucm_add_ports(&data.ports, data.proplist, u->ucm_context, true, card, u->pcm_handle, ignore_dB);
     else if (u->mixer_path_set)
         pa_alsa_add_ports(&data, u->mixer_path_set, card);
 
@@ -2366,10 +2406,7 @@ pa_sink *pa_alsa_sink_new(pa_module *m, pa_modargs *ma, const char*driver, pa_ca
     if (update_sw_params(u) < 0)
         goto fail;
 
-    if (u->ucm_context) {
-        if (u->sink->active_port && pa_alsa_ucm_set_port(u->ucm_context, u->sink->active_port, true) < 0)
-            goto fail;
-    } else if (setup_mixer(u, ignore_dB) < 0)
+    if (setup_mixer(u, ignore_dB) < 0)
         goto fail;
 
     pa_alsa_dump(PA_LOG_DEBUG, u->pcm_handle);
diff --git a/src/modules/alsa/alsa-source.c b/src/modules/alsa/alsa-source.c
index 8416ba8..9b80b9e 100644
--- a/src/modules/alsa/alsa-source.c
+++ b/src/modules/alsa/alsa-source.c
@@ -1652,16 +1652,31 @@ static int setup_mixer(struct userdata *u, bool ignore_dB) {
         return 0;
 
     if (u->source->active_port) {
-        pa_alsa_port_data *data;
+        if (!u->ucm_context) {
+            pa_alsa_port_data *data;
 
-        /* We have a list of supported paths, so let's activate the
-         * one that has been chosen as active */
+            /* We have a list of supported paths, so let's activate the
+             * one that has been chosen as active */
 
-        data = PA_DEVICE_PORT_DATA(u->source->active_port);
-        u->mixer_path = data->path;
+            data = PA_DEVICE_PORT_DATA(u->source->active_port);
+            u->mixer_path = data->path;
 
-        pa_alsa_path_select(data->path, data->setting, u->mixer_handle, u->source->muted);
+            pa_alsa_path_select(data->path, data->setting, u->mixer_handle, u->source->muted);
+        } else {
+            pa_alsa_ucm_port_data *data;
+
+            /* First activate the port on the UCM side */
+            if (pa_alsa_ucm_set_port(u->ucm_context, u->source->active_port, true) < 0)
+                return -1;
+
+            data = PA_DEVICE_PORT_DATA(u->source->active_port);
 
+            /* Now activate volume controls, if any */
+            if (data->path) {
+                u->mixer_path = data->path;
+                pa_alsa_path_select(data->path, NULL, u->mixer_handle, u->source->muted);
+            }
+        }
     } else {
 
         if (!u->mixer_path && u->mixer_path_set)
@@ -1994,7 +2009,7 @@ pa_source *pa_alsa_source_new(pa_module *m, pa_modargs *ma, const char*driver, p
     }
 
     if (u->ucm_context)
-        pa_alsa_ucm_add_ports(&data.ports, data.proplist, u->ucm_context, false, card);
+        pa_alsa_ucm_add_ports(&data.ports, data.proplist, u->ucm_context, false, card, u->pcm_handle, ignore_dB);
     else if (u->mixer_path_set)
         pa_alsa_add_ports(&data, u->mixer_path_set, card);
 
@@ -2057,10 +2072,7 @@ pa_source *pa_alsa_source_new(pa_module *m, pa_modargs *ma, const char*driver, p
     if (update_sw_params(u) < 0)
         goto fail;
 
-    if (u->ucm_context) {
-        if (u->source->active_port && pa_alsa_ucm_set_port(u->ucm_context, u->source->active_port, false) < 0)
-            goto fail;
-    } else if (setup_mixer(u, ignore_dB) < 0)
+    if (setup_mixer(u, ignore_dB) < 0)
         goto fail;
 
     pa_alsa_dump(PA_LOG_DEBUG, u->pcm_handle);
diff --git a/src/modules/alsa/alsa-ucm.c b/src/modules/alsa/alsa-ucm.c
index 47ff926..822f760 100644
--- a/src/modules/alsa/alsa-ucm.c
+++ b/src/modules/alsa/alsa-ucm.c
@@ -278,6 +278,17 @@ static int ucm_get_device_property(
             else
                 pa_log_debug("UCM playback priority %s for device %s error", value, device_name);
         }
+
+        /* Volume control element */
+        if (!device->playback_volume)
+            device->playback_volume = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree,
+                                                          pa_xfree);
+
+        value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_PLAYBACK_VOLUME);
+        if (value) {
+            pa_hashmap_put(device->playback_volume, pa_xstrdup(pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME)),
+                           pa_xstrdup(value));
+        }
     }
 
     if (device->capture_channels) { /* source device */
@@ -299,6 +310,17 @@ static int ucm_get_device_property(
             else
                 pa_log_debug("UCM capture priority %s for device %s error", value, device_name);
         }
+
+        /* Volume control element */
+        if (!device->capture_volume)
+            device->capture_volume = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree,
+                                                         pa_xfree);
+
+        value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_CAPTURE_VOLUME);
+        if (value) {
+            pa_hashmap_put(device->capture_volume, pa_xstrdup(pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME)),
+                           pa_xstrdup(value));
+        }
     }
 
     if (PA_UCM_PLAYBACK_PRIORITY_UNSET(device) || PA_UCM_CAPTURE_PRIORITY_UNSET(device)) {
@@ -671,6 +693,57 @@ static int pa_alsa_ucm_device_cmp(const void *a, const void *b) {
     return strcmp(pa_proplist_gets(d1->proplist, PA_ALSA_PROP_UCM_NAME), pa_proplist_gets(d2->proplist, PA_ALSA_PROP_UCM_NAME));
 }
 
+static void ucm_port_data_free(pa_device_port *port) {
+    pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(port);
+
+    if (data->paths) {
+        pa_hashmap_free(data->paths);
+    }
+
+    data->path = NULL;
+}
+
+static void probe_volumes(pa_hashmap *hash, snd_pcm_t *pcm_handle, bool ignore_dB) {
+    pa_device_port *port;
+    pa_alsa_path *path;
+    pa_alsa_ucm_port_data *data;
+    snd_mixer_t *mixer_handle;
+    snd_hctl_t *hctl;
+    const char *profile;
+    void *state, *state2;
+
+    if (!(mixer_handle = pa_alsa_open_mixer_for_pcm(pcm_handle, NULL, &hctl))) {
+        pa_log_error("Failed to find a working mixer device.");
+        goto fail;
+    }
+
+    PA_HASHMAP_FOREACH(port, hash, state) {
+        data = PA_DEVICE_PORT_DATA(port);
+
+        PA_HASHMAP_FOREACH_KV(profile, path, data->paths, state2) {
+            if (pa_alsa_path_probe(path, mixer_handle, hctl, ignore_dB) < 0) {
+                pa_log_warn("Could not probe path: %s, using s/w volume", data->path->name);
+                pa_hashmap_remove(data->paths, profile);
+                pa_alsa_path_free(path);
+            } else
+                pa_log_debug("Set up h/w volume using '%s' for %s:%s", path->name, profile, port->name);
+        }
+    }
+
+    return;
+
+fail:
+    /* We could not probe the paths we created. Free them and rever to software volumes. */
+    PA_HASHMAP_FOREACH(port, hash, state) {
+        data = PA_DEVICE_PORT_DATA(port);
+
+        if (data->path) {
+            pa_alsa_path_free(data->path);
+            data->path = NULL;
+        }
+    }
+}
+
 static void ucm_add_port_combination(
         pa_hashmap *hash,
         pa_alsa_ucm_mapping_context *context,
@@ -688,7 +761,10 @@ static void ucm_add_port_combination(
     char *name, *desc;
     const char *dev_name;
     const char *direction;
+    const char *profile, *volume;
     pa_alsa_ucm_device *sorted[num], *dev;
+    pa_alsa_ucm_port_data *data;
+    void *state;
 
     for (i = 0; i < num; i++)
         sorted[i] = pdevices[i];
@@ -743,15 +819,35 @@ static void ucm_add_port_combination(
         pa_device_port_new_data_set_description(&port_data, desc);
         pa_device_port_new_data_set_direction(&port_data, is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT);
 
-        port = pa_device_port_new(core, &port_data, 0);
+        port = pa_device_port_new(core, &port_data, sizeof(pa_alsa_ucm_port_data));
         pa_device_port_new_data_done(&port_data);
-        pa_assert(port);
+
+        data = PA_DEVICE_PORT_DATA(port);
+        data->paths = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree,
+                                          (pa_free_cb_t) pa_alsa_path_free);
+        port->impl_free = ucm_port_data_free;
 
         pa_hashmap_put(ports, port->name, port);
         pa_log_debug("Add port %s: %s", port->name, port->description);
         port->profiles = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func);
     }
 
+    if (num == 1) {
+        /* To keep things simple and not worry about stacking controls, we only support hardware volumes on non-combination
+         * ports. */
+        data = PA_DEVICE_PORT_DATA(port);
+
+        PA_HASHMAP_FOREACH_KV(profile, volume, is_sink ? dev->playback_volume : dev->capture_volume, state) {
+            pa_alsa_path *path = pa_alsa_path_synthesize(volume,
+                    is_sink ? PA_ALSA_DIRECTION_OUTPUT : PA_ALSA_DIRECTION_INPUT);
+
+            if (!path)
+                pa_log_warn("Failed to set up volume control: %s", volume);
+            else
+                pa_hashmap_put(data->paths, pa_xstrdup(profile), path);
+        }
+    }
+
     port->priority = priority;
 
     pa_xfree(name);
@@ -931,7 +1027,9 @@ void pa_alsa_ucm_add_ports(
         pa_proplist *proplist,
         pa_alsa_ucm_mapping_context *context,
         bool is_sink,
-        pa_card *card) {
+        pa_card *card,
+        snd_pcm_t *pcm_handle,
+        bool ignore_dB) {
 
     uint32_t idx;
     char *merged_roles;
@@ -946,6 +1044,9 @@ void pa_alsa_ucm_add_ports(
     /* add ports first */
     pa_alsa_ucm_add_ports_combination(*p, context, is_sink, card->ports, NULL, card->core);
 
+    /* now set up volume paths if any */
+    probe_volumes(*p, pcm_handle, ignore_dB);
+
     /* then set property PA_PROP_DEVICE_INTENDED_ROLES */
     merged_roles = pa_xstrdup(pa_proplist_gets(proplist, PA_PROP_DEVICE_INTENDED_ROLES));
     PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) {
@@ -970,10 +1071,13 @@ void pa_alsa_ucm_add_ports(
 }
 
 /* Change UCM verb and device to match selected card profile */
-int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, const char *new_profile, const char *old_profile) {
+int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, pa_card *card, const char *new_profile, const char *old_profile) {
     int ret = 0;
     const char *profile;
     pa_alsa_ucm_verb *verb;
+    pa_device_port *port;
+    pa_alsa_ucm_port_data *data;
+    void *state;
 
     if (new_profile == old_profile)
         return ret;
@@ -1002,6 +1106,12 @@ int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, const char *new_profile, co
         }
     }
 
+    /* select volume controls on ports */
+    PA_HASHMAP_FOREACH(port, card->ports, state) {
+        data = PA_DEVICE_PORT_DATA(port);
+        data->path = pa_hashmap_get(data->paths, new_profile);
+    }
+
     return ret;
 }
 
@@ -1551,6 +1661,8 @@ static void free_verb(pa_alsa_ucm_verb *verb) {
 
     PA_LLIST_FOREACH_SAFE(di, dn, verb->devices) {
         PA_LLIST_REMOVE(pa_alsa_ucm_device, verb->devices, di);
+        pa_hashmap_free(di->playback_volume);
+        pa_hashmap_free(di->capture_volume);
         pa_proplist_free(di->proplist);
         if (di->conflicting_devices)
             pa_idxset_free(di->conflicting_devices, NULL);
@@ -1684,7 +1796,7 @@ pa_alsa_profile_set* pa_alsa_ucm_add_profile_set(pa_alsa_ucm_config *ucm, pa_cha
     return NULL;
 }
 
-int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, const char *new_profile, const char *old_profile) {
+int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, pa_card *card, const char *new_profile, const char *old_profile) {
     return -1;
 }
 
@@ -1697,7 +1809,9 @@ void pa_alsa_ucm_add_ports(
         pa_proplist *proplist,
         pa_alsa_ucm_mapping_context *context,
         bool is_sink,
-        pa_card *card) {
+        pa_card *card,
+        snd_pcm_t *pcm_handle,
+        bool ignore_dB) {
 }
 
 void pa_alsa_ucm_add_ports_combination(
diff --git a/src/modules/alsa/alsa-ucm.h b/src/modules/alsa/alsa-ucm.h
index 2fae6c4..36a491f 100644
--- a/src/modules/alsa/alsa-ucm.h
+++ b/src/modules/alsa/alsa-ucm.h
@@ -91,10 +91,11 @@ typedef struct pa_alsa_ucm_modifier pa_alsa_ucm_modifier;
 typedef struct pa_alsa_ucm_device pa_alsa_ucm_device;
 typedef struct pa_alsa_ucm_config pa_alsa_ucm_config;
 typedef struct pa_alsa_ucm_mapping_context pa_alsa_ucm_mapping_context;
+typedef struct pa_alsa_ucm_port_data pa_alsa_ucm_port_data;
 
 int pa_alsa_ucm_query_profiles(pa_alsa_ucm_config *ucm, int card_index);
 pa_alsa_profile_set* pa_alsa_ucm_add_profile_set(pa_alsa_ucm_config *ucm, pa_channel_map *default_channel_map);
-int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, const char *new_profile, const char *old_profile);
+int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, pa_card *card, const char *new_profile, const char *old_profile);
 
 int pa_alsa_ucm_get_verb(snd_use_case_mgr_t *uc_mgr, const char *verb_name, const char *verb_desc, pa_alsa_ucm_verb **p_verb);
 
@@ -103,7 +104,9 @@ void pa_alsa_ucm_add_ports(
         pa_proplist *proplist,
         pa_alsa_ucm_mapping_context *context,
         bool is_sink,
-        pa_card *card);
+        pa_card *card,
+        snd_pcm_t *pcm_handle,
+        bool ignore_dB);
 void pa_alsa_ucm_add_ports_combination(
         pa_hashmap *hash,
         pa_alsa_ucm_mapping_context *context,
@@ -135,6 +138,11 @@ struct pa_alsa_ucm_device {
     unsigned playback_channels;
     unsigned capture_channels;
 
+    /* These may be different per verb, so we store this as a hashmap of verb -> volume_control. We might eventually want to
+     * make this a hashmap of verb -> per-verb-device-properties-struct. */
+    pa_hashmap *playback_volume;
+    pa_hashmap *capture_volume;
+
     pa_alsa_mapping *playback_mapping;
     pa_alsa_mapping *capture_mapping;
 
@@ -194,4 +202,11 @@ struct pa_alsa_ucm_mapping_context {
     pa_idxset *ucm_modifiers;
 };
 
+struct pa_alsa_ucm_port_data {
+    /* profile -> pa_alsa_path for volume control */
+    pa_hashmap *paths;
+    /* Current path, set when activating profile */
+    pa_alsa_path *path;
+};
+
 #endif
diff --git a/src/modules/alsa/module-alsa-card.c b/src/modules/alsa/module-alsa-card.c
index 96e88e5..970344a 100644
--- a/src/modules/alsa/module-alsa-card.c
+++ b/src/modules/alsa/module-alsa-card.c
@@ -244,7 +244,7 @@ static int card_set_profile(pa_card *c, pa_card_profile *new_profile) {
 
     /* if UCM is available for this card then update the verb */
     if (u->use_ucm) {
-        if (pa_alsa_ucm_set_profile(&u->ucm, nd->profile ? nd->profile->name : NULL,
+        if (pa_alsa_ucm_set_profile(&u->ucm, c, nd->profile ? nd->profile->name : NULL,
                     od->profile ? od->profile->name : NULL) < 0)
             return -1;
     }
@@ -294,7 +294,7 @@ static void init_profile(struct userdata *u) {
 
     if (d->profile && u->use_ucm) {
         /* Set initial verb */
-        if (pa_alsa_ucm_set_profile(ucm, d->profile->name, NULL) < 0) {
+        if (pa_alsa_ucm_set_profile(ucm, u->card, d->profile->name, NULL) < 0) {
             pa_log("Failed to set ucm profile %s", d->profile->name);
             return;
         }
-- 
1.8.3.1



[Index of Archives]     [Linux Audio Users]     [AMD Graphics]     [Linux USB Devel]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]

  Powered by Linux