libgnome-volume-control/gvc-mixer-control.c

3889 lines
146 KiB
C
Raw Normal View History

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
*
* Copyright (C) 2006-2008 Lennart Poettering
* Copyright (C) 2008 Sjoerd Simons <sjoerd@luon.net>
* Copyright (C) 2008 William Jon McCann
* Copyright (C) 2012 Conor Curran
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
*/
#include "config.h"
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <glib.h>
#include <glib/gi18n-lib.h>
#include <pulse/pulseaudio.h>
#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"
#include "gvc-mixer-sink-input.h"
#include "gvc-mixer-source-output.h"
#include "gvc-mixer-event-role.h"
#include "gvc-mixer-card.h"
#include "gvc-mixer-card-private.h"
#include "gvc-channel-map-private.h"
#include "gvc-mixer-control-private.h"
#include "gvc-mixer-ui-device.h"
#define RECONNECT_DELAY 5
enum {
PROP_0,
PROP_NAME,
N_PROPS
};
static GParamSpec *obj_props[N_PROPS] = { NULL, };
struct GvcMixerControlPrivate
{
pa_glib_mainloop *pa_mainloop;
pa_mainloop_api *pa_api;
pa_context *pa_context;
guint server_protocol_version;
int n_outstanding;
guint reconnect_id;
char *name;
gboolean default_sink_is_set;
guint default_sink_id;
char *default_sink_name;
gboolean default_source_is_set;
guint default_source_id;
char *default_source_name;
gboolean event_sink_input_is_set;
guint event_sink_input_id;
GHashTable *all_streams;
GHashTable *sinks; /* fixed outputs */
GHashTable *sources; /* fixed inputs */
GHashTable *sink_inputs; /* routable output streams */
GHashTable *source_outputs; /* routable input streams */
GHashTable *clients;
GHashTable *cards;
GvcMixerStream *new_default_sink_stream; /* new default sink stream, used in gvc_mixer_control_set_default_sink () */
GvcMixerStream *new_default_source_stream; /* new default source stream, used in gvc_mixer_control_set_default_source () */
GHashTable *ui_outputs; /* UI visible outputs */
GHashTable *ui_inputs; /* UI visible inputs */
/* When we change profile on a device that is not the server default sink,
* it will jump back to the default sink set by the server to prevent the
* audio setup from being 'outputless'.
*
* All well and good but then when we get the new stream created for the
* new profile how do we know that this is the intended default or selected
* 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;
char *headphones_name;
char *headsetmic_name;
char *headphonemic_name;
char *internalspk_name;
char *internalmic_name;
#endif /* HAVE_ALSA */
GvcMixerControlState state;
};
enum {
STATE_CHANGED,
STREAM_ADDED,
STREAM_REMOVED,
STREAM_CHANGED,
CARD_ADDED,
CARD_REMOVED,
DEFAULT_SINK_CHANGED,
DEFAULT_SOURCE_CHANGED,
ACTIVE_OUTPUT_UPDATE,
ACTIVE_INPUT_UPDATE,
OUTPUT_ADDED,
INPUT_ADDED,
OUTPUT_REMOVED,
INPUT_REMOVED,
AUDIO_DEVICE_SELECTION_NEEDED,
LAST_SIGNAL
};
static guint signals [LAST_SIGNAL] = { 0, };
static void gvc_mixer_control_finalize (GObject *object);
G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerControl, gvc_mixer_control, G_TYPE_OBJECT)
pa_context *
gvc_mixer_control_get_pa_context (GvcMixerControl *control)
{
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
return control->priv->pa_context;
}
/**
* gvc_mixer_control_get_event_sink_input:
* @control:
*
* Returns: (transfer none):
*/
GvcMixerStream *
gvc_mixer_control_get_event_sink_input (GvcMixerControl *control)
{
GvcMixerStream *stream;
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
stream = g_hash_table_lookup (control->priv->all_streams,
GUINT_TO_POINTER (control->priv->event_sink_input_id));
return stream;
}
static void
gvc_mixer_control_stream_restore_cb (pa_context *c,
GvcMixerStream *new_stream,
const pa_ext_stream_restore_info *info,
GvcMixerControl *control)
{
pa_operation *o;
pa_ext_stream_restore_info new_info;
if (new_stream == NULL)
return;
new_info.name = info->name;
new_info.channel_map = info->channel_map;
new_info.volume = info->volume;
new_info.mute = info->mute;
new_info.device = gvc_mixer_stream_get_name (new_stream);
o = pa_ext_stream_restore_write (control->priv->pa_context,
PA_UPDATE_REPLACE,
&new_info, 1,
TRUE, NULL, NULL);
if (o == NULL) {
g_warning ("pa_ext_stream_restore_write() failed: %s",
pa_strerror (pa_context_errno (control->priv->pa_context)));
return;
}
g_debug ("Changed default device for %s to %s", info->name, new_info.device);
pa_operation_unref (o);
}
static void
gvc_mixer_control_stream_restore_sink_cb (pa_context *c,
const pa_ext_stream_restore_info *info,
int eol,
void *userdata)
{
GvcMixerControl *control = (GvcMixerControl *) userdata;
if (eol || info == NULL || !g_str_has_prefix(info->name, "sink-input-by"))
return;
gvc_mixer_control_stream_restore_cb (c, control->priv->new_default_sink_stream, info, control);
}
static void
gvc_mixer_control_stream_restore_source_cb (pa_context *c,
const pa_ext_stream_restore_info *info,
int eol,
void *userdata)
{
GvcMixerControl *control = (GvcMixerControl *) userdata;
if (eol || info == NULL || !g_str_has_prefix(info->name, "source-output-by"))
return;
gvc_mixer_control_stream_restore_cb (c, control->priv->new_default_source_stream, info, control);
}
/**
* gvc_mixer_control_lookup_device_from_stream:
* @control:
* @stream:
*
* Returns: (transfer none): a #GvcUIDevice or %NULL
*/
GvcMixerUIDevice *
gvc_mixer_control_lookup_device_from_stream (GvcMixerControl *control,
GvcMixerStream *stream)
{
GList *devices, *d;
gboolean is_network_stream;
const GList *ports;
GvcMixerUIDevice *ret;
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
if (GVC_IS_MIXER_SOURCE (stream))
devices = g_hash_table_get_values (control->priv->ui_inputs);
else
devices = g_hash_table_get_values (control->priv->ui_outputs);
ret = NULL;
ports = gvc_mixer_stream_get_ports (stream);
is_network_stream = (ports == NULL);
for (d = devices; d != NULL; d = d->next) {
GvcMixerUIDevice *device = d->data;
guint stream_id = G_MAXUINT;
g_object_get (G_OBJECT (device),
"stream-id", &stream_id,
NULL);
if (is_network_stream &&
stream_id == gvc_mixer_stream_get_id (stream)) {
g_debug ("lookup device from stream - %s - it is a network_stream ",
gvc_mixer_ui_device_get_description (device));
ret = device;
break;
} else if (!is_network_stream) {
const GvcMixerStreamPort *port;
port = gvc_mixer_stream_get_port (stream);
if (stream_id == gvc_mixer_stream_get_id (stream) &&
g_strcmp0 (gvc_mixer_ui_device_get_port (device),
port->port) == 0) {
g_debug ("lookup-device-from-stream found device: device description '%s', device port = '%s', device stream id %i AND stream port = '%s' stream id '%u' and stream description '%s'",
gvc_mixer_ui_device_get_description (device),
gvc_mixer_ui_device_get_port (device),
stream_id,
port->port,
gvc_mixer_stream_get_id (stream),
gvc_mixer_stream_get_description (stream));
ret = device;
break;
}
}
}
g_debug ("gvc_mixer_control_lookup_device_from_stream - Could not find a device for stream '%s'",gvc_mixer_stream_get_description (stream));
g_list_free (devices);
return ret;
}
gboolean
gvc_mixer_control_set_default_sink (GvcMixerControl *control,
GvcMixerStream *stream)
{
pa_operation *o;
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
g_debug ("about to set default sink on server");
o = pa_context_set_default_sink (control->priv->pa_context,
gvc_mixer_stream_get_name (stream),
NULL,
NULL);
if (o == NULL) {
g_warning ("pa_context_set_default_sink() failed: %s",
pa_strerror (pa_context_errno (control->priv->pa_context)));
return FALSE;
}
pa_operation_unref (o);
control->priv->new_default_sink_stream = stream;
g_object_add_weak_pointer (G_OBJECT (stream), (gpointer *) &control->priv->new_default_sink_stream);
o = pa_ext_stream_restore_read (control->priv->pa_context,
gvc_mixer_control_stream_restore_sink_cb,
control);
if (o == NULL) {
g_warning ("pa_ext_stream_restore_read() failed: %s",
pa_strerror (pa_context_errno (control->priv->pa_context)));
return FALSE;
}
pa_operation_unref (o);
return TRUE;
}
gboolean
gvc_mixer_control_set_default_source (GvcMixerControl *control,
GvcMixerStream *stream)
{
GvcMixerUIDevice* input;
pa_operation *o;
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
o = pa_context_set_default_source (control->priv->pa_context,
gvc_mixer_stream_get_name (stream),
NULL,
NULL);
if (o == NULL) {
g_warning ("pa_context_set_default_source() failed");
return FALSE;
}
pa_operation_unref (o);
control->priv->new_default_source_stream = stream;
g_object_add_weak_pointer (G_OBJECT (stream), (gpointer *) &control->priv->new_default_source_stream);
o = pa_ext_stream_restore_read (control->priv->pa_context,
gvc_mixer_control_stream_restore_source_cb,
control);
if (o == NULL) {
g_warning ("pa_ext_stream_restore_read() failed: %s",
pa_strerror (pa_context_errno (control->priv->pa_context)));
return FALSE;
}
pa_operation_unref (o);
/* source change successful, update the UI. */
input = gvc_mixer_control_lookup_device_from_stream (control, stream);
g_signal_emit (G_OBJECT (control),
signals[ACTIVE_INPUT_UPDATE],
0,
gvc_mixer_ui_device_get_id (input));
return TRUE;
}
/**
* gvc_mixer_control_get_default_sink:
* @control:
*
* Returns: (transfer none):
*/
GvcMixerStream *
gvc_mixer_control_get_default_sink (GvcMixerControl *control)
{
GvcMixerStream *stream;
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
if (control->priv->default_sink_is_set) {
stream = g_hash_table_lookup (control->priv->all_streams,
GUINT_TO_POINTER (control->priv->default_sink_id));
} else {
stream = NULL;
}
return stream;
}
/**
* gvc_mixer_control_get_default_source:
* @control:
*
* Returns: (transfer none):
*/
GvcMixerStream *
gvc_mixer_control_get_default_source (GvcMixerControl *control)
{
GvcMixerStream *stream;
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
if (control->priv->default_source_is_set) {
stream = g_hash_table_lookup (control->priv->all_streams,
GUINT_TO_POINTER (control->priv->default_source_id));
} else {
stream = NULL;
}
return stream;
}
static gpointer
gvc_mixer_control_lookup_id (GHashTable *hash_table,
guint id)
{
return g_hash_table_lookup (hash_table,
GUINT_TO_POINTER (id));
}
/**
* gvc_mixer_control_lookup_stream_id:
* @control:
* @id:
*
* Returns: (transfer none):
*/
GvcMixerStream *
gvc_mixer_control_lookup_stream_id (GvcMixerControl *control,
guint id)
{
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
return gvc_mixer_control_lookup_id (control->priv->all_streams, id);
}
/**
* gvc_mixer_control_lookup_card_id:
* @control:
* @id:
*
* Returns: (transfer none):
*/
GvcMixerCard *
gvc_mixer_control_lookup_card_id (GvcMixerControl *control,
guint id)
{
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
return gvc_mixer_control_lookup_id (control->priv->cards, id);
}
/**
* gvc_mixer_control_lookup_output_id:
* @control:
* @id:
*
* Returns: (transfer none):
*/
GvcMixerUIDevice *
gvc_mixer_control_lookup_output_id (GvcMixerControl *control,
guint id)
{
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
return gvc_mixer_control_lookup_id (control->priv->ui_outputs, id);
}
/**
* gvc_mixer_control_lookup_input_id:
* @control:
* @id:
*
* Returns: (transfer none):
*/
GvcMixerUIDevice *
gvc_mixer_control_lookup_input_id (GvcMixerControl *control,
guint id)
{
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
return gvc_mixer_control_lookup_id (control->priv->ui_inputs, id);
}
/**
* gvc_mixer_control_get_stream_from_device:
* @control:
* @device:
*
* Returns: (transfer none):
*/
GvcMixerStream *
gvc_mixer_control_get_stream_from_device (GvcMixerControl *control,
GvcMixerUIDevice *device)
{
gint stream_id;
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
stream_id = gvc_mixer_ui_device_get_stream_id (device);
if (stream_id == GVC_MIXER_UI_DEVICE_INVALID) {
g_debug ("gvc_mixer_control_get_stream_from_device - device has a null stream");
return NULL;
}
return gvc_mixer_control_lookup_stream_id (control, stream_id);
}
/**
* gvc_mixer_control_change_profile_on_selected_device:
* @control:
* @device:
2019-03-14 16:47:00 -04:00
* @profile: (allow-none): Can be %NULL if any profile present on this port is okay
*
* Returns: This method will attempt to swap the profile on the card of
* the device with given profile name. If successfull it will set the
* preferred profile on that device so as we know the next time the user
* moves to that device it should have this profile active.
*/
gboolean
gvc_mixer_control_change_profile_on_selected_device (GvcMixerControl *control,
GvcMixerUIDevice *device,
const gchar *profile)
{
const gchar *best_profile;
GvcMixerCardProfile *current_profile;
GvcMixerCard *card;
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE);
g_object_get (G_OBJECT (device), "card", &card, NULL);
current_profile = gvc_mixer_card_get_profile (card);
if (current_profile)
best_profile = gvc_mixer_ui_device_get_best_profile (device, profile, current_profile->profile);
else
best_profile = profile;
g_assert (best_profile);
g_debug ("Selected '%s', moving to profile '%s' on card '%s' on stream id %i",
profile ? profile : "(any)", best_profile,
gvc_mixer_card_get_name (card),
gvc_mixer_ui_device_get_stream_id (device));
g_debug ("default sink name = %s and default sink id %u",
control->priv->default_sink_name,
control->priv->default_sink_id);
control->priv->profile_swapping_device_id = gvc_mixer_ui_device_get_id (device);
if (gvc_mixer_card_change_profile (card, best_profile)) {
gvc_mixer_ui_device_set_user_preferred_profile (device, best_profile);
return TRUE;
}
return FALSE;
}
/**
* gvc_mixer_control_change_output:
* @control:
* @output:
* This method is called from the UI when the user selects a previously unselected device.
* - Firstly it queries the stream from the device.
* - It assumes that if the stream is null that it cannot be a bluetooth or network stream (they never show unless they have valid sinks and sources)
* In the scenario of a NULL stream on the device
* - It fetches the device's preferred profile or if NUll the profile with the highest priority on that device.
* - It then caches this device in control->priv->cached_desired_output_id so that when the update_sink triggered
* from when we attempt to change profile we will know exactly what device to highlight on that stream.
* - It attempts to swap the profile on the card from that device and returns.
* - Next, it handles network or bluetooth streams that only require their stream to be made the default.
* - Next it deals with port changes so if the stream's active port is not the same as the port on the device
* it will attempt to change the port on that stream to be same as the device. If this fails it will return.
* - Finally it will set this new stream to be the default stream and emit a signal for the UI confirming the active output device.
*/
void
gvc_mixer_control_change_output (GvcMixerControl *control,
GvcMixerUIDevice* output)
{
GvcMixerStream *stream;
GvcMixerStream *default_stream;
const GvcMixerStreamPort *active_port;
const gchar *output_port;
g_return_if_fail (GVC_IS_MIXER_CONTROL (control));
g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (output));
g_debug ("control change output");
stream = gvc_mixer_control_get_stream_from_device (control, output);
if (stream == NULL) {
gvc_mixer_control_change_profile_on_selected_device (control,
output, NULL);
return;
}
/* Handle a network sink as a portless or cardless device */
if (!gvc_mixer_ui_device_has_ports (output)) {
g_debug ("Did we try to move to a software/bluetooth sink ?");
if (gvc_mixer_control_set_default_sink (control, stream)) {
/* sink change was successful, update the UI.*/
g_signal_emit (G_OBJECT (control),
signals[ACTIVE_OUTPUT_UPDATE],
0,
gvc_mixer_ui_device_get_id (output));
}
else {
g_warning ("Failed to set default sink with stream from output %s",
gvc_mixer_ui_device_get_description (output));
}
return;
}
active_port = gvc_mixer_stream_get_port (stream);
output_port = gvc_mixer_ui_device_get_port (output);
/* First ensure the correct port is active on the sink */
if (g_strcmp0 (active_port->port, output_port) != 0) {
g_debug ("Port change, switch to = %s", output_port);
if (gvc_mixer_stream_change_port (stream, output_port) == FALSE) {
g_warning ("Could not change port !");
return;
}
}
default_stream = gvc_mixer_control_get_default_sink (control);
/* Finally if we are not on the correct stream, swap over. */
if (stream != default_stream) {
GvcMixerUIDevice* device;
g_debug ("Attempting to swap over to stream %s ",
gvc_mixer_stream_get_description (stream));
if (gvc_mixer_control_set_default_sink (control, stream)) {
device = gvc_mixer_control_lookup_device_from_stream (control, stream);
g_signal_emit (G_OBJECT (control),
signals[ACTIVE_OUTPUT_UPDATE],
0,
gvc_mixer_ui_device_get_id (device));
} else {
/* If the move failed for some reason reset the UI. */
device = gvc_mixer_control_lookup_device_from_stream (control, default_stream);
g_signal_emit (G_OBJECT (control),
signals[ACTIVE_OUTPUT_UPDATE],
0,
gvc_mixer_ui_device_get_id (device));
}
}
}
/**
* gvc_mixer_control_change_input:
* @control:
* @input:
* This method is called from the UI when the user selects a previously unselected device.
* - Firstly it queries the stream from the device.
* - It assumes that if the stream is null that it cannot be a bluetooth or network stream (they never show unless they have valid sinks and sources)
* In the scenario of a NULL stream on the device
* - It fetches the device's preferred profile or if NUll the profile with the highest priority on that device.
* - It then caches this device in control->priv->cached_desired_input_id so that when the update_source triggered
* from when we attempt to change profile we will know exactly what device to highlight on that stream.
* - It attempts to swap the profile on the card from that device and returns.
* - Next, it handles network or bluetooth streams that only require their stream to be made the default.
* - Next it deals with port changes so if the stream's active port is not the same as the port on the device
* it will attempt to change the port on that stream to be same as the device. If this fails it will return.
* - Finally it will set this new stream to be the default stream and emit a signal for the UI confirming the active input device.
*/
void
gvc_mixer_control_change_input (GvcMixerControl *control,
GvcMixerUIDevice* input)
{
GvcMixerStream *stream;
GvcMixerStream *default_stream;
const GvcMixerStreamPort *active_port;
const gchar *input_port;
g_return_if_fail (GVC_IS_MIXER_CONTROL (control));
g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (input));
stream = gvc_mixer_control_get_stream_from_device (control, input);
if (stream == NULL) {
gvc_mixer_control_change_profile_on_selected_device (control,
input, NULL);
return;
}
/* Handle a network sink as a portless/cardless device */
if (!gvc_mixer_ui_device_has_ports (input)) {
g_debug ("Did we try to move to a software/bluetooth source ?");
if (! gvc_mixer_control_set_default_source (control, stream)) {
g_warning ("Failed to set default source with stream from input %s",
gvc_mixer_ui_device_get_description (input));
}
return;
}
active_port = gvc_mixer_stream_get_port (stream);
input_port = gvc_mixer_ui_device_get_port (input);
/* First ensure the correct port is active on the sink */
if (g_strcmp0 (active_port->port, input_port) != 0) {
g_debug ("Port change, switch to = %s", input_port);
if (gvc_mixer_stream_change_port (stream, input_port) == FALSE) {
g_warning ("Could not change port!");
return;
}
}
default_stream = gvc_mixer_control_get_default_source (control);
/* Finally if we are not on the correct stream, swap over. */
if (stream != default_stream) {
g_debug ("change-input - attempting to swap over to stream %s",
gvc_mixer_stream_get_description (stream));
gvc_mixer_control_set_default_source (control, stream);
}
}
static void
listify_hash_values_hfunc (gpointer key,
gpointer value,
gpointer user_data)
{
GSList **list = user_data;
*list = g_slist_prepend (*list, value);
}
static int
gvc_name_collate (const char *namea,
const char *nameb)
{
if (nameb == NULL && namea == NULL)
return 0;
if (nameb == NULL)
return 1;
if (namea == NULL)
return -1;
return g_utf8_collate (namea, nameb);
}
static int
gvc_card_collate (GvcMixerCard *a,
GvcMixerCard *b)
{
const char *namea;
const char *nameb;
g_return_val_if_fail (a == NULL || GVC_IS_MIXER_CARD (a), 0);
g_return_val_if_fail (b == NULL || GVC_IS_MIXER_CARD (b), 0);
namea = gvc_mixer_card_get_name (a);
nameb = gvc_mixer_card_get_name (b);
return gvc_name_collate (namea, nameb);
}
/**
* gvc_mixer_control_get_cards:
* @control:
*
* Returns: (transfer container) (element-type Gvc.MixerCard):
*/
GSList *
gvc_mixer_control_get_cards (GvcMixerControl *control)
{
GSList *retval;
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
retval = NULL;
g_hash_table_foreach (control->priv->cards,
listify_hash_values_hfunc,
&retval);
return g_slist_sort (retval, (GCompareFunc) gvc_card_collate);
}
static int
gvc_stream_collate (GvcMixerStream *a,
GvcMixerStream *b)
{
const char *namea;
const char *nameb;
g_return_val_if_fail (a == NULL || GVC_IS_MIXER_STREAM (a), 0);
g_return_val_if_fail (b == NULL || GVC_IS_MIXER_STREAM (b), 0);
namea = gvc_mixer_stream_get_name (a);
nameb = gvc_mixer_stream_get_name (b);
return gvc_name_collate (namea, nameb);
}
/**
* gvc_mixer_control_get_streams:
* @control:
*
* Returns: (transfer container) (element-type Gvc.MixerStream):
*/
GSList *
gvc_mixer_control_get_streams (GvcMixerControl *control)
{
GSList *retval;
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
retval = NULL;
g_hash_table_foreach (control->priv->all_streams,
listify_hash_values_hfunc,
&retval);
return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
}
/**
* gvc_mixer_control_get_sinks:
* @control:
*
* Returns: (transfer container) (element-type Gvc.MixerSink):
*/
GSList *
gvc_mixer_control_get_sinks (GvcMixerControl *control)
{
GSList *retval;
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
retval = NULL;
g_hash_table_foreach (control->priv->sinks,
listify_hash_values_hfunc,
&retval);
return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
}
/**
* gvc_mixer_control_get_sources:
* @control:
*
* Returns: (transfer container) (element-type Gvc.MixerSource):
*/
GSList *
gvc_mixer_control_get_sources (GvcMixerControl *control)
{
GSList *retval;
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
retval = NULL;
g_hash_table_foreach (control->priv->sources,
listify_hash_values_hfunc,
&retval);
return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
}
/**
* gvc_mixer_control_get_sink_inputs:
* @control:
*
* Returns: (transfer container) (element-type Gvc.MixerSinkInput):
*/
GSList *
gvc_mixer_control_get_sink_inputs (GvcMixerControl *control)
{
GSList *retval;
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
retval = NULL;
g_hash_table_foreach (control->priv->sink_inputs,
listify_hash_values_hfunc,
&retval);
return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
}
/**
* gvc_mixer_control_get_source_outputs:
* @control:
*
* Returns: (transfer container) (element-type Gvc.MixerSourceOutput):
*/
GSList *
gvc_mixer_control_get_source_outputs (GvcMixerControl *control)
{
GSList *retval;
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
retval = NULL;
g_hash_table_foreach (control->priv->source_outputs,
listify_hash_values_hfunc,
&retval);
return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
}
static void
dec_outstanding (GvcMixerControl *control)
{
if (control->priv->n_outstanding <= 0) {
return;
}
if (--control->priv->n_outstanding <= 0) {
control->priv->state = GVC_STATE_READY;
g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_READY);
}
}
GvcMixerControlState
gvc_mixer_control_get_state (GvcMixerControl *control)
{
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), GVC_STATE_CLOSED);
return control->priv->state;
}
static void
on_default_source_port_notify (GObject *object,
GParamSpec *pspec,
GvcMixerControl *control)
{
char *port;
GvcMixerUIDevice *input;
g_object_get (object, "port", &port, NULL);
input = gvc_mixer_control_lookup_device_from_stream (control,
GVC_MIXER_STREAM (object));
g_debug ("on_default_source_port_notify - moved to port '%s' which SHOULD ?? correspond to output '%s'",
port,
gvc_mixer_ui_device_get_description (input));
g_signal_emit (G_OBJECT (control),
signals[ACTIVE_INPUT_UPDATE],
0,
gvc_mixer_ui_device_get_id (input));
g_free (port);
}
static void
_set_default_source (GvcMixerControl *control,
GvcMixerStream *stream)
{
guint new_id;
if (stream == NULL) {
control->priv->default_source_id = 0;
control->priv->default_source_is_set = FALSE;
g_signal_emit (control,
signals[DEFAULT_SOURCE_CHANGED],
0,
PA_INVALID_INDEX);
return;
}
new_id = gvc_mixer_stream_get_id (stream);
if (control->priv->default_source_id != new_id) {
GvcMixerUIDevice *input;
control->priv->default_source_id = new_id;
control->priv->default_source_is_set = TRUE;
g_signal_emit (control,
signals[DEFAULT_SOURCE_CHANGED],
0,
new_id);
if (control->priv->default_source_is_set) {
g_signal_handlers_disconnect_by_func (gvc_mixer_control_get_default_source (control),
on_default_source_port_notify,
control);
}
g_signal_connect (stream,
"notify::port",
G_CALLBACK (on_default_source_port_notify),
control);
input = gvc_mixer_control_lookup_device_from_stream (control, stream);
g_signal_emit (G_OBJECT (control),
signals[ACTIVE_INPUT_UPDATE],
0,
gvc_mixer_ui_device_get_id (input));
}
}
static void
on_default_sink_port_notify (GObject *object,
GParamSpec *pspec,
GvcMixerControl *control)
{
char *port;
GvcMixerUIDevice *output;
g_object_get (object, "port", &port, NULL);
output = gvc_mixer_control_lookup_device_from_stream (control,
GVC_MIXER_STREAM (object));
if (output != NULL) {
g_debug ("on_default_sink_port_notify - moved to port %s - which SHOULD correspond to output %s",
port,
gvc_mixer_ui_device_get_description (output));
g_signal_emit (G_OBJECT (control),
signals[ACTIVE_OUTPUT_UPDATE],
0,
gvc_mixer_ui_device_get_id (output));
}
g_free (port);
}
static void
_set_default_sink (GvcMixerControl *control,
GvcMixerStream *stream)
{
guint new_id;
if (stream == NULL) {
/* Don't tell front-ends about an unset default
* sink if it's already unset */
if (control->priv->default_sink_is_set == FALSE)
return;
control->priv->default_sink_id = 0;
control->priv->default_sink_is_set = FALSE;
g_signal_emit (control,
signals[DEFAULT_SINK_CHANGED],
0,
PA_INVALID_INDEX);
return;
}
new_id = gvc_mixer_stream_get_id (stream);
if (control->priv->default_sink_id != new_id) {
GvcMixerUIDevice *output;
if (control->priv->default_sink_is_set) {
g_signal_handlers_disconnect_by_func (gvc_mixer_control_get_default_sink (control),
on_default_sink_port_notify,
control);
}
control->priv->default_sink_id = new_id;
control->priv->default_sink_is_set = TRUE;
g_signal_emit (control,
signals[DEFAULT_SINK_CHANGED],
0,
new_id);
g_signal_connect (stream,
"notify::port",
G_CALLBACK (on_default_sink_port_notify),
control);
output = gvc_mixer_control_lookup_device_from_stream (control, stream);
g_debug ("active_sink change");
g_signal_emit (G_OBJECT (control),
signals[ACTIVE_OUTPUT_UPDATE],
0,
gvc_mixer_ui_device_get_id (output));
}
}
static gboolean
_stream_has_name (gpointer key,
GvcMixerStream *stream,
const char *name)
{
const char *t_name;
t_name = gvc_mixer_stream_get_name (stream);
if (t_name != NULL
&& name != NULL
&& strcmp (t_name, name) == 0) {
return TRUE;
}
return FALSE;
}
static GvcMixerStream *
find_stream_for_name (GvcMixerControl *control,
const char *name)
{
GvcMixerStream *stream;
stream = g_hash_table_find (control->priv->all_streams,
(GHRFunc)_stream_has_name,
(char *)name);
return stream;
}
static void
update_default_source_from_name (GvcMixerControl *control,
const char *name)
{
gboolean changed = FALSE;
if ((control->priv->default_source_name == NULL
&& name != NULL)
|| (control->priv->default_source_name != NULL
&& name == NULL)
|| (name != NULL && strcmp (control->priv->default_source_name, name) != 0)) {
changed = TRUE;
}
if (changed) {
GvcMixerStream *stream;
g_free (control->priv->default_source_name);
control->priv->default_source_name = g_strdup (name);
stream = find_stream_for_name (control, name);
_set_default_source (control, stream);
}
}
static void
update_default_sink_from_name (GvcMixerControl *control,
const char *name)
{
gboolean changed = FALSE;
if ((control->priv->default_sink_name == NULL
&& name != NULL)
|| (control->priv->default_sink_name != NULL
&& name == NULL)
|| (name != NULL && strcmp (control->priv->default_sink_name, name) != 0)) {
changed = TRUE;
}
if (changed) {
GvcMixerStream *stream;
g_free (control->priv->default_sink_name);
control->priv->default_sink_name = g_strdup (name);
stream = find_stream_for_name (control, name);
_set_default_sink (control, stream);
}
}
static void
update_server (GvcMixerControl *control,
const pa_server_info *info)
{
if (info->default_source_name != NULL) {
update_default_source_from_name (control, info->default_source_name);
}
if (info->default_sink_name != NULL) {
g_debug ("update server");
update_default_sink_from_name (control, info->default_sink_name);
}
}
static void
remove_stream (GvcMixerControl *control,
GvcMixerStream *stream)
{
guint id;
g_object_ref (stream);
id = gvc_mixer_stream_get_id (stream);
if (id == control->priv->default_sink_id) {
_set_default_sink (control, NULL);
} else if (id == control->priv->default_source_id) {
_set_default_source (control, NULL);
}
g_hash_table_remove (control->priv->all_streams,
GUINT_TO_POINTER (id));
g_signal_emit (G_OBJECT (control),
signals[STREAM_REMOVED],
0,
gvc_mixer_stream_get_id (stream));
g_object_unref (stream);
}
static void
add_stream (GvcMixerControl *control,
GvcMixerStream *stream)
{
g_hash_table_insert (control->priv->all_streams,
GUINT_TO_POINTER (gvc_mixer_stream_get_id (stream)),
stream);
g_signal_emit (G_OBJECT (control),
signals[STREAM_ADDED],
0,
gvc_mixer_stream_get_id (stream));
}
/* This method will match individual stream ports against its corresponding device
* It does this by:
* - iterates through our devices and finds the one where the card-id on the device is the same as the card-id on the stream
* and the port-name on the device is the same as the streamport-name.
* This should always find a match and is used exclusively by sync_devices().
*/
static gboolean
match_stream_with_devices (GvcMixerControl *control,
GvcMixerStreamPort *stream_port,
GvcMixerStream *stream)
{
GList *devices, *d;
guint stream_card_id;
guint stream_id;
gboolean in_possession = FALSE;
stream_id = gvc_mixer_stream_get_id (stream);
stream_card_id = gvc_mixer_stream_get_card_index (stream);
devices = g_hash_table_get_values (GVC_IS_MIXER_SOURCE (stream) ? control->priv->ui_inputs : control->priv->ui_outputs);
for (d = devices; d != NULL; d = d->next) {
GvcMixerUIDevice *device;
guint device_stream_id;
gchar *device_port_name;
gchar *origin;
gchar *description;
GvcMixerCard *card;
guint card_id;
device = d->data;
g_object_get (G_OBJECT (device),
"stream-id", &device_stream_id,
"card", &card,
"origin", &origin,
"description", &description,
"port-name", &device_port_name,
NULL);
if (card == NULL) {
if (device_stream_id == stream_id) {
g_debug ("Matched stream %u with card-less device '%s', with stream already setup",
stream_id, description);
in_possession = TRUE;
}
} else {
card_id = gvc_mixer_card_get_index (card);
g_debug ("Attempt to match_stream update_with_existing_outputs - Try description : '%s', origin : '%s', device port name : '%s', card : %p, AGAINST stream port: '%s', sink card id %i",
description,
origin,
device_port_name,
card,
stream_port->port,
stream_card_id);
if (stream_card_id == card_id &&
g_strcmp0 (device_port_name, stream_port->port) == 0) {
g_debug ("Match device with stream: We have a match with description: '%s', origin: '%s', cached already with device id %u, so set stream id to %i",
description,
origin,
gvc_mixer_ui_device_get_id (device),
stream_id);
g_object_set (G_OBJECT (device),
"stream-id", stream_id,
NULL);
in_possession = TRUE;
}
}
g_free (device_port_name);
g_free (origin);
g_free (description);
if (in_possession == TRUE)
break;
}
g_list_free (devices);
return in_possession;
}
/*
* This method attempts to match a sink or source with its relevant UI device.
* GvcMixerStream can represent both a sink or source.
* Using static card port introspection implies that we know beforehand what
* outputs and inputs are available to the user.
* But that does not mean that all of these inputs and outputs are available to be used.
* For instance we might be able to see that there is a HDMI port available but if
* we are on the default analog stereo output profile there is no valid sink for
* that HDMI device. We first need to change profile and when update_sink() is called
* only then can we match the new hdmi sink with its corresponding device.
*
* Firstly it checks to see if the incoming stream has no ports.
* - If a stream has no ports but has a valid card ID (bluetooth), it will attempt
* to match the device with the stream using the card id.
* - If a stream has no ports and no valid card id, it goes ahead and makes a new
* device (software/network devices are only detectable at the sink/source level)
* If the stream has ports it will match each port against the stream using match_stream_with_devices().
*
* This method should always find a match.
*/
static void
sync_devices (GvcMixerControl *control,
GvcMixerStream* stream)
{
/* Go through ports to see what outputs can be created. */
const GList *stream_ports;
const GList *n = NULL;
gboolean is_output = !GVC_IS_MIXER_SOURCE (stream);
stream_ports = gvc_mixer_stream_get_ports (stream);
if (stream_ports == NULL) {
GvcMixerUIDevice *device;
/* Bluetooth, no ports but a valid card */
if (gvc_mixer_stream_get_card_index (stream) != PA_INVALID_INDEX) {
GList *devices, *d;
gboolean in_possession = FALSE;
devices = g_hash_table_get_values (is_output ? control->priv->ui_outputs : control->priv->ui_inputs);
for (d = devices; d != NULL; d = d->next) {
GvcMixerCard *card;
guint card_id;
device = d->data;
g_object_get (G_OBJECT (device),
"card", &card,
NULL);
card_id = gvc_mixer_card_get_index (card);
g_debug ("sync devices, device description - '%s', device card id - %i, stream description - %s, stream card id - %i",
gvc_mixer_ui_device_get_description (device),
card_id,
gvc_mixer_stream_get_description (stream),
gvc_mixer_stream_get_card_index (stream));
if (card_id == gvc_mixer_stream_get_card_index (stream)) {
in_possession = TRUE;
break;
}
}
g_list_free (devices);
if (!in_possession) {
g_warning ("Couldn't match the portless stream (with card) - '%s' is it an input ? -> %i, streams card id -> %i",
gvc_mixer_stream_get_description (stream),
GVC_IS_MIXER_SOURCE (stream),
gvc_mixer_stream_get_card_index (stream));
return;
}
g_object_set (G_OBJECT (device),
"stream-id", gvc_mixer_stream_get_id (stream),
"description", gvc_mixer_stream_get_description (stream),
"origin", "", /*Leave it empty for these special cases*/
"port-name", NULL,
"port-available", TRUE,
NULL);
} else { /* Network sink/source has no ports and no card. */
GObject *object;
object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE,
"stream-id", gvc_mixer_stream_get_id (stream),
"description", gvc_mixer_stream_get_description (stream),
"origin", "", /* Leave it empty for these special cases */
"port-name", NULL,
"port-available", TRUE,
"icon-name", gvc_mixer_stream_get_icon_name (stream),
NULL);
device = GVC_MIXER_UI_DEVICE (object);
g_hash_table_insert (is_output ? control->priv->ui_outputs : control->priv->ui_inputs,
GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (device)),
g_object_ref (device));
}
g_signal_emit (G_OBJECT (control),
signals[is_output ? OUTPUT_ADDED : INPUT_ADDED],
0,
gvc_mixer_ui_device_get_id (device));
return;
}
/* Go ahead and make sure to match each port against a previously created device */
for (n = stream_ports; n != NULL; n = n->next) {
GvcMixerStreamPort *stream_port;
stream_port = n->data;
if (match_stream_with_devices (control, stream_port, stream))
continue;
g_warning ("Sync_devices: Failed to match stream id: %u, description: '%s', origin: '%s'",
gvc_mixer_stream_get_id (stream),
stream_port->human_port,
gvc_mixer_stream_get_description (stream));
}
}
static void
set_icon_name_from_proplist (GvcMixerStream *stream,
pa_proplist *l,
const char *default_icon_name)
{
const char *t;
if ((t = pa_proplist_gets (l, PA_PROP_DEVICE_ICON_NAME))) {
goto finish;
}
if ((t = pa_proplist_gets (l, PA_PROP_MEDIA_ICON_NAME))) {
goto finish;
}
if ((t = pa_proplist_gets (l, PA_PROP_WINDOW_ICON_NAME))) {
goto finish;
}
if ((t = pa_proplist_gets (l, PA_PROP_APPLICATION_ICON_NAME))) {
goto finish;
}
if ((t = pa_proplist_gets (l, PA_PROP_MEDIA_ROLE))) {
if (strcmp (t, "video") == 0 ||
strcmp (t, "phone") == 0) {
goto finish;
}
if (strcmp (t, "music") == 0) {
t = "audio";
goto finish;
}
if (strcmp (t, "game") == 0) {
t = "applications-games";
goto finish;
}
if (strcmp (t, "event") == 0) {
t = "dialog-information";
goto finish;
}
}
t = default_icon_name;
finish:
gvc_mixer_stream_set_icon_name (stream, t);
}
static GvcMixerStreamState
translate_pa_state (pa_sink_state_t state) {
switch (state) {
case PA_SINK_RUNNING:
return GVC_STREAM_STATE_RUNNING;
case PA_SINK_IDLE:
return GVC_STREAM_STATE_IDLE;
case PA_SINK_SUSPENDED:
return GVC_STREAM_STATE_SUSPENDED;
case PA_SINK_INIT:
case PA_SINK_INVALID_STATE:
case PA_SINK_UNLINKED:
default:
return GVC_STREAM_STATE_INVALID;
}
}
/*
* Called when anything changes with a sink.
*/
static void
update_sink (GvcMixerControl *control,
const pa_sink_info *info)
{
GvcMixerStream *stream;
gboolean is_new;
pa_volume_t max_volume;
GvcChannelMap *map;
char map_buff[PA_CHANNEL_MAP_SNPRINT_MAX];
pa_channel_map_snprint (map_buff, PA_CHANNEL_MAP_SNPRINT_MAX, &info->channel_map);
#if 1
g_debug ("Updating sink: index=%u name='%s' description='%s' map='%s'",
info->index,
info->name,
info->description,
map_buff);
#endif
map = NULL;
is_new = FALSE;
stream = g_hash_table_lookup (control->priv->sinks,
GUINT_TO_POINTER (info->index));
if (stream == NULL) {
GList *list = NULL;
guint i;
map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map);
stream = gvc_mixer_sink_new (control->priv->pa_context,
info->index,
map);
for (i = 0; i < info->n_ports; i++) {
GvcMixerStreamPort *port;
port = g_slice_new0 (GvcMixerStreamPort);
port->port = g_strdup (info->ports[i]->name);
port->human_port = g_strdup (info->ports[i]->description);
port->priority = info->ports[i]->priority;
port->available = info->ports[i]->available != PA_PORT_AVAILABLE_NO;
list = g_list_prepend (list, port);
}
gvc_mixer_stream_set_ports (stream, list);
g_object_unref (map);
is_new = TRUE;
} else if (gvc_mixer_stream_is_running (stream)) {
/* Ignore events if volume changes are outstanding */
g_debug ("Ignoring event, volume changes are outstanding");
return;
}
max_volume = pa_cvolume_max (&info->volume);
gvc_mixer_stream_set_name (stream, info->name);
gvc_mixer_stream_set_card_index (stream, info->card);
gvc_mixer_stream_set_description (stream, info->description);
set_icon_name_from_proplist (stream, info->proplist, "audio-card");
gvc_mixer_stream_set_form_factor (stream, pa_proplist_gets (info->proplist, PA_PROP_DEVICE_FORM_FACTOR));
gvc_mixer_stream_set_sysfs_path (stream, pa_proplist_gets (info->proplist, "sysfs.path"));
gvc_mixer_stream_set_volume (stream, (guint)max_volume);
gvc_mixer_stream_set_is_muted (stream, info->mute);
gvc_mixer_stream_set_can_decibel (stream, !!(info->flags & PA_SINK_DECIBEL_VOLUME));
gvc_mixer_stream_set_base_volume (stream, (guint32) info->base_volume);
gvc_mixer_stream_set_state (stream, translate_pa_state (info->state));
/* Messy I know but to set the port everytime regardless of whether it has changed will cost us a
* port change notify signal which causes the frontend to resync.
* Only update the UI when something has changed. */
if (info->active_port != NULL) {
if (is_new)
gvc_mixer_stream_set_port (stream, info->active_port->name);
else {
const GvcMixerStreamPort *active_port;
active_port = gvc_mixer_stream_get_port (stream);
if (active_port == NULL ||
g_strcmp0 (active_port->port, info->active_port->name) != 0) {
g_debug ("update sink - apparently a port update");
gvc_mixer_stream_set_port (stream, info->active_port->name);
}
}
}
if (is_new) {
g_debug ("update sink - is new");
g_hash_table_insert (control->priv->sinks,
GUINT_TO_POINTER (info->index),
g_object_ref (stream));
add_stream (control, stream);
/* Always sink on a new stream to able to assign the right stream id
* to the appropriate outputs (multiple potential outputs per stream). */
sync_devices (control, stream);
} else {
g_signal_emit (G_OBJECT (control),
signals[STREAM_CHANGED],
0,
gvc_mixer_stream_get_id (stream));
}
/*
* When we change profile on a device that is not the server default sink,
* it will jump back to the default sink set by the server to prevent the audio setup from being 'outputless'.
* All well and good but then when we get the new stream created for the new profile how do we know
* that this is the intended default or selected device the user wishes to use.
* This is messy but it's the only reliable way that it can be done without ripping the whole thing apart.
*/
if (control->priv->profile_swapping_device_id != GVC_MIXER_UI_DEVICE_INVALID) {
GvcMixerUIDevice *dev = NULL;
dev = gvc_mixer_control_lookup_output_id (control, control->priv->profile_swapping_device_id);
if (dev != NULL) {
/* now check to make sure this new stream is the same stream just matched and set on the device object */
if (gvc_mixer_ui_device_get_stream_id (dev) == gvc_mixer_stream_get_id (stream)) {
g_debug ("Looks like we profile swapped on a non server default sink");
gvc_mixer_control_set_default_sink (control, stream);
control->priv->profile_swapping_device_id = GVC_MIXER_UI_DEVICE_INVALID;
}
}
}
if (control->priv->default_sink_name != NULL
&& info->name != NULL
&& strcmp (control->priv->default_sink_name, info->name) == 0) {
_set_default_sink (control, stream);
}
if (map == NULL)
map = (GvcChannelMap *) gvc_mixer_stream_get_channel_map (stream);
gvc_channel_map_volume_changed (map, &info->volume, FALSE);
}
static void
update_source (GvcMixerControl *control,
const pa_source_info *info)
{
GvcMixerStream *stream;
gboolean is_new;
pa_volume_t max_volume;
#if 1
g_debug ("Updating source: index=%u name='%s' description='%s'",
info->index,
info->name,
info->description);
#endif
/* completely ignore monitors, they're not real sources */
if (info->monitor_of_sink != PA_INVALID_INDEX) {
return;
}
is_new = FALSE;
stream = g_hash_table_lookup (control->priv->sources,
GUINT_TO_POINTER (info->index));
if (stream == NULL) {
GList *list = NULL;
guint i;
GvcChannelMap *map;
map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map);
stream = gvc_mixer_source_new (control->priv->pa_context,
info->index,
map);
for (i = 0; i < info->n_ports; i++) {
GvcMixerStreamPort *port;
port = g_slice_new0 (GvcMixerStreamPort);
port->port = g_strdup (info->ports[i]->name);
port->human_port = g_strdup (info->ports[i]->description);
port->priority = info->ports[i]->priority;
list = g_list_prepend (list, port);
}
gvc_mixer_stream_set_ports (stream, list);
g_object_unref (map);
is_new = TRUE;
} else if (gvc_mixer_stream_is_running (stream)) {
/* Ignore events if volume changes are outstanding */
g_debug ("Ignoring event, volume changes are outstanding");
return;
}
max_volume = pa_cvolume_max (&info->volume);
gvc_mixer_stream_set_name (stream, info->name);
gvc_mixer_stream_set_card_index (stream, info->card);
gvc_mixer_stream_set_description (stream, info->description);
set_icon_name_from_proplist (stream, info->proplist, "audio-input-microphone");
gvc_mixer_stream_set_form_factor (stream, pa_proplist_gets (info->proplist, PA_PROP_DEVICE_FORM_FACTOR));
gvc_mixer_stream_set_volume (stream, (guint)max_volume);
gvc_mixer_stream_set_is_muted (stream, info->mute);
gvc_mixer_stream_set_can_decibel (stream, !!(info->flags & PA_SOURCE_DECIBEL_VOLUME));
gvc_mixer_stream_set_base_volume (stream, (guint32) info->base_volume);
g_debug ("update source");
if (info->active_port != NULL) {
if (is_new)
gvc_mixer_stream_set_port (stream, info->active_port->name);
else {
const GvcMixerStreamPort *active_port;
active_port = gvc_mixer_stream_get_port (stream);
if (active_port == NULL ||
g_strcmp0 (active_port->port, info->active_port->name) != 0) {
g_debug ("update source - apparently a port update");
gvc_mixer_stream_set_port (stream, info->active_port->name);
}
}
}
if (is_new) {
g_hash_table_insert (control->priv->sources,
GUINT_TO_POINTER (info->index),
g_object_ref (stream));
add_stream (control, stream);
sync_devices (control, stream);
} else {
g_signal_emit (G_OBJECT (control),
signals[STREAM_CHANGED],
0,
gvc_mixer_stream_get_id (stream));
}
if (control->priv->profile_swapping_device_id != GVC_MIXER_UI_DEVICE_INVALID) {
GvcMixerUIDevice *dev = NULL;
dev = gvc_mixer_control_lookup_input_id (control, control->priv->profile_swapping_device_id);
if (dev != NULL) {
/* now check to make sure this new stream is the same stream just matched and set on the device object */
if (gvc_mixer_ui_device_get_stream_id (dev) == gvc_mixer_stream_get_id (stream)) {
g_debug ("Looks like we profile swapped on a non server default source");
gvc_mixer_control_set_default_source (control, stream);
control->priv->profile_swapping_device_id = GVC_MIXER_UI_DEVICE_INVALID;
}
}
}
if (control->priv->default_source_name != NULL
&& info->name != NULL
&& strcmp (control->priv->default_source_name, info->name) == 0) {
_set_default_source (control, stream);
}
}
static void
set_is_event_stream_from_proplist (GvcMixerStream *stream,
pa_proplist *l)
{
const char *t;
gboolean is_event_stream;
is_event_stream = FALSE;
if ((t = pa_proplist_gets (l, PA_PROP_MEDIA_ROLE))) {
if (g_str_equal (t, "event"))
is_event_stream = TRUE;
}
gvc_mixer_stream_set_is_event_stream (stream, is_event_stream);
}
static void
set_application_id_from_proplist (GvcMixerStream *stream,
pa_proplist *l)
{
const char *t;
if ((t = pa_proplist_gets (l, PA_PROP_APPLICATION_ID))) {
gvc_mixer_stream_set_application_id (stream, t);
}
}
static void
update_sink_input (GvcMixerControl *control,
const pa_sink_input_info *info)
{
GvcMixerStream *stream;
gboolean is_new;
pa_volume_t max_volume;
const char *name;
#if 0
g_debug ("Updating sink input: index=%u name='%s' client=%u sink=%u",
info->index,
info->name,
info->client,
info->sink);
#endif
is_new = FALSE;
stream = g_hash_table_lookup (control->priv->sink_inputs,
GUINT_TO_POINTER (info->index));
if (stream == NULL) {
GvcChannelMap *map;
map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map);
stream = gvc_mixer_sink_input_new (control->priv->pa_context,
info->index,
map);
g_object_unref (map);
is_new = TRUE;
} else if (gvc_mixer_stream_is_running (stream)) {
/* Ignore events if volume changes are outstanding */
g_debug ("Ignoring event, volume changes are outstanding");
return;
}
max_volume = pa_cvolume_max (&info->volume);
name = (const char *)g_hash_table_lookup (control->priv->clients,
GUINT_TO_POINTER (info->client));
gvc_mixer_stream_set_name (stream, name);
gvc_mixer_stream_set_description (stream, info->name);
set_application_id_from_proplist (stream, info->proplist);
set_is_event_stream_from_proplist (stream, info->proplist);
set_icon_name_from_proplist (stream, info->proplist, "application-x-executable");
gvc_mixer_stream_set_volume (stream, (guint)max_volume);
gvc_mixer_stream_set_is_muted (stream, info->mute);
gvc_mixer_stream_set_is_virtual (stream, info->client == PA_INVALID_INDEX);
if (is_new) {
g_hash_table_insert (control->priv->sink_inputs,
GUINT_TO_POINTER (info->index),
g_object_ref (stream));
add_stream (control, stream);
} else {
g_signal_emit (G_OBJECT (control),
signals[STREAM_CHANGED],
0,
gvc_mixer_stream_get_id (stream));
}
}
static void
update_source_output (GvcMixerControl *control,
const pa_source_output_info *info)
{
GvcMixerStream *stream;
gboolean is_new;
pa_volume_t max_volume;
const char *name;
#if 1
g_debug ("Updating source output: index=%u name='%s' client=%u source=%u",
info->index,
info->name,
info->client,
info->source);
#endif
is_new = FALSE;
stream = g_hash_table_lookup (control->priv->source_outputs,
GUINT_TO_POINTER (info->index));
if (stream == NULL) {
GvcChannelMap *map;
map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map);
stream = gvc_mixer_source_output_new (control->priv->pa_context,
info->index,
map);
g_object_unref (map);
is_new = TRUE;
}
name = (const char *)g_hash_table_lookup (control->priv->clients,
GUINT_TO_POINTER (info->client));
max_volume = pa_cvolume_max (&info->volume);
gvc_mixer_stream_set_name (stream, name);
gvc_mixer_stream_set_description (stream, info->name);
set_application_id_from_proplist (stream, info->proplist);
set_is_event_stream_from_proplist (stream, info->proplist);
gvc_mixer_stream_set_volume (stream, (guint)max_volume);
gvc_mixer_stream_set_is_muted (stream, info->mute);
set_icon_name_from_proplist (stream, info->proplist, "audio-input-microphone");
if (is_new) {
g_hash_table_insert (control->priv->source_outputs,
GUINT_TO_POINTER (info->index),
g_object_ref (stream));
add_stream (control, stream);
} else {
g_signal_emit (G_OBJECT (control),
signals[STREAM_CHANGED],
0,
gvc_mixer_stream_get_id (stream));
}
}
static void
update_client (GvcMixerControl *control,
const pa_client_info *info)
{
#if 1
g_debug ("Updating client: index=%u name='%s'",
info->index,
info->name);
#endif
g_hash_table_insert (control->priv->clients,
GUINT_TO_POINTER (info->index),
g_strdup (info->name));
}
static char *
card_num_streams_to_status (guint sinks,
guint sources)
{
char *sinks_str;
char *sources_str;
char *ret;
if (sinks == 0 && sources == 0) {
/* translators:
* The device has been disabled */
return g_strdup (_("Disabled"));
}
if (sinks == 0) {
sinks_str = NULL;
} else {
/* translators:
* The number of sound outputs on a particular device */
sinks_str = g_strdup_printf (ngettext ("%u Output",
"%u Outputs",
sinks),
sinks);
}
if (sources == 0) {
sources_str = NULL;
} else {
/* translators:
* The number of sound inputs on a particular device */
sources_str = g_strdup_printf (ngettext ("%u Input",
"%u Inputs",
sources),
sources);
}
if (sources_str == NULL)
return sinks_str;
if (sinks_str == NULL)
return sources_str;
ret = g_strdup_printf ("%s / %s", sinks_str, sources_str);
g_free (sinks_str);
g_free (sources_str);
return ret;
}
/*
* A utility method to gather which card profiles are relevant to the port .
*/
static GList *
determine_profiles_for_port (pa_card_port_info *port,
GList* card_profiles)
{
guint i;
GList *supported_profiles = NULL;
GList *p;
for (i = 0; i < port->n_profiles; i++) {
for (p = card_profiles; p != NULL; p = p->next) {
GvcMixerCardProfile *prof;
prof = p->data;
if (g_strcmp0 (port->profiles[i]->name, prof->profile) == 0)
supported_profiles = g_list_append (supported_profiles, prof);
}
}
g_debug ("%i profiles supported on port %s",
g_list_length (supported_profiles),
port->description);
return g_list_sort (supported_profiles, (GCompareFunc) gvc_mixer_card_profile_compare);
}
static gboolean
is_card_port_an_output (GvcMixerCardPort* port)
{
return port->direction == PA_DIRECTION_OUTPUT ? TRUE : FALSE;
}
/*
* This method will create a ui device for the given port.
*/
static void
create_ui_device_from_port (GvcMixerControl* control,
GvcMixerCardPort* port,
GvcMixerCard* card)
{
GvcMixerUIDeviceDirection direction;
GObject *object;
GvcMixerUIDevice *uidevice;
gboolean available = port->available != PA_PORT_AVAILABLE_NO;
direction = (is_card_port_an_output (port) == TRUE) ? UIDeviceOutput : UIDeviceInput;
object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE,
"type", (guint)direction,
"card", card,
"port-name", port->port,
"description", port->human_port,
"origin", gvc_mixer_card_get_name (card),
"port-available", available,
"icon-name", port->icon_name,
NULL);
uidevice = GVC_MIXER_UI_DEVICE (object);
gvc_mixer_ui_device_set_profiles (uidevice, port->profiles);
g_hash_table_insert (is_card_port_an_output (port) ? control->priv->ui_outputs : control->priv->ui_inputs,
GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (uidevice)),
uidevice);
if (available) {
g_signal_emit (G_OBJECT (control),
signals[is_card_port_an_output (port) ? OUTPUT_ADDED : INPUT_ADDED],
0,
gvc_mixer_ui_device_get_id (uidevice));
}
g_debug ("create_ui_device_from_port, direction %u, description '%s', origin '%s', port available %i",
direction,
port->human_port,
gvc_mixer_card_get_name (card),
available);
}
/*
* This method will match up GvcMixerCardPorts with existing devices.
* A match is achieved if the device's card-id and the port's card-id are the same
* && the device's port-name and the card-port's port member are the same.
* A signal is then sent adding or removing that device from the UI depending on the availability of the port.
*/
static void
match_card_port_with_existing_device (GvcMixerControl *control,
GvcMixerCardPort *card_port,
GvcMixerCard *card,
gboolean available)
{
GList *d;
GList *devices;
GvcMixerUIDevice *device;
gboolean is_output = is_card_port_an_output (card_port);
devices = g_hash_table_get_values (is_output ? control->priv->ui_outputs : control->priv->ui_inputs);
for (d = devices; d != NULL; d = d->next) {
GvcMixerCard *device_card;
gchar *device_port_name;
device = d->data;
g_object_get (G_OBJECT (device),
"card", &device_card,
"port-name", &device_port_name,
NULL);
if (g_strcmp0 (card_port->port, device_port_name) == 0 &&
device_card == card) {
g_debug ("Found the relevant device %s, update its port availability flag to %i, is_output %i",
device_port_name,
available,
is_output);
g_object_set (G_OBJECT (device),
"port-available", available, NULL);
g_signal_emit (G_OBJECT (control),
is_output ? signals[available ? OUTPUT_ADDED : OUTPUT_REMOVED] : signals[available ? INPUT_ADDED : INPUT_REMOVED],
0,
gvc_mixer_ui_device_get_id (device));
}
g_free (device_port_name);
}
g_list_free (devices);
}
static void
create_ui_device_from_card (GvcMixerControl *control,
GvcMixerCard *card)
{
GObject *object;
GvcMixerUIDevice *in;
GvcMixerUIDevice *out;
const GList *profiles;
/* For now just create two devices and presume this device is multi directional
* Ensure to remove both on card removal (available to false by default) */
profiles = gvc_mixer_card_get_profiles (card);
g_debug ("Portless card just registered - %i", gvc_mixer_card_get_index (card));
object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE,
"type", UIDeviceInput,
"description", gvc_mixer_card_get_name (card),
"origin", "", /* Leave it empty for these special cases */
"port-name", NULL,
"port-available", FALSE,
"card", card,
NULL);
in = GVC_MIXER_UI_DEVICE (object);
gvc_mixer_ui_device_set_profiles (in, profiles);
g_hash_table_insert (control->priv->ui_inputs,
GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (in)),
g_object_ref (in));
object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE,
"type", UIDeviceOutput,
"description", gvc_mixer_card_get_name (card),
"origin", "", /* Leave it empty for these special cases */
"port-name", NULL,
"port-available", FALSE,
"card", card,
NULL);
out = GVC_MIXER_UI_DEVICE (object);
gvc_mixer_ui_device_set_profiles (out, profiles);
g_hash_table_insert (control->priv->ui_outputs,
GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (out)),
g_object_ref (out));
}
#ifdef HAVE_ALSA
typedef struct {
char *port_name_to_set;
guint32 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;
guint j;
const char *s;
if (eol != 0) {
port_status_data_free (data);
return;
}
if (i->card != data->headset_card)
return;
s = data->port_name_to_set;
if (i->active_port &&
strcmp (i->active_port->name, s) == 0)
return;
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);
}
static void
source_info_cb (pa_context *c,
const pa_source_info *i,
int eol,
void *userdata)
{
PortStatusData *data = userdata;
pa_operation *o;
guint j;
const char *s;
if (eol != 0) {
port_status_data_free (data);
return;
}
if (i->card != data->headset_card)
return;
s = data->port_name_to_set;
for (j = 0; j < i->n_ports; j++) {
if (g_str_equal (i->ports[j]->name, s)) {
o = pa_context_set_default_source (c,
i->name,
NULL,
NULL);
if (o == NULL) {
g_warning ("pa_context_set_default_source() failed");
return;
}
}
}
if (i->active_port && strcmp (i->active_port->name, s) == 0)
return;
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);
}
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;
if (port_name == NULL)
return;
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 */
static void
free_priv_port_names (GvcMixerControl *control)
{
#ifdef HAVE_ALSA
g_clear_pointer (&control->priv->headphones_name, g_free);
g_clear_pointer (&control->priv->headsetmic_name, g_free);
g_clear_pointer (&control->priv->headphonemic_name, g_free);
g_clear_pointer (&control->priv->internalspk_name, g_free);
g_clear_pointer (&control->priv->internalmic_name, g_free);
#endif
}
void
gvc_mixer_control_set_headset_port (GvcMixerControl *control,
guint id,
GvcHeadsetPortChoice choice)
{
g_return_if_fail (GVC_IS_MIXER_CONTROL (control));
#ifdef HAVE_ALSA
switch (choice) {
case GVC_HEADSET_PORT_CHOICE_HEADPHONES:
gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headphones_name, TRUE);
gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->internalmic_name, FALSE);
break;
case GVC_HEADSET_PORT_CHOICE_HEADSET:
gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headphones_name, TRUE);
gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headsetmic_name, FALSE);
break;
case GVC_HEADSET_PORT_CHOICE_MIC:
gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->internalspk_name, TRUE);
gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headphonemic_name, FALSE);
break;
case GVC_HEADSET_PORT_CHOICE_NONE:
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;
const pa_card_port_info *internalmic;
const pa_card_port_info *internalspk;
} headset_ports;
/*
In PulseAudio without ucm, 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.
From the PA_PROCOTOL_VERSION=34, The device_port structure adds 2 members
availability_group and type, with the help of these 2 members, we could
consolidate the port checking and port setting for non-ucm and with-ucm
cases.
*/
#define HEADSET_PORT_SET(dst, src) \
do { \
if (!(dst) || (dst)->priority < (src)->priority) \
dst = src; \
} while (0)
#define GET_PORT_NAME(x) (x ? g_strdup (x->name) : NULL)
static headset_ports *
get_headset_ports (GvcMixerControl *control,
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 (control->priv->server_protocol_version < 34) {
if (g_str_equal (p->name, "analog-output-headphones"))
h->headphones = p;
else if (g_str_equal (p->name, "analog-input-headset-mic"))
h->headsetmic = p;
else if (g_str_equal (p->name, "analog-input-headphone-mic"))
h->headphonemic = p;
else if (g_str_equal (p->name, "analog-input-internal-mic"))
h->internalmic = p;
else if (g_str_equal (p->name, "analog-output-speaker"))
h->internalspk = p;
} else {
#if (PA_PROTOCOL_VERSION >= 34)
/* in the first loop, set only headphones */
/* the microphone ports are assigned in the second loop */
if (p->type == PA_DEVICE_PORT_TYPE_HEADPHONES) {
if (p->availability_group)
HEADSET_PORT_SET (h->headphones, p);
} else if (p->type == PA_DEVICE_PORT_TYPE_SPEAKER) {
HEADSET_PORT_SET (h->internalspk, p);
} else if (p->type == PA_DEVICE_PORT_TYPE_MIC) {
if (!p->availability_group)
HEADSET_PORT_SET (h->internalmic, p);
}
#else
g_warning_once ("libgnome-volume-control running against PulseAudio %u, "
"but compiled against older %d, report a bug to your distribution",
control->priv->server_protocol_version,
PA_PROTOCOL_VERSION);
#endif
}
}
#if (PA_PROTOCOL_VERSION >= 34)
if (h->headphones && (control->priv->server_protocol_version >= 34)) {
for (i = 0; i < c->n_ports; i++) {
pa_card_port_info *p = c->ports[i];
if (g_strcmp0(h->headphones->availability_group, p->availability_group))
continue;
if (p->direction != PA_DIRECTION_INPUT)
continue;
if (p->type == PA_DEVICE_PORT_TYPE_HEADSET)
HEADSET_PORT_SET (h->headsetmic, p);
else if (p->type == PA_DEVICE_PORT_TYPE_MIC)
HEADSET_PORT_SET (h->headphonemic, p);
}
}
#endif
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 (control, 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 = TRUE;
gboolean hpmic = TRUE;
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 (control->priv->server_protocol_version < 34) {
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;
free_priv_port_names (control);
control->priv->headphones_name = GET_PORT_NAME(h->headphones);
control->priv->headsetmic_name = GET_PORT_NAME(h->headsetmic);
control->priv->headphonemic_name = GET_PORT_NAME(h->headphonemic);
control->priv->internalspk_name = GET_PORT_NAME(h->internalspk);
control->priv->internalmic_name = GET_PORT_NAME(h->internalmic);
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:
*
* - gvc_mixer_card and gvc_mixer_card_ports are created and relevant setters are called.
* - First it checks to see if it's a portless card. Bluetooth devices are portless AFAIHS.
* If so it creates two devices, an input and an output.
* - If it's a 'normal' card with ports it will create a new ui-device or
* synchronise port availability with the existing device cached for that port on this card. */
static void
update_card (GvcMixerControl *control,
const pa_card_info *info)
{
const GList *card_ports = NULL;
const GList *m = NULL;
GvcMixerCard *card;
gboolean is_new = FALSE;
#if 1
guint i;
const char *key;
void *state;
g_debug ("Updating card %s (index: %u driver: %s):",
info->name, info->index, info->driver);
for (i = 0; i < info->n_profiles; i++) {
struct pa_card_profile_info pi = info->profiles[i];
gboolean is_default;
is_default = (g_strcmp0 (pi.name, info->active_profile->name) == 0);
g_debug ("\tProfile '%s': %d sources %d sinks%s",
pi.name, pi.n_sources, pi.n_sinks,
is_default ? " (Current)" : "");
}
state = NULL;
key = pa_proplist_iterate (info->proplist, &state);
while (key != NULL) {
g_debug ("\tProperty: '%s' = '%s'",
key, pa_proplist_gets (info->proplist, key));
key = pa_proplist_iterate (info->proplist, &state);
}
#endif
card = g_hash_table_lookup (control->priv->cards,
GUINT_TO_POINTER (info->index));
if (card == NULL) {
GList *profile_list = NULL;
GList *port_list = NULL;
for (i = 0; i < info->n_profiles; i++) {
GvcMixerCardProfile *profile;
struct pa_card_profile_info pi = info->profiles[i];
profile = g_new0 (GvcMixerCardProfile, 1);
profile->profile = g_strdup (pi.name);
profile->human_profile = g_strdup (pi.description);
profile->status = card_num_streams_to_status (pi.n_sinks, pi.n_sources);
profile->n_sinks = pi.n_sinks;
profile->n_sources = pi.n_sources;
profile->priority = pi.priority;
profile_list = g_list_prepend (profile_list, profile);
}
card = gvc_mixer_card_new (control->priv->pa_context,
info->index);
for (i = 0; i < info->n_ports; i++) {
GvcMixerCardPort *port;
port = g_new0 (GvcMixerCardPort, 1);
port->port = g_strdup (info->ports[i]->name);
port->human_port = g_strdup (info->ports[i]->description);
port->priority = info->ports[i]->priority;
port->available = info->ports[i]->available;
port->direction = info->ports[i]->direction;
port->icon_name = g_strdup (pa_proplist_gets (info->ports[i]->proplist, "device.icon_name"));
port->profiles = determine_profiles_for_port (info->ports[i], profile_list);
port_list = g_list_prepend (port_list, port);
}
gvc_mixer_card_set_profiles (card, profile_list);
gvc_mixer_card_set_ports (card, port_list);
is_new = TRUE;
}
gvc_mixer_card_set_name (card, pa_proplist_gets (info->proplist, "device.description"));
gvc_mixer_card_set_icon_name (card, pa_proplist_gets (info->proplist, "device.icon_name"));
gvc_mixer_card_set_profile (card, info->active_profile->name);
if (is_new) {
g_hash_table_insert (control->priv->cards,
GUINT_TO_POINTER (info->index),
card);
}
card_ports = gvc_mixer_card_get_ports (card);
if (card_ports == NULL && is_new) {
g_debug ("Portless card just registered - %s", gvc_mixer_card_get_name (card));
create_ui_device_from_card (control, card);
}
for (m = card_ports; m != NULL; m = m->next) {
GvcMixerCardPort *card_port;
card_port = m->data;
if (is_new)
create_ui_device_from_port (control, card_port, card);
else {
for (i = 0; i < info->n_ports; i++) {
if (g_strcmp0 (card_port->port, info->ports[i]->name) == 0) {
if ((card_port->available == PA_PORT_AVAILABLE_NO) != (info->ports[i]->available == PA_PORT_AVAILABLE_NO)) {
card_port->available = info->ports[i]->available;
g_debug ("sync port availability on card %i, card port name '%s', new available value %i",
gvc_mixer_card_get_index (card),
card_port->port,
card_port->available);
match_card_port_with_existing_device (control,
card_port,
card,
card_port->available != PA_PORT_AVAILABLE_NO);
}
}
}
}
}
#ifdef HAVE_ALSA
check_audio_device_selection_needed (control, info);
#endif /* HAVE_ALSA */
g_signal_emit (G_OBJECT (control),
signals[CARD_ADDED],
0,
info->index);
}
static void
_pa_context_get_sink_info_cb (pa_context *context,
const pa_sink_info *i,
int eol,
void *userdata)
{
GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
if (eol < 0) {
if (pa_context_errno (context) == PA_ERR_NOENTITY) {
return;
}
g_warning ("Sink callback failure");
return;
}
if (eol > 0) {
dec_outstanding (control);
return;
}
update_sink (control, i);
}
static void
_pa_context_get_source_info_cb (pa_context *context,
const pa_source_info *i,
int eol,
void *userdata)
{
GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
if (eol < 0) {
if (pa_context_errno (context) == PA_ERR_NOENTITY) {
return;
}
g_warning ("Source callback failure");
return;
}
if (eol > 0) {
dec_outstanding (control);
return;
}
update_source (control, i);
}
static void
_pa_context_get_sink_input_info_cb (pa_context *context,
const pa_sink_input_info *i,
int eol,
void *userdata)
{
GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
if (eol < 0) {
if (pa_context_errno (context) == PA_ERR_NOENTITY) {
return;
}
g_warning ("Sink input callback failure");
return;
}
if (eol > 0) {
dec_outstanding (control);
return;
}
update_sink_input (control, i);
}
static void
_pa_context_get_source_output_info_cb (pa_context *context,
const pa_source_output_info *i,
int eol,
void *userdata)
{
GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
if (eol < 0) {
if (pa_context_errno (context) == PA_ERR_NOENTITY) {
return;
}
g_warning ("Source output callback failure");
return;
}
if (eol > 0) {
dec_outstanding (control);
return;
}
update_source_output (control, i);
}
static void
_pa_context_get_client_info_cb (pa_context *context,
const pa_client_info *i,
int eol,
void *userdata)
{
GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
if (eol < 0) {
if (pa_context_errno (context) == PA_ERR_NOENTITY) {
return;
}
g_warning ("Client callback failure");
return;
}
if (eol > 0) {
dec_outstanding (control);
return;
}
update_client (control, i);
}
static void
_pa_context_get_card_info_by_index_cb (pa_context *context,
const pa_card_info *i,
int eol,
void *userdata)
{
GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
if (eol < 0) {
if (pa_context_errno (context) == PA_ERR_NOENTITY)
return;
g_warning ("Card callback failure");
return;
}
if (eol > 0) {
dec_outstanding (control);
return;
}
update_card (control, i);
}
static void
_pa_context_get_server_info_cb (pa_context *context,
const pa_server_info *i,
void *userdata)
{
GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
if (i == NULL) {
g_warning ("Server info callback failure");
return;
}
g_debug ("get server info");
update_server (control, i);
dec_outstanding (control);
}
static void
remove_event_role_stream (GvcMixerControl *control)
{
g_debug ("Removing event role");
}
static void
update_event_role_stream (GvcMixerControl *control,
const pa_ext_stream_restore_info *info)
{
GvcMixerStream *stream;
gboolean is_new;
pa_volume_t max_volume;
if (strcmp (info->name, "sink-input-by-media-role:event") != 0) {
return;
}
#if 0
g_debug ("Updating event role: name='%s' device='%s'",
info->name,
info->device);
#endif
is_new = FALSE;
if (!control->priv->event_sink_input_is_set) {
pa_channel_map pa_map;
GvcChannelMap *map;
pa_map.channels = 1;
pa_map.map[0] = PA_CHANNEL_POSITION_MONO;
map = gvc_channel_map_new_from_pa_channel_map (&pa_map);
stream = gvc_mixer_event_role_new (control->priv->pa_context,
info->device,
map);
control->priv->event_sink_input_id = gvc_mixer_stream_get_id (stream);
control->priv->event_sink_input_is_set = TRUE;
is_new = TRUE;
} else {
stream = g_hash_table_lookup (control->priv->all_streams,
GUINT_TO_POINTER (control->priv->event_sink_input_id));
}
mixer-control: set max_volume to PA_VOLUME_NORM if no valid volume Reproducers: 1. After a fresh install of Ubuntu 20.04, open a gnome-terminal and press the Tab key, the system will output a bell notification sound. Open gnome-control-center and check the sound page, the output volume of 'System Sounds' is at 0, but the notification sound is still output at max volume. 2. After a fresh install of Ubuntu 20.04, open gnome-control-center directly, change to the sound page, the output volume of 'System Sounds' is at its max level. Click any 'Alert Sound' button at the bottom of the page, the sound is output at max volume but the UI slider for the output volume of 'System Sounds' changes to 0. The notification sound can however still could be heard at max volume. After a fresh install of the system, there is no saved entry for "sink-input-by-media-role:event" in PulseAudio's module-stream-restore, so libgvc will create a pa_ext_stream_restore_info in the _pa_ext_stream_restore_read_cb(). When a notification sound stream is sent to PA, PA will create an entry with name of "sink-input-by-media-role:event" and save it to the database, but volume_valid is false until gnome calls pa_ext_stream_restore_write(). So if users open the gnome-control-center before pa_ext_stream_restore_write() is called, although there is an entry named "sink-input-by-media-role:event" in the module-stream-restore, the volume is not valid in that entry, and calling pa_cvolume_max (&info->volume) will returns 0. In this case, libgvc reports the max_volume is 0, but in PulseAudio, the stream/sink-input volume is max (pa_sink_input_new() of the sink-input.c). Check if info->volume.channels is 0, which would mean the volume is not valid in the entry, and set max_volume to PA_VOLUME_NORM like _pa_ext_stream_restore_read_cb() does in this case. Below is the PA log about the stream entry without a valid volume: E: [pulseaudio] module-stream-restore.c: name=sink-input-by-media-role:event E: [pulseaudio] module-stream-restore.c: device=(null) no E: [pulseaudio] module-stream-restore.c: channel_map=(invalid) E: [pulseaudio] module-stream-restore.c: volume=(invalid) no E: [pulseaudio] module-stream-restore.c: mute=no no
2021-08-23 02:06:20 -04:00
/* 0 channels here means there is no valid volume in
* pa_ext_stream_restore_info() and in the saved stream entry of
* module-stream-restore in PulseAudio. */
if (info->volume.channels == 0)
max_volume = PA_VOLUME_NORM;
else
max_volume = pa_cvolume_max (&info->volume);
gvc_mixer_stream_set_name (stream, _("System Sounds"));
gvc_mixer_stream_set_icon_name (stream, "audio-x-generic");
gvc_mixer_stream_set_volume (stream, (guint)max_volume);
gvc_mixer_stream_set_is_muted (stream, info->mute);
if (is_new) {
add_stream (control, stream);
}
}
static void
_pa_ext_stream_restore_read_cb (pa_context *context,
const pa_ext_stream_restore_info *i,
int eol,
void *userdata)
{
GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
if (eol < 0) {
g_debug ("Failed to initialized stream_restore extension: %s",
pa_strerror (pa_context_errno (context)));
remove_event_role_stream (control);
return;
}
if (eol > 0) {
dec_outstanding (control);
/* If we don't have an event stream to restore, then
* set one up with a default 100% volume */
if (!control->priv->event_sink_input_is_set) {
pa_ext_stream_restore_info info;
memset (&info, 0, sizeof(info));
info.name = "sink-input-by-media-role:event";
info.volume.channels = 1;
info.volume.values[0] = PA_VOLUME_NORM;
update_event_role_stream (control, &info);
}
return;
}
update_event_role_stream (control, i);
}
static void
_pa_ext_stream_restore_subscribe_cb (pa_context *context,
void *userdata)
{
GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
pa_operation *o;
o = pa_ext_stream_restore_read (context,
_pa_ext_stream_restore_read_cb,
control);
if (o == NULL) {
g_warning ("pa_ext_stream_restore_read() failed");
return;
}
pa_operation_unref (o);
}
static void
req_update_server_info (GvcMixerControl *control,
int index)
{
pa_operation *o;
o = pa_context_get_server_info (control->priv->pa_context,
_pa_context_get_server_info_cb,
control);
if (o == NULL) {
g_warning ("pa_context_get_server_info() failed");
return;
}
pa_operation_unref (o);
}
static void
req_update_client_info (GvcMixerControl *control,
int index)
{
pa_operation *o;
if (index < 0) {
o = pa_context_get_client_info_list (control->priv->pa_context,
_pa_context_get_client_info_cb,
control);
} else {
o = pa_context_get_client_info (control->priv->pa_context,
index,
_pa_context_get_client_info_cb,
control);
}
if (o == NULL) {
g_warning ("pa_context_client_info_list() failed");
return;
}
pa_operation_unref (o);
}
static void
req_update_card (GvcMixerControl *control,
int index)
{
pa_operation *o;
if (index < 0) {
o = pa_context_get_card_info_list (control->priv->pa_context,
_pa_context_get_card_info_by_index_cb,
control);
} else {
o = pa_context_get_card_info_by_index (control->priv->pa_context,
index,
_pa_context_get_card_info_by_index_cb,
control);
}
if (o == NULL) {
g_warning ("pa_context_get_card_info_by_index() failed");
return;
}
pa_operation_unref (o);
}
static void
req_update_sink_info (GvcMixerControl *control,
int index)
{
pa_operation *o;
if (index < 0) {
o = pa_context_get_sink_info_list (control->priv->pa_context,
_pa_context_get_sink_info_cb,
control);
} else {
o = pa_context_get_sink_info_by_index (control->priv->pa_context,
index,
_pa_context_get_sink_info_cb,
control);
}
if (o == NULL) {
g_warning ("pa_context_get_sink_info_list() failed");
return;
}
pa_operation_unref (o);
}
static void
req_update_source_info (GvcMixerControl *control,
int index)
{
pa_operation *o;
if (index < 0) {
o = pa_context_get_source_info_list (control->priv->pa_context,
_pa_context_get_source_info_cb,
control);
} else {
o = pa_context_get_source_info_by_index(control->priv->pa_context,
index,
_pa_context_get_source_info_cb,
control);
}
if (o == NULL) {
g_warning ("pa_context_get_source_info_list() failed");
return;
}
pa_operation_unref (o);
}
static void
req_update_sink_input_info (GvcMixerControl *control,
int index)
{
pa_operation *o;
if (index < 0) {
o = pa_context_get_sink_input_info_list (control->priv->pa_context,
_pa_context_get_sink_input_info_cb,
control);
} else {
o = pa_context_get_sink_input_info (control->priv->pa_context,
index,
_pa_context_get_sink_input_info_cb,
control);
}
if (o == NULL) {
g_warning ("pa_context_get_sink_input_info_list() failed");
return;
}
pa_operation_unref (o);
}
static void
req_update_source_output_info (GvcMixerControl *control,
int index)
{
pa_operation *o;
if (index < 0) {
o = pa_context_get_source_output_info_list (control->priv->pa_context,
_pa_context_get_source_output_info_cb,
control);
} else {
o = pa_context_get_source_output_info (control->priv->pa_context,
index,
_pa_context_get_source_output_info_cb,
control);
}
if (o == NULL) {
g_warning ("pa_context_get_source_output_info_list() failed");
return;
}
pa_operation_unref (o);
}
static void
remove_client (GvcMixerControl *control,
guint index)
{
g_hash_table_remove (control->priv->clients,
GUINT_TO_POINTER (index));
}
static void
remove_card (GvcMixerControl *control,
guint index)
{
GList *devices, *d;
devices = g_list_concat (g_hash_table_get_values (control->priv->ui_inputs),
g_hash_table_get_values (control->priv->ui_outputs));
for (d = devices; d != NULL; d = d->next) {
GvcMixerCard *card;
GvcMixerUIDevice *device = d->data;
g_object_get (G_OBJECT (device), "card", &card, NULL);
if (card == NULL)
continue;
if (gvc_mixer_card_get_index (card) == index) {
g_signal_emit (G_OBJECT (control),
signals[gvc_mixer_ui_device_is_output (device) ? OUTPUT_REMOVED : INPUT_REMOVED],
0,
gvc_mixer_ui_device_get_id (device));
g_debug ("Card removal remove device %s",
gvc_mixer_ui_device_get_description (device));
g_hash_table_remove (gvc_mixer_ui_device_is_output (device) ? control->priv->ui_outputs : control->priv->ui_inputs,
GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (device)));
}
}
g_list_free (devices);
g_hash_table_remove (control->priv->cards,
GUINT_TO_POINTER (index));
g_signal_emit (G_OBJECT (control),
signals[CARD_REMOVED],
0,
index);
}
static void
remove_sink (GvcMixerControl *control,
guint index)
{
GvcMixerStream *stream;
GvcMixerUIDevice *device;
g_debug ("Removing sink: index=%u", index);
stream = g_hash_table_lookup (control->priv->sinks,
GUINT_TO_POINTER (index));
if (stream == NULL)
return;
device = gvc_mixer_control_lookup_device_from_stream (control, stream);
if (device != NULL) {
gvc_mixer_ui_device_invalidate_stream (device);
if (!gvc_mixer_ui_device_has_ports (device)) {
g_signal_emit (G_OBJECT (control),
signals[OUTPUT_REMOVED],
0,
gvc_mixer_ui_device_get_id (device));
} else {
GList *devices, *d;
devices = g_hash_table_get_values (control->priv->ui_outputs);
for (d = devices; d != NULL; d = d->next) {
guint stream_id = GVC_MIXER_UI_DEVICE_INVALID;
device = d->data;
g_object_get (G_OBJECT (device),
"stream-id", &stream_id,
NULL);
if (stream_id == gvc_mixer_stream_get_id (stream))
gvc_mixer_ui_device_invalidate_stream (device);
}
g_list_free (devices);
}
}
g_hash_table_remove (control->priv->sinks,
GUINT_TO_POINTER (index));
remove_stream (control, stream);
}
static void
remove_source (GvcMixerControl *control,
guint index)
{
GvcMixerStream *stream;
GvcMixerUIDevice *device;
g_debug ("Removing source: index=%u", index);
stream = g_hash_table_lookup (control->priv->sources,
GUINT_TO_POINTER (index));
if (stream == NULL)
return;
device = gvc_mixer_control_lookup_device_from_stream (control, stream);
if (device != NULL) {
gvc_mixer_ui_device_invalidate_stream (device);
if (!gvc_mixer_ui_device_has_ports (device)) {
g_signal_emit (G_OBJECT (control),
signals[INPUT_REMOVED],
0,
gvc_mixer_ui_device_get_id (device));
} else {
GList *devices, *d;
devices = g_hash_table_get_values (control->priv->ui_inputs);
for (d = devices; d != NULL; d = d->next) {
guint stream_id = GVC_MIXER_UI_DEVICE_INVALID;
device = d->data;
g_object_get (G_OBJECT (device),
"stream-id", &stream_id,
NULL);
if (stream_id == gvc_mixer_stream_get_id (stream))
gvc_mixer_ui_device_invalidate_stream (device);
}
g_list_free (devices);
}
}
g_hash_table_remove (control->priv->sources,
GUINT_TO_POINTER (index));
remove_stream (control, stream);
}
static void
remove_sink_input (GvcMixerControl *control,
guint index)
{
GvcMixerStream *stream;
g_debug ("Removing sink input: index=%u", index);
stream = g_hash_table_lookup (control->priv->sink_inputs,
GUINT_TO_POINTER (index));
if (stream == NULL) {
return;
}
g_hash_table_remove (control->priv->sink_inputs,
GUINT_TO_POINTER (index));
remove_stream (control, stream);
}
static void
remove_source_output (GvcMixerControl *control,
guint index)
{
GvcMixerStream *stream;
g_debug ("Removing source output: index=%u", index);
stream = g_hash_table_lookup (control->priv->source_outputs,
GUINT_TO_POINTER (index));
if (stream == NULL) {
return;
}
g_hash_table_remove (control->priv->source_outputs,
GUINT_TO_POINTER (index));
remove_stream (control, stream);
}
static void
_pa_context_subscribe_cb (pa_context *context,
pa_subscription_event_type_t t,
uint32_t index,
void *userdata)
{
GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
case PA_SUBSCRIPTION_EVENT_SINK:
if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
remove_sink (control, index);
} else {
req_update_sink_info (control, index);
}
break;
case PA_SUBSCRIPTION_EVENT_SOURCE:
if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
remove_source (control, index);
} else {
req_update_source_info (control, index);
}
break;
case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
remove_sink_input (control, index);
} else {
req_update_sink_input_info (control, index);
}
break;
case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT:
if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
remove_source_output (control, index);
} else {
req_update_source_output_info (control, index);
}
break;
case PA_SUBSCRIPTION_EVENT_CLIENT:
if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
remove_client (control, index);
} else {
req_update_client_info (control, index);
}
break;
case PA_SUBSCRIPTION_EVENT_SERVER:
req_update_server_info (control, index);
break;
case PA_SUBSCRIPTION_EVENT_CARD:
if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
remove_card (control, index);
} else {
req_update_card (control, index);
}
break;
default:
break;
}
}
static void
gvc_mixer_control_ready (GvcMixerControl *control)
{
pa_operation *o;
pa_context_set_subscribe_callback (control->priv->pa_context,
_pa_context_subscribe_cb,
control);
o = pa_context_subscribe (control->priv->pa_context,
(pa_subscription_mask_t)
(PA_SUBSCRIPTION_MASK_SINK|
PA_SUBSCRIPTION_MASK_SOURCE|
PA_SUBSCRIPTION_MASK_SINK_INPUT|
PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT|
PA_SUBSCRIPTION_MASK_CLIENT|
PA_SUBSCRIPTION_MASK_SERVER|
PA_SUBSCRIPTION_MASK_CARD),
NULL,
NULL);
if (o == NULL) {
g_warning ("pa_context_subscribe() failed");
return;
}
pa_operation_unref (o);
req_update_server_info (control, -1);
req_update_card (control, -1);
req_update_client_info (control, -1);
req_update_sink_info (control, -1);
req_update_source_info (control, -1);
req_update_sink_input_info (control, -1);
req_update_source_output_info (control, -1);
control->priv->server_protocol_version = pa_context_get_server_protocol_version (control->priv->pa_context);
control->priv->n_outstanding = 6;
/* This call is not always supported */
o = pa_ext_stream_restore_read (control->priv->pa_context,
_pa_ext_stream_restore_read_cb,
control);
if (o != NULL) {
pa_operation_unref (o);
control->priv->n_outstanding++;
pa_ext_stream_restore_set_subscribe_cb (control->priv->pa_context,
_pa_ext_stream_restore_subscribe_cb,
control);
o = pa_ext_stream_restore_subscribe (control->priv->pa_context,
1,
NULL,
NULL);
if (o != NULL) {
pa_operation_unref (o);
}
} else {
g_debug ("Failed to initialized stream_restore extension: %s",
pa_strerror (pa_context_errno (control->priv->pa_context)));
}
}
static void
gvc_mixer_new_pa_context (GvcMixerControl *self)
{
pa_proplist *proplist;
g_return_if_fail (self);
g_return_if_fail (!self->priv->pa_context);
proplist = pa_proplist_new ();
pa_proplist_sets (proplist,
PA_PROP_APPLICATION_NAME,
self->priv->name);
pa_proplist_sets (proplist,
PA_PROP_APPLICATION_ID,
"org.gnome.VolumeControl");
pa_proplist_sets (proplist,
PA_PROP_APPLICATION_ICON_NAME,
"multimedia-volume-control");
pa_proplist_sets (proplist,
PA_PROP_APPLICATION_VERSION,
PACKAGE_VERSION);
self->priv->pa_context = pa_context_new_with_proplist (self->priv->pa_api, NULL, proplist);
pa_proplist_free (proplist);
g_assert (self->priv->pa_context);
}
static void
remove_all_items (GvcMixerControl *control,
GHashTable *hash_table,
void (*remove_item)(GvcMixerControl *control, guint index))
{
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init (&iter, hash_table);
while (g_hash_table_iter_next (&iter, &key, &value)) {
if (remove_item) {
remove_item (control, GPOINTER_TO_UINT (key));
g_hash_table_remove (hash_table, key);
g_hash_table_iter_init (&iter, hash_table);
} else {
g_hash_table_iter_remove (&iter);
}
}
}
static gboolean
idle_reconnect (gpointer data)
{
GvcMixerControl *control = GVC_MIXER_CONTROL (data);
g_return_val_if_fail (control, FALSE);
g_debug ("Reconnect: clean up all objects");
remove_all_items (control, control->priv->sinks, remove_sink);
remove_all_items (control, control->priv->sources, remove_source);
remove_all_items (control, control->priv->sink_inputs, remove_sink_input);
remove_all_items (control, control->priv->source_outputs, remove_source_output);
remove_all_items (control, control->priv->cards, remove_card);
remove_all_items (control, control->priv->ui_inputs, NULL);
remove_all_items (control, control->priv->ui_outputs, NULL);
remove_all_items (control, control->priv->clients, remove_client);
g_debug ("Reconnect: make new connection");
if (control->priv->pa_context) {
pa_context_unref (control->priv->pa_context);
control->priv->pa_context = NULL;
control->priv->server_protocol_version = 0;
gvc_mixer_new_pa_context (control);
}
gvc_mixer_control_open (control); /* cannot fail */
control->priv->reconnect_id = 0;
return FALSE;
}
static void
_pa_context_state_cb (pa_context *context,
void *userdata)
{
GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
switch (pa_context_get_state (context)) {
case PA_CONTEXT_UNCONNECTED:
case PA_CONTEXT_CONNECTING:
case PA_CONTEXT_AUTHORIZING:
case PA_CONTEXT_SETTING_NAME:
break;
case PA_CONTEXT_READY:
gvc_mixer_control_ready (control);
break;
case PA_CONTEXT_FAILED:
control->priv->state = GVC_STATE_FAILED;
g_signal_emit (control, signals[STATE_CHANGED], 0, GVC_STATE_FAILED);
if (control->priv->reconnect_id == 0)
control->priv->reconnect_id = g_timeout_add_seconds (RECONNECT_DELAY, idle_reconnect, control);
break;
case PA_CONTEXT_TERMINATED:
default:
/* FIXME: */
break;
}
}
gboolean
gvc_mixer_control_open (GvcMixerControl *control)
{
int res;
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
g_return_val_if_fail (control->priv->pa_context != NULL, FALSE);
g_return_val_if_fail (pa_context_get_state (control->priv->pa_context) == PA_CONTEXT_UNCONNECTED, FALSE);
pa_context_set_state_callback (control->priv->pa_context,
_pa_context_state_cb,
control);
control->priv->state = GVC_STATE_CONNECTING;
g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_CONNECTING);
res = pa_context_connect (control->priv->pa_context, NULL, (pa_context_flags_t) PA_CONTEXT_NOFAIL, NULL);
if (res < 0) {
g_warning ("Failed to connect context: %s",
pa_strerror (pa_context_errno (control->priv->pa_context)));
}
return res;
}
gboolean
gvc_mixer_control_close (GvcMixerControl *control)
{
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
g_return_val_if_fail (control->priv->pa_context != NULL, FALSE);
pa_context_disconnect (control->priv->pa_context);
control->priv->state = GVC_STATE_CLOSED;
g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_CLOSED);
return TRUE;
}
static void
gvc_mixer_control_dispose (GObject *object)
{
GvcMixerControl *control = GVC_MIXER_CONTROL (object);
if (control->priv->reconnect_id != 0) {
g_source_remove (control->priv->reconnect_id);
control->priv->reconnect_id = 0;
}
if (control->priv->pa_context != NULL) {
pa_context_unref (control->priv->pa_context);
control->priv->pa_context = NULL;
}
if (control->priv->default_source_name != NULL) {
g_free (control->priv->default_source_name);
control->priv->default_source_name = NULL;
}
if (control->priv->default_sink_name != NULL) {
g_free (control->priv->default_sink_name);
control->priv->default_sink_name = NULL;
}
if (control->priv->pa_mainloop != NULL) {
pa_glib_mainloop_free (control->priv->pa_mainloop);
control->priv->pa_mainloop = NULL;
}
if (control->priv->all_streams != NULL) {
g_hash_table_destroy (control->priv->all_streams);
control->priv->all_streams = NULL;
}
if (control->priv->sinks != NULL) {
g_hash_table_destroy (control->priv->sinks);
control->priv->sinks = NULL;
}
if (control->priv->sources != NULL) {
g_hash_table_destroy (control->priv->sources);
control->priv->sources = NULL;
}
if (control->priv->sink_inputs != NULL) {
g_hash_table_destroy (control->priv->sink_inputs);
control->priv->sink_inputs = NULL;
}
if (control->priv->source_outputs != NULL) {
g_hash_table_destroy (control->priv->source_outputs);
control->priv->source_outputs = NULL;
}
if (control->priv->clients != NULL) {
g_hash_table_destroy (control->priv->clients);
control->priv->clients = NULL;
}
if (control->priv->cards != NULL) {
g_hash_table_destroy (control->priv->cards);
control->priv->cards = NULL;
}
if (control->priv->ui_outputs != NULL) {
g_hash_table_destroy (control->priv->ui_outputs);
control->priv->ui_outputs = NULL;
}
if (control->priv->ui_inputs != NULL) {
g_hash_table_destroy (control->priv->ui_inputs);
control->priv->ui_inputs = NULL;
}
free_priv_port_names (control);
G_OBJECT_CLASS (gvc_mixer_control_parent_class)->dispose (object);
}
static void
gvc_mixer_control_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GvcMixerControl *self = GVC_MIXER_CONTROL (object);
switch (prop_id) {
case PROP_NAME:
g_free (self->priv->name);
self->priv->name = g_value_dup_string (value);
g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_NAME]);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gvc_mixer_control_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
GvcMixerControl *self = GVC_MIXER_CONTROL (object);
switch (prop_id) {
case PROP_NAME:
g_value_set_string (value, self->priv->name);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static GObject *
gvc_mixer_control_constructor (GType type,
guint n_construct_properties,
GObjectConstructParam *construct_params)
{
GObject *object;
GvcMixerControl *self;
object = G_OBJECT_CLASS (gvc_mixer_control_parent_class)->constructor (type, n_construct_properties, construct_params);
self = GVC_MIXER_CONTROL (object);
gvc_mixer_new_pa_context (self);
self->priv->profile_swapping_device_id = GVC_MIXER_UI_DEVICE_INVALID;
return object;
}
static void
gvc_mixer_control_class_init (GvcMixerControlClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->constructor = gvc_mixer_control_constructor;
object_class->dispose = gvc_mixer_control_dispose;
object_class->finalize = gvc_mixer_control_finalize;
object_class->set_property = gvc_mixer_control_set_property;
object_class->get_property = gvc_mixer_control_get_property;
obj_props[PROP_NAME] = g_param_spec_string ("name",
"Name",
"Name to display for this mixer control",
NULL,
G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY|G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, N_PROPS, obj_props);
signals [STATE_CHANGED] =
g_signal_new ("state-changed",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GvcMixerControlClass, state_changed),
NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_UINT);
signals [STREAM_ADDED] =
g_signal_new ("stream-added",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GvcMixerControlClass, stream_added),
NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_UINT);
signals [STREAM_REMOVED] =
g_signal_new ("stream-removed",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GvcMixerControlClass, stream_removed),
NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_UINT);
signals [STREAM_CHANGED] =
g_signal_new ("stream-changed",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GvcMixerControlClass, stream_changed),
NULL, NULL, NULL,
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, NULL,
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),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GvcMixerControlClass, card_added),
NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_UINT);
signals [CARD_REMOVED] =
g_signal_new ("card-removed",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GvcMixerControlClass, card_removed),
NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_UINT);
signals [DEFAULT_SINK_CHANGED] =
g_signal_new ("default-sink-changed",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GvcMixerControlClass, default_sink_changed),
NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_UINT);
signals [DEFAULT_SOURCE_CHANGED] =
g_signal_new ("default-source-changed",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GvcMixerControlClass, default_source_changed),
NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_UINT);
signals [ACTIVE_OUTPUT_UPDATE] =
g_signal_new ("active-output-update",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GvcMixerControlClass, active_output_update),
NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_UINT);
signals [ACTIVE_INPUT_UPDATE] =
g_signal_new ("active-input-update",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GvcMixerControlClass, active_input_update),
NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_UINT);
signals [OUTPUT_ADDED] =
g_signal_new ("output-added",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GvcMixerControlClass, output_added),
NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_UINT);
signals [INPUT_ADDED] =
g_signal_new ("input-added",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GvcMixerControlClass, input_added),
NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_UINT);
signals [OUTPUT_REMOVED] =
g_signal_new ("output-removed",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GvcMixerControlClass, output_removed),
NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_UINT);
signals [INPUT_REMOVED] =
g_signal_new ("input-removed",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GvcMixerControlClass, input_removed),
NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_UINT);
}
static void
gvc_mixer_control_init (GvcMixerControl *control)
{
control->priv = gvc_mixer_control_get_instance_private (control);
control->priv->pa_mainloop = pa_glib_mainloop_new (g_main_context_default ());
g_assert (control->priv->pa_mainloop);
control->priv->pa_api = pa_glib_mainloop_get_api (control->priv->pa_mainloop);
g_assert (control->priv->pa_api);
control->priv->all_streams = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
control->priv->sinks = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
control->priv->sources = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
control->priv->sink_inputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
control->priv->source_outputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
control->priv->cards = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
control->priv->ui_outputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
control->priv->ui_inputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
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;
}
static void
gvc_mixer_control_finalize (GObject *object)
{
GvcMixerControl *mixer_control;
g_return_if_fail (object != NULL);
g_return_if_fail (GVC_IS_MIXER_CONTROL (object));
mixer_control = GVC_MIXER_CONTROL (object);
g_free (mixer_control->priv->name);
mixer_control->priv->name = NULL;
g_return_if_fail (mixer_control->priv != NULL);
G_OBJECT_CLASS (gvc_mixer_control_parent_class)->finalize (object);
}
GvcMixerControl *
gvc_mixer_control_new (const char *name)
{
GObject *control;
control = g_object_new (GVC_TYPE_MIXER_CONTROL,
"name", name,
NULL);
return GVC_MIXER_CONTROL (control);
}
gdouble
gvc_mixer_control_get_vol_max_norm (GvcMixerControl *control)
{
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), 0);
return (gdouble) PA_VOLUME_NORM;
}
gdouble
gvc_mixer_control_get_vol_max_amplified (GvcMixerControl *control)
{
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), 0);
return (gdouble) PA_VOLUME_UI_MAX;
}