[libgnome-volume-control] gvc: Add "what did you plug in" support API



commit f3f6812eb9d4589ffe161260b80cb8a9609b3ab2
Author: Bastien Nocera <hadess hadess net>
Date:   Mon Jan 11 19:03:51 2016 +0100

    gvc: Add "what did you plug in" support API
    
    Add "audio-device-selection-needed" which will be emitted when a
    headphones, headset or microphone is plugged into a jack socket that
    cannot detect which type it was.
    
    Once the user of libgnome-volume-control has asked the user which type
    of device this was, they can call gvc_mixer_control_set_headset_port()
    to switch the ports for that configuration.
    
    Note that gvc_mixer_control_set_headset_port() supports passing the
    card ID, but the detection code only supports a single such device. When
    we find hardware that can support > 1 such device, we can test and
    implement support without breaking the API.
    
    Based on the original code by David Henningsson <david henningsson canonical com>
    for the unity-settings-daemon
    
    https://bugzilla.gnome.org/show_bug.cgi?id=755062

 gvc-mixer-control.c |  355 +++++++++++++++++++++++++++++++++++++++++++++++++++
 gvc-mixer-control.h |   17 +++
 2 files changed, 372 insertions(+), 0 deletions(-)
---
diff --git a/gvc-mixer-control.c b/gvc-mixer-control.c
index 2177956..e2186b1 100644
--- a/gvc-mixer-control.c
+++ b/gvc-mixer-control.c
@@ -34,6 +34,10 @@
 #include <pulse/glib-mainloop.h>
 #include <pulse/ext-stream-restore.h>
 
+#ifdef HAVE_ALSA
+#include <alsa/asoundlib.h>
+#endif /* HAVE_ALSA */
+
 #include "gvc-mixer-control.h"
 #include "gvc-mixer-sink.h"
 #include "gvc-mixer-source.h"
@@ -97,6 +101,13 @@ struct GvcMixerControlPrivate
          * device the user wishes to use. */
         guint            profile_swapping_device_id;
 
+#ifdef HAVE_ALSA
+        int      headset_card;
+        gboolean has_headsetmic;
+        gboolean has_headphonemic;
+        gboolean headset_plugged_in;
+#endif /* HAVE_ALSA */
+
         GvcMixerControlState state;
 };
 
@@ -115,6 +126,7 @@ enum {
         INPUT_ADDED,
         OUTPUT_REMOVED,
         INPUT_REMOVED,
+        AUDIO_DEVICE_SELECTION_NEEDED,
         LAST_SIGNAL
 };
 
@@ -2053,6 +2065,332 @@ create_ui_device_from_card (GvcMixerControl *control,
                              g_object_ref (out));
 }
 
