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
This commit is contained in:
Bastien Nocera 2016-01-11 19:03:51 +01:00
parent d4eda71c49
commit f3f6812eb9
2 changed files with 372 additions and 0 deletions

View File

@ -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;
}

View File

@ -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