[PATCH 3/3] switch-on-port-available: prefer ports that have been selected by the user

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

 



This fixes the problem that when switch-on-port-available changes
profile due to all ports becoming unavailable in the old profile, it
doesn't switch back to the old profile when a port in the old profile
becomes available, not even if the user previously manually selected
the old profile.

A particularly annoying scenario is when the user chooses a HDMI
profile, and then the display goes to sleep. At least on some hardware
the sleep mode also makes the HDMI port unavailable in PulseAudio,
which triggers a profile switch. When the display then wakes up again,
PulseAudio won't switch to the HDMI profile.

This patch modifies module-switch-on-port-available so that the module
keeps track of which input and output port on each card is preferred
by the user, based on the user's manual profile and port switches.
When a user-preferred port becomes available, the module switches to
that port automatically.

This new functionality could be further improved by saving the
preferred ports on disk. Now we forget the preferred ports, except in
cases where module-device-restore has restored the port for a sink or
source.

BugLink: https://bugs.freedesktop.org/show_bug.cgi?id=93946
---
 src/modules/module-switch-on-port-available.c | 311 ++++++++++++++++++++++++--
 1 file changed, 297 insertions(+), 14 deletions(-)

diff --git a/src/modules/module-switch-on-port-available.c b/src/modules/module-switch-on-port-available.c
index bef079b..ce4f4f6 100644
--- a/src/modules/module-switch-on-port-available.c
+++ b/src/modules/module-switch-on-port-available.c
@@ -29,7 +29,110 @@
 
 #include "module-switch-on-port-available-symdef.h"
 