+#ifdef HAVE_ALSA
+typedef struct {
+        char *port_name_to_set;
+        int headset_card;
+} PortStatusData;
+
+static void
+port_status_data_free (PortStatusData *data)
+{
+        if (data == NULL)
+                return;
+        g_free (data->port_name_to_set);
+        g_free (data);
+}
+
+/*
+ We need to re-enumerate sources and sinks every time the user makes a choice,
+ because they can change due to use interaction in other software (or policy
+ changes inside PulseAudio). Enumeration means PulseAudio will do a series of
+ callbacks, one for every source/sink.
+ Set the port when we find the correct source/sink.
+ */
+
+static void
+sink_info_cb (pa_context         *c,
+              const pa_sink_info *i,
+              int                 eol,
+              void               *userdata)
+{
+        PortStatusData *data = userdata;
+        pa_operation *o;
+        int j;
+        const char *s;
+
+        if (eol) {
+                port_status_data_free (data);
+                return;
+        }
+
+        if (i->card != data->headset_card)
+                return;
+
+        if (i->active_port &&
+            strcmp (i->active_port->name, s) == 0)
+                return;
+
+        s = data->port_name_to_set;
+
+        for (j = 0; j < i->n_ports; j++)
+                if (strcmp (i->ports[j]->name, s) == 0)
+                        break;
+
+        if (j >= i->n_ports)
+                return;
+
+        o = pa_context_set_sink_port_by_index (c, i->index, s, NULL, NULL);
+        g_clear_pointer (&o, pa_operation_unref);
+        port_status_data_free (data);
+}
+
+static void
+source_info_cb (pa_context           *c,
+                const pa_source_info *i,
+                int                   eol,
+                void                 *userdata)
+{
+        PortStatusData *data = userdata;
+        pa_operation *o;
+        int j;
+        const char *s;
+
+        if (eol) {
+                port_status_data_free (data);
+                return;
+        }
+
+        if (i->card != data->headset_card)
+                return;
+
+        if (i->active_port && strcmp (i->active_port->name, s) == 0)
+                return;
+
+        s = data->port_name_to_set;
+
+        for (j = 0; j < i->n_ports; j++)
+                if (strcmp (i->ports[j]->name, s) == 0)
+                        break;
+
+        if (j >= i->n_ports)
+                return;
+
+        o = pa_context_set_source_port_by_index(c, i->index, s, NULL, NULL);
+        g_clear_pointer (&o, pa_operation_unref);
+        port_status_data_free (data);
+}
+
+static void
+gvc_mixer_control_set_port_status_for_headset (GvcMixerControl *control,
+                                               guint            id,
+                                               const char      *port_name,
+                                               gboolean         is_output)
+{
+        pa_operation *o;
+        PortStatusData *data;
+
+        data = g_new0 (PortStatusData, 1);
+        data->port_name_to_set = g_strdup (port_name);
+        data->headset_card = id;
+
+        if (is_output)
+                o = pa_context_get_sink_info_list (control->priv->pa_context, sink_info_cb, data);
+        else
+                o = pa_context_get_source_info_list (control->priv->pa_context, source_info_cb, data);
+
+        g_clear_pointer (&o, pa_operation_unref);
+}
+#endif /* HAVE_ALSA */
+
+void
+gvc_mixer_control_set_headset_port (GvcMixerControl      *control,
+                                    guint                 id,
+                                    GvcHeadsetPortChoice  choice)
+{
+#ifdef HAVE_ALSA
+        switch (choice) {
+        case GVC_HEADSET_PORT_CHOICE_HEADPHONES:
+                gvc_mixer_control_set_port_status_for_headset (control, id, "analog-output-headphones", 
TRUE);
+                gvc_mixer_control_set_port_status_for_headset (control, id, "analog-input-internal-mic", 
FALSE);
+                break;
+        case GVC_HEADSET_PORT_CHOICE_HEADSET:
+                gvc_mixer_control_set_port_status_for_headset (control, id, "analog-output-headphones", 
TRUE);
+                gvc_mixer_control_set_port_status_for_headset (control, id, "analog-input-headset-mic", 
FALSE);
+                break;
+        case GVC_HEADSET_PORT_CHOICE_MIC:
+                gvc_mixer_control_set_port_status_for_headset (control, id, "analog-output-speaker", TRUE);
+                gvc_mixer_control_set_port_status_for_headset (control, id, "analog-input-headphone-mic", 
FALSE);
+                break;
+        default:
+                g_assert_not_reached ();
+        }
+#else
+        g_warning ("BUG: libgnome-volume-control compiled without ALSA support");
+#endif /* HAVE_ALSA */
+}
+
+#ifdef HAVE_ALSA
+typedef struct {
+        const pa_card_port_info *headphones;
+        const pa_card_port_info *headsetmic;
+        const pa_card_port_info *headphonemic;
+} headset_ports;
+
+/*
+   TODO: Check if we still need this with the changed PA port names
+
+   In PulseAudio ports will show up with the following names:
+   Headphones - analog-output-headphones
+   Headset mic - analog-input-headset-mic (was: analog-input-microphone-headset)
+   Jack in mic-in mode - analog-input-headphone-mic (was: analog-input-microphone)
+
+   However, since regular mics also show up as analog-input-microphone,
+   we need to check for certain controls on alsa mixer level too, to know
+   if we deal with a separate mic jack, or a multi-function jack with a
+   mic-in mode (also called "headphone mic").
+   We check for the following names:
+
+   Headphone Mic Jack - indicates headphone and mic-in mode share the same jack,
+     i e, not two separate jacks. Hardware cannot distinguish between a
+     headphone and a mic.
+   Headset Mic Phantom Jack - indicates headset jack where hardware can not
+     distinguish between headphones and headsets
+   Headset Mic Jack - indicates headset jack where hardware can distinguish
+     between headphones and headsets. There is no use popping up a dialog in
+     this case, unless we already need to do this for the mic-in mode.
+*/
+
+static headset_ports *
+get_headset_ports (const pa_card_info *c)
+{
+        headset_ports *h;
+        guint i;
+
+        h = g_new0 (headset_ports, 1);
+
+        for (i = 0; i < c->n_ports; i++) {
+                pa_card_port_info *p = c->ports[i];
+
+                if (strcmp (p->name, "analog-output-headphones") == 0)
+                        h->headphones = p;
+                else if (strcmp (p->name, "analog-input-headset-mic") == 0)
+                        h->headsetmic = p;
+                else if (strcmp(p->name, "analog-input-headphone-mic") == 0)
+                        h->headphonemic = p;
+        }
+        return h;
+}
+
+static gboolean
+verify_alsa_card (int       cardindex,
+                  gboolean *headsetmic,
+                  gboolean *headphonemic)
+{
+        char *ctlstr;
+        snd_hctl_t *hctl;
+        snd_ctl_elem_id_t *id;
+        int err;
+
+        *headsetmic = FALSE;
+        *headphonemic = FALSE;
+
+        ctlstr = g_strdup_printf ("hw:%i", cardindex);
+        if ((err = snd_hctl_open (&hctl, ctlstr, 0)) < 0) {
+                g_warning ("snd_hctl_open failed: %s", snd_strerror(err));
+                g_free (ctlstr);
+                return FALSE;
+        }
+        g_free (ctlstr);
+
+        if ((err = snd_hctl_load (hctl)) < 0) {
+                g_warning ("snd_hctl_load failed: %s", snd_strerror(err));
+                snd_hctl_close (hctl);
+                return FALSE;
+        }
+
+        snd_ctl_elem_id_alloca (&id);
+
+        snd_ctl_elem_id_clear (id);
+        snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD);
+        snd_ctl_elem_id_set_name (id, "Headphone Mic Jack");
+        if (snd_hctl_find_elem (hctl, id))
+                *headphonemic = TRUE;
+
+        snd_ctl_elem_id_clear (id);
+        snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD);
+        snd_ctl_elem_id_set_name (id, "Headset Mic Phantom Jack");
+        if (snd_hctl_find_elem (hctl, id))
+                *headsetmic = TRUE;
+
+        if (*headphonemic) {
+                snd_ctl_elem_id_clear (id);
+                snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD);
+                snd_ctl_elem_id_set_name (id, "Headset Mic Jack");
+                if (snd_hctl_find_elem (hctl, id))
+                        *headsetmic = TRUE;
+        }
+
+        snd_hctl_close (hctl);
+        return *headsetmic || *headphonemic;
+}
+
+static void
+check_audio_device_selection_needed (GvcMixerControl    *control,
+                                     const pa_card_info *info)
+{
+        headset_ports *h;
+        gboolean start_dialog, stop_dialog;
+
+        start_dialog = FALSE;
+        stop_dialog = FALSE;
+        h = get_headset_ports (info);
+
+        if (!h->headphones ||
+            (!h->headsetmic && !h->headphonemic)) {
+                /* Not a headset jack */
+                goto out;
+        }
+
+        if (control->priv->headset_card != (int) info->index) {
+                int cardindex;
+                gboolean hsmic, hpmic;
+                const char *s;
+
+                s = pa_proplist_gets (info->proplist, "alsa.card");
+                if (!s)
+                        goto out;
+
+                cardindex = strtol (s, NULL, 10);
+                if (cardindex == 0 && strcmp(s, "0") != 0)
+                        goto out;
+
+                if (!verify_alsa_card(cardindex, &hsmic, &hpmic))
+                        goto out;
+
+                control->priv->headset_card = info->index;
+                control->priv->has_headsetmic = hsmic && h->headsetmic;
+                control->priv->has_headphonemic = hpmic && h->headphonemic;
+        } else {
+                start_dialog = (h->headphones->available != PA_PORT_AVAILABLE_NO) && 
!control->priv->headset_plugged_in;
+                stop_dialog = (h->headphones->available == PA_PORT_AVAILABLE_NO) && 
control->priv->headset_plugged_in;
+        }
+
+        control->priv->headset_plugged_in = h->headphones->available != PA_PORT_AVAILABLE_NO;
+
+        if (!start_dialog &&
+            !stop_dialog)
+                goto out;
+
+        if (stop_dialog) {
+                g_signal_emit (G_OBJECT (control),
+                               signals[AUDIO_DEVICE_SELECTION_NEEDED],
+                               0,
+                               info->index,
+                               FALSE,
+                               GVC_HEADSET_PORT_CHOICE_NONE);
+        } else {
+                GvcHeadsetPortChoice choices;
+
+                choices = GVC_HEADSET_PORT_CHOICE_HEADPHONES;
+                if (control->priv->has_headsetmic)
+                        choices |= GVC_HEADSET_PORT_CHOICE_HEADSET;
+                if (control->priv->has_headphonemic)
+                        choices |= GVC_HEADSET_PORT_CHOICE_MIC;
+
+                g_signal_emit (G_OBJECT (control),
+                               signals[AUDIO_DEVICE_SELECTION_NEEDED],
+                               0,
+                               info->index,
+                               TRUE,
+                               choices);
+        }
+
+out:
+        g_free (h);
+}
+#endif /* HAVE_ALSA */
+
 /*
  * At this point we can determine all devices available to us (besides network 'ports')
  * This is done by the following:
@@ -2175,6 +2513,11 @@ update_card (GvcMixerControl      *control,
                         }
                 }
         }
+
+#ifdef HAVE_ALSA
+        check_audio_device_selection_needed (control, info);
+#endif /* HAVE_ALSA */
+
         g_signal_emit (G_OBJECT (control),
                        signals[CARD_ADDED],
                        0,
@@ -3242,6 +3585,14 @@ gvc_mixer_control_class_init (GvcMixerControlClass *klass)
                               NULL, NULL,
                               g_cclosure_marshal_VOID__UINT,
                               G_TYPE_NONE, 1, G_TYPE_UINT);
