[pulseaudio-discuss] [PATCH v2 6/6] switch-on-port-available: prefer ports that have been selected by the user

Tanu Kaskinen tanuk at iki.fi
Fri Mar 4 10:57:38 UTC 2016


Let's assume that there are two output ports, and they are on
different profiles:

    * Integrated speakers (priority: 10000, available)
    * HDMI                (priority:  5900, not available)

Then the user plugs in an HDMI monitor with speakers. Since the HDMI
priority is lower than the speaker priority, we don't route to HDMI by
default. However, the user manually switches the profile to use the
HDMI output.

Then the user plugs out the monitor, so we switch back to speakers.
When the monitor is plugged back in, the user needs to manually switch
the audio output again. That should be improved: if the user preferred
to the HDMI output over the speakers, we should remember that and
automatically switch to HDMI whenever it becomes available.

The lack of automatic switching is even worse when the monitor goes to
a sleep mode after some period of inactivity. The monitor audio may
become unavailable, and PulseAudio can't distinguish that from the
case where the monitor is physically unplugged. Even worse, the
monitor may become unavailable for a short while when adjusting the
display parameters (for example, media center software may adjust the
display parameters to match the media that is being played back). In
these cases we clearly should switch automatically back to HDMI when
it becomes available again.

This patch fixes the problem by setting pa_card.preferred_input_port
and pa_card.preferred_output_port when the user changes the card
profile or a port, and switching to the preferred port when it becomes
available.

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

diff --git a/src/modules/module-switch-on-port-available.c b/src/modules/module-switch-on-port-available.c
index 2453644..b9a0f3b 100644
--- a/src/modules/module-switch-on-port-available.c
+++ b/src/modules/module-switch-on-port-available.c
@@ -29,7 +29,37 @@
 
 #include "module-switch-on-port-available-symdef.h"
 
-static bool profile_good_for_output(pa_card_profile *profile, unsigned prio) {
+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;
+};
+
+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;
+
+    info = pa_xnew0(struct card_info, 1);
+    info->userdata = u;
+    info->card = card;
+    info->active_profile = card->active_profile;
+
+    pa_hashmap_put(u->card_infos, card, info);
+}
+
+static void card_info_free(struct card_info *info) {
+    pa_hashmap_remove(info->userdata->card_infos, info->card);
+    pa_xfree(info);
+}
+
+static bool profile_good_for_output(pa_card_profile *profile, pa_device_port *port) {
     pa_card *card;
     pa_sink *sink;
     uint32_t idx;
@@ -47,19 +77,21 @@ static bool profile_good_for_output(pa_card_profile *profile, unsigned prio) {
     if (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 */
+    if (port == card->preferred_output_port)
+        return true;
+
     PA_IDXSET_FOREACH(sink, card->sinks, idx) {
         if (!sink->active_port)
             continue;
 
-        if ((sink->active_port->available != PA_AVAILABLE_NO) && (sink->active_port->priority >= prio))
+        if ((sink->active_port->available != PA_AVAILABLE_NO) && (sink->active_port->priority >= port->priority))
             return false;
     }
 
     return true;
 }
 
-static bool profile_good_for_input(pa_card_profile *profile, unsigned prio) {
+static bool profile_good_for_input(pa_card_profile *profile, pa_device_port *port) {
     pa_card *card;
     pa_source *source;
     uint32_t idx;
@@ -77,11 +109,14 @@ static bool profile_good_for_input(pa_card_profile *profile, unsigned prio) {
     if (card->active_profile->max_sink_channels != profile->max_sink_channels)
         return false;
 
+    if (port == card->preferred_input_port)
+        return true;
+
     PA_IDXSET_FOREACH(source, card->sources, idx) {
         if (!source->active_port)
             continue;
 
-        if ((source->active_port->available != PA_AVAILABLE_NO) && (source->active_port->priority >= prio))
+        if ((source->active_port->available != PA_AVAILABLE_NO) && (source->active_port->priority >= port->priority))
             return false;
     }
 
@@ -105,12 +140,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, port->priority);
+                good = profile_good_for_output(profile, port);
                 break;
 
             case PA_DIRECTION_INPUT:
                 name = profile->input_name;
-                good = profile_good_for_input(profile, port->priority);
+                good = profile_good_for_input(profile, port);
                 break;
         }
 
@@ -324,9 +359,142 @@ 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(pa_card *card, 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(card->sources) != 1) {
+        pa_card_set_preferred_port(card, PA_DIRECTION_INPUT, 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(card->sinks) > 0 && !pa_safe_streq(old_profile->output_name, new_profile->output_name)) {
+        pa_card_set_preferred_port(card, PA_DIRECTION_INPUT, NULL);
+        return;
+    }
+
+    source = pa_idxset_first(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. */
+    pa_card_set_preferred_port(card, PA_DIRECTION_INPUT, source->active_port);
+}
+
+static void update_preferred_output_port(pa_card *card, 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(card->sinks) != 1) {
+        pa_card_set_preferred_port(card, PA_DIRECTION_OUTPUT, 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(card->sources) > 0 && !pa_safe_streq(old_profile->input_name, new_profile->input_name)) {
+        pa_card_set_preferred_port(card, PA_DIRECTION_OUTPUT, NULL);
+        return;
+    }
+
+    sink = pa_idxset_first(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. */
+    pa_card_set_preferred_port(card, PA_DIRECTION_OUTPUT, 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;
+
+    info = pa_hashmap_get(u->card_infos, card);
+    old_profile = info->active_profile;
+    new_profile = card->active_profile;
+    info->active_profile = new_profile;
+
+    /* 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;
+
+    update_preferred_input_port(card, old_profile, new_profile);
+    update_preferred_output_port(card, old_profile, new_profile);
+
+    return PA_HOOK_OK;
+}
+
+static pa_hook_result_t source_port_changed_callback(pa_core *core, pa_source *source, void *userdata) {
+    if (!source->save_port)
+        return PA_HOOK_OK;
+
+    pa_card_set_preferred_port(source->card, PA_DIRECTION_INPUT, source->active_port);
+
+    return PA_HOOK_OK;
+}
+
+static pa_hook_result_t sink_port_changed_callback(pa_core *core, pa_sink *sink, void *userdata) {
+    if (!sink->save_port)
+        return PA_HOOK_OK;
+
+    pa_card_set_preferred_port(sink->card, PA_DIRECTION_OUTPUT, 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);
@@ -334,8 +502,35 @@ int pa__init(pa_module*m) {
                            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_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, NULL);
+    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, NULL);
 
     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



More information about the pulseaudio-discuss mailing list