From f3f6812eb9d4589ffe161260b80cb8a9609b3ab2 Mon Sep 17 00:00:00 2001 From: Bastien Nocera Date: Mon, 11 Jan 2016 19:03:51 +0100 Subject: 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 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(+) 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 #include +#ifdef HAVE_ALSA +#include +#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 -- cgit v1.2.1