+        signals [AUDIO_DEVICE_SELECTION_NEEDED] =
+                g_signal_new ("audio-device-selection-needed",
+                              G_TYPE_FROM_CLASS (klass),
+                              G_SIGNAL_RUN_LAST,
+                              0,
+                              NULL, NULL,
+                              g_cclosure_marshal_generic,
+                              G_TYPE_NONE, 3, G_TYPE_UINT, G_TYPE_BOOLEAN, G_TYPE_UINT);
         signals [CARD_ADDED] =
                 g_signal_new ("card-added",
                               G_TYPE_FROM_CLASS (klass),
@@ -3348,6 +3699,10 @@ gvc_mixer_control_init (GvcMixerControl *control)
 
         control->priv->clients = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_free);
 
+#ifdef HAVE_ALSA
+        control->priv->headset_card = -1;
+#endif /* HAVE_ALSA */
+
         control->priv->state = GVC_STATE_CLOSED;
 }
 
diff --git a/gvc-mixer-control.h b/gvc-mixer-control.h
index 4ba1d3b..8137849 100644
--- a/gvc-mixer-control.h
+++ b/gvc-mixer-control.h
@@ -36,6 +36,14 @@ typedef enum
         GVC_STATE_FAILED
 } GvcMixerControlState;
 
+typedef enum
+{
+        GVC_HEADSET_PORT_CHOICE_NONE        = 0,
+        GVC_HEADSET_PORT_CHOICE_HEADPHONES  = 1 << 0,
+        GVC_HEADSET_PORT_CHOICE_HEADSET     = 1 << 1,
+        GVC_HEADSET_PORT_CHOICE_MIC         = 1 << 2
+} GvcHeadsetPortChoice;
+
 #define GVC_TYPE_MIXER_CONTROL         (gvc_mixer_control_get_type ())
 #define GVC_MIXER_CONTROL(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_CONTROL, 
GvcMixerControl))
 #define GVC_MIXER_CONTROL_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_CONTROL, 
GvcMixerControlClass))
@@ -83,6 +91,11 @@ typedef struct
                                         guint            id);
         void (*input_removed)          (GvcMixerControl *control,
                                         guint            id);
+        void (*audio_device_selection_needed)
+                                       (GvcMixerControl      *control,
+                                        guint                 id,
+                                        gboolean              show_dialog,
+                                        GvcHeadsetPortChoice  choices);
 } GvcMixerControlClass;
 
 GType               gvc_mixer_control_get_type            (void);
@@ -131,6 +144,10 @@ gboolean                gvc_mixer_control_change_profile_on_selected_device (Gvc
                                                                              GvcMixerUIDevice *device,
                                                                              const gchar* profile);
 
+void                    gvc_mixer_control_set_headset_port                  (GvcMixerControl      *control,
+                                                                             guint                 id,
+                                                                             GvcHeadsetPortChoice  choices);
+
 GvcMixerControlState gvc_mixer_control_get_state            (GvcMixerControl *control);
 
 G_END_DECLS


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