summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBastien Nocera <hadess@hadess.net>2016-01-11 19:03:51 +0100
committerBastien Nocera <hadess@hadess.net>2016-01-17 18:34:07 -0200
commitf3f6812eb9d4589ffe161260b80cb8a9609b3ab2 (patch)
tree6e186f7ab39e6a27953103a8698a290247d12fc2
parentd4eda71c49bb458eda7cf9d22d47002b8528552c (diff)
downloadlibgnome-volume-control-f3f6812eb9d4589ffe161260b80cb8a9609b3ab2.tar.gz
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
-rw-r--r--gvc-mixer-control.c355
-rw-r--r--gvc-mixer-control.h17
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