-static bool profile_good_for_output(pa_card_profile *profile) {
+struct card_info {
+    struct userdata *userdata;
+    pa_card *card;
+
+    /* We need to cache the active profile, because we want to compare the old
+     * and new profiles in the PROFILE_CHANGED hook. Without this we'd only
+     * have access to the new profile. */
+    pa_card_profile *active_profile;
+
+    /* When there's clearly just one input or output port that the user prefers
+     * over all other ports, that's saved in these variables. These ports are
+     * automatically activated more aggressively than others. */
+    pa_device_port *preferred_input_port;
+    pa_device_port *preferred_output_port;
+};
+
+struct userdata {
+    pa_hashmap *card_infos; /* pa_card -> struct card_info */
+};
+
+static void card_info_new(struct userdata *u, pa_card *card) {
+    struct card_info *info;
+    const char *input_port_name = "(unset)";
+    const char *output_port_name = "(unset)";
+
+    info = pa_xnew0(struct card_info, 1);
+    info->userdata = u;
+    info->card = card;
+    info->active_profile = card->active_profile;
+
+    /* If there's only one source and its port was selected by the user, we
+     * know that the user wants to use that port for input. */
+    if (pa_idxset_size(card->sources) == 1) {
+        pa_source *source = pa_idxset_first(card->sources, NULL);
+
+        if (source->save_port)
+            info->preferred_input_port = source->active_port;
+    }
+
+    /* If there's only one sink and its port was selected by the user, we know
+     * that the user wants to use that port for output. */
+    if (pa_idxset_size(card->sinks) == 1) {
+        pa_sink *sink = pa_idxset_first(card->sinks, NULL);
+
+        if (sink->save_port)
+            info->preferred_output_port = sink->active_port;
+    }
+
+    pa_hashmap_put(u->card_infos, card, info);
+
+    if (info->preferred_input_port)
+        input_port_name = info->preferred_input_port->name;
+
+    if (info->preferred_output_port)
+        output_port_name = info->preferred_output_port->name;
+
+    pa_log_debug("New card %s:", card->name);
+    pa_log_debug("    preferred_input_port: %s", input_port_name);
+    pa_log_debug("    preferred_output_port: %s", output_port_name);
+}
+
+static void card_info_free(struct card_info *info) {
+    pa_hashmap_remove(info->userdata->card_infos, info->card);
+    pa_xfree(info);
+}
+
+static void card_info_set_preferred_input_port(struct card_info *info, pa_device_port *port) {
+    const char *old_port_name = "(unset)";
+    const char *new_port_name = "(unset)";
+
+    if (port == info->preferred_input_port)
+        return;
+
+    if (info->preferred_input_port)
+        old_port_name = info->preferred_input_port->name;
+
+    if (port)
+        new_port_name = port->name;
+
+    info->preferred_input_port = port;
+
+    pa_log_debug("%s: preferred_input_port: %s -> %s", info->card->name, old_port_name, new_port_name);
+}
+
+static void card_info_set_preferred_output_port(struct card_info *info, pa_device_port *port) {
+    const char *old_port_name = "(unset)";
+    const char *new_port_name = "(unset)";
+
+    if (port == info->preferred_output_port)
+        return;
+
+    if (info->preferred_output_port)
+        old_port_name = info->preferred_output_port->name;
+
+    if (port)
+        new_port_name = port->name;
+
+    info->preferred_output_port = port;
+
+    pa_log_debug("%s: preferred_output_port: %s -> %s", info->card->name, old_port_name, new_port_name);
+}
+
+static bool profile_good_for_output(struct userdata *u, pa_card_profile *profile, pa_device_port *port) {
+    struct card_info *info;
     pa_sink *sink;
     uint32_t idx;
 
@@ -44,7 +147,11 @@ static bool profile_good_for_output(pa_card_profile *profile) {
     if (profile->card->active_profile->max_source_channels != profile->max_source_channels)
         return false;
 
-    /* Try not to switch to HDMI sinks from analog when HDMI is becoming available */
+    info = pa_hashmap_get(u->card_infos, port->card);
+
+    if (port == info->preferred_output_port)
+        return true;
+
     PA_IDXSET_FOREACH(sink, profile->card->sinks, idx) {
         if (!sink->active_port)
             continue;
@@ -56,7 +163,8 @@ static bool profile_good_for_output(pa_card_profile *profile) {
     return true;
 }
 
-static bool profile_good_for_input(pa_card_profile *profile) {
+static bool profile_good_for_input(struct userdata *u, pa_card_profile *profile, pa_device_port *port) {
+    struct card_info *info;
     pa_source *source;
     uint32_t idx;
 
@@ -71,6 +179,11 @@ static bool profile_good_for_input(pa_card_profile *profile) {
     if (profile->card->active_profile->max_sink_channels != profile->max_sink_channels)
         return false;
 
+    info = pa_hashmap_get(u->card_infos, port->card);
+
+    if (port == info->preferred_input_port)
+        return true;
+
     PA_IDXSET_FOREACH(source, profile->card->sources, idx) {
         if (!source->active_port)
             continue;
@@ -82,7 +195,7 @@ static bool profile_good_for_input(pa_card_profile *profile) {
     return true;
 }
 
-static int try_to_switch_profile(pa_device_port *port) {
+static int try_to_switch_profile(struct userdata *u, pa_device_port *port) {
     pa_card_profile *best_profile = NULL, *profile;
     void *state;
     unsigned best_prio = 0;
@@ -99,12 +212,12 @@ static int try_to_switch_profile(pa_device_port *port) {
         switch (port->direction) {
             case PA_DIRECTION_OUTPUT:
                 name = profile->output_name;
-                good = profile_good_for_output(profile);
+                good = profile_good_for_output(u, profile, port);
                 break;
 
             case PA_DIRECTION_INPUT:
                 name = profile->input_name;
-                good = profile_good_for_input(profile);
+                good = profile_good_for_input(u, profile, port);
                 break;
         }
 
@@ -184,7 +297,7 @@ static struct port_pointers find_port_pointers(pa_device_port *port) {
 }
 
 /* Switches to a port, switching profiles if necessary or preferred */
-static bool switch_to_port(pa_device_port *port) {
+static bool switch_to_port(struct userdata *u, pa_device_port *port) {
     struct port_pointers pp = find_port_pointers(port);
 
     if (pp.is_port_active)
@@ -192,7 +305,7 @@ static bool switch_to_port(pa_device_port *port) {
 
     pa_log_debug("Trying to switch to port %s", port->name);
     if (!pp.is_preferred_profile_active) {
-        if (try_to_switch_profile(port) < 0) {
+        if (try_to_switch_profile(u, port) < 0) {
             if (!pp.is_possible_profile_active)
                 return false;
         }
@@ -209,7 +322,7 @@ static bool switch_to_port(pa_device_port *port) {
 }
 
 /* Switches away from a port, switching profiles if necessary or preferred */
-static bool switch_from_port(pa_device_port *port) {
+static bool switch_from_port(struct userdata *u, pa_device_port *port) {
     struct port_pointers pp = find_port_pointers(port);
     pa_device_port *p, *best_port = NULL;
     void *state;
@@ -226,13 +339,13 @@ static bool switch_from_port(pa_device_port *port) {
     pa_log_debug("Trying to switch away from port %s, found %s", port->name, best_port ? best_port->name : "no better option");
 
     if (best_port)
-        return switch_to_port(best_port);
+        return switch_to_port(u, best_port);
 
     return false;
 }
 
 
-static pa_hook_result_t port_available_hook_callback(pa_core *c, pa_device_port *port, void* userdata) {
+static pa_hook_result_t port_available_hook_callback(pa_core *c, pa_device_port *port, struct userdata *u) {
     pa_assert(port);
 
     if (!port->card) {
@@ -247,10 +360,10 @@ static pa_hook_result_t port_available_hook_callback(pa_core *c, pa_device_port
 
     switch (port->available) {
     case PA_AVAILABLE_YES:
-        switch_to_port(port);
+        switch_to_port(u, port);
         break;
     case PA_AVAILABLE_NO:
-        switch_from_port(port);
+        switch_from_port(u, port);
         break;
     default:
         break;
@@ -318,18 +431,188 @@ static pa_hook_result_t source_new_hook_callback(pa_core *c, pa_source_new_data
     return PA_HOOK_OK;
 }
 
+static pa_hook_result_t card_put_hook_callback(pa_core *core, pa_card *card, struct userdata *u) {
+    card_info_new(u, card);
+
+    return PA_HOOK_OK;
+}
+
+static pa_hook_result_t card_unlink_hook_callback(pa_core *core, pa_card *card, struct userdata *u) {
+    card_info_free(pa_hashmap_get(u->card_infos, card));
+
+    return PA_HOOK_OK;
+}
+
+static void update_preferred_input_port(struct card_info *info, pa_card_profile *old_profile, pa_card_profile *new_profile) {
+    pa_source *source;
+
+    /* If the profile change didn't affect input, it doesn't indicate change in
+     * the user's input port preference. */
+    if (pa_safe_streq(old_profile->input_name, new_profile->input_name))
+        return;
+
+    /* If there are more than one source, we don't know which of those the user
+     * prefers. If there are no sources, then the user doesn't seem to care
+     * about input at all. */
+    if (pa_idxset_size(info->card->sources) != 1) {
+        card_info_set_preferred_input_port(info, NULL);
+        return;
+    }
+
+    /* If the profile change modified the set of sinks, then it's unclear
+     * whether the user wanted to activate some specific input port, or was the
+     * input change only a side effect of activating some output. If the new
+     * profile contains no sinks, though, then we know the user only cares
+     * about input. */
+    if (pa_idxset_size(info->card->sinks) > 0 && !pa_safe_streq(old_profile->output_name, new_profile->output_name)) {
+        card_info_set_preferred_input_port(info, NULL);
+        return;
+    }
+
+    source = pa_idxset_first(info->card->sources, NULL);
+
+    /* We know the user wanted to activate this source. The user might not have
+     * wanted to activate the port that was selected by default, but if that's
+     * the case, the user will change the port manually, and we'll update the
+     * port preference at that time. If no port change occurs, we can assume
+     * that the user likes the port that is now active. */
+    card_info_set_preferred_input_port(info, source->active_port);
+}
+
+static void update_preferred_output_port(struct card_info *info, pa_card_profile *old_profile, pa_card_profile *new_profile) {
+    pa_sink *sink;
+
+    /* If the profile change didn't affect output, it doesn't indicate change in
+     * the user's output port preference. */
+    if (pa_safe_streq(old_profile->output_name, new_profile->output_name))
+        return;
+
+    /* If there are more than one sink, we don't know which of those the user
+     * prefers. If there are no sinks, then the user doesn't seem to care about
+     * output at all. */
+    if (pa_idxset_size(info->card->sinks) != 1) {
+        card_info_set_preferred_output_port(info, NULL);
+        return;
+    }
+
+    /* If the profile change modified the set of sources, then it's unclear
+     * whether the user wanted to activate some specific output port, or was
+     * the output change only a side effect of activating some input. If the
+     * new profile contains no sources, though, then we know the user only
+     * cares about output. */
+    if (pa_idxset_size(info->card->sources) > 0 && !pa_safe_streq(old_profile->input_name, new_profile->input_name)) {
+        card_info_set_preferred_output_port(info, NULL);
+        return;
+    }
+
+    sink = pa_idxset_first(info->card->sinks, NULL);
+
+    /* We know the user wanted to activate this sink. The user might not have
+     * wanted to activate the port that was selected by default, but if that's
+     * the case, the user will change the port manually, and we'll update the
+     * port preference at that time. If no port change occurs, we can assume
+     * that the user likes the port that is now active. */
+    card_info_set_preferred_output_port(info, sink->active_port);
+}
+
+static pa_hook_result_t card_profile_changed_callback(pa_core *core, pa_card *card, struct userdata *u) {
+    struct card_info *info;
+    pa_card_profile *old_profile;
+    pa_card_profile *new_profile;
+
+    /* Here we'll just update the preferred input and output ports. No routing
+     * happens in this callback. */
+
+    /* This profile change wasn't initiated by the user, so it doesn't signal
+     * a change in the user's port preferences. */
+    if (!card->save_profile)
+        return PA_HOOK_OK;
+
+    info = pa_hashmap_get(u->card_infos, card);
+    old_profile = info->active_profile;
+    new_profile = card->active_profile;
+
+    update_preferred_input_port(info, old_profile, new_profile);
+    update_preferred_output_port(info, old_profile, new_profile);
+
+    info->active_profile = card->active_profile;
+
+    return PA_HOOK_OK;
+}
+
+static pa_hook_result_t source_port_changed_callback(pa_core *core, pa_source *source, struct userdata *u) {
+    struct card_info *info;
+
+    if (!source->save_port)
+        return PA_HOOK_OK;
+
+    info = pa_hashmap_get(u->card_infos, source->card);
+    card_info_set_preferred_input_port(info, source->active_port);
+
+    return PA_HOOK_OK;
+}
+
+static pa_hook_result_t sink_port_changed_callback(pa_core *core, pa_sink *sink, struct userdata *u) {
+    struct card_info *info;
+
+    if (!sink->save_port)
+        return PA_HOOK_OK;
+
+    info = pa_hashmap_get(u->card_infos, sink->card);
+    card_info_set_preferred_output_port(info, sink->active_port);
+
+    return PA_HOOK_OK;
+}
+
 int pa__init(pa_module*m) {
+    struct userdata *u;
+    pa_card *card;
+    uint32_t idx;
+
     pa_assert(m);
 
+    u = m->userdata = pa_xnew0(struct userdata, 1);
+    u->card_infos = pa_hashmap_new(NULL, NULL);
+
+    PA_IDXSET_FOREACH(card, m->core->cards, idx)
+        card_info_new(u, card);
+
     /* Make sure we are after module-device-restore, so we can overwrite that suggestion if necessary */
     pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SINK_NEW],
                            PA_HOOK_NORMAL, (pa_hook_cb_t) sink_new_hook_callback, NULL);
     pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SOURCE_NEW],
                            PA_HOOK_NORMAL, (pa_hook_cb_t) source_new_hook_callback, NULL);
     pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_PORT_AVAILABLE_CHANGED],
-                           PA_HOOK_LATE, (pa_hook_cb_t) port_available_hook_callback, NULL);
+                           PA_HOOK_LATE, (pa_hook_cb_t) port_available_hook_callback, u);
+    pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_CARD_PUT],
+                           PA_HOOK_NORMAL, (pa_hook_cb_t) card_put_hook_callback, u);
+    pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_CARD_UNLINK],
+                           PA_HOOK_NORMAL, (pa_hook_cb_t) card_unlink_hook_callback, u);
+    pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_CARD_PROFILE_CHANGED],
+                           PA_HOOK_NORMAL, (pa_hook_cb_t) card_profile_changed_callback, u);
+    pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SOURCE_PORT_CHANGED],
+                           PA_HOOK_NORMAL, (pa_hook_cb_t) source_port_changed_callback, u);
+    pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SINK_PORT_CHANGED],
+                           PA_HOOK_NORMAL, (pa_hook_cb_t) sink_port_changed_callback, u);
 
     handle_all_unavailable(m->core);
 
     return 0;
 }
+
+void pa__done(pa_module *module) {
+    struct userdata *u;
+    struct card_info *info;
+
+    pa_assert(module);
+
+    if (!(u = module->userdata))
+        return;
+
+    while ((info = pa_hashmap_last(u->card_infos)))
+        card_info_free(info);
+
+    pa_hashmap_free(u->card_infos);
+
+    pa_xfree(u);
+}
-- 
2.7.0



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

  Powered by Linux