mirror of
https://gitlab.gnome.org/GNOME/libgnome-volume-control.git
synced 2024-11-27 11:40:49 -05:00
ce8e4880ce
When on A2DP profile and a Bluetooth input is selected, we first need to switch the profile to HFP/HSP, then select the default source to be that profile. In some cases the latter step was forgotten, because the variable "profile_swapping_device_id" was reset before it was supposed to be used. https://bugzilla.gnome.org/show_bug.cgi?id=736943
3744 lines
139 KiB
C
3744 lines
139 KiB
C
/* -*- 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 GVC_MIXER_CONTROL_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GVC_TYPE_MIXER_CONTROL, GvcMixerControlPrivate))
|
|
|
|
#define RECONNECT_DELAY 5
|
|
|
|
enum {
|
|
PROP_0,
|
|
PROP_NAME
|
|
};
|
|
|
|
struct GvcMixerControlPrivate
|
|
{
|
|
pa_glib_mainloop *pa_mainloop;
|
|
pa_mainloop_api *pa_api;
|
|
pa_context *pa_context;
|
|
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;
|
|
#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 (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;
|
|
|
|
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:
|
|
* @profile: 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_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_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;
|
|
|
|
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), FALSE);
|
|
|
|
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;
|
|
gint 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);
|
|
|
|
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", (gint)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);
|
|
gint stream_port_count = 0;
|
|
|
|
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", (gint)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", (gint)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);
|
|
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;
|
|
stream_port_count ++;
|
|
|
|
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);
|
|
}
|
|
|
|
/*
|
|
* 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);
|
|
|
|
/* 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, "applications-multimedia");
|
|
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;
|
|
int headset_card;
|
|
} PortStatusData;
|
|
|
|
static void
|
|
port_status_data_free (PortStatusData *data)
|
|
{
|
|
if (data == NULL)
|
|
return;
|
|
g_free (data->port_name_to_set);
|
|
g_free (data);
|
|
}
|
|
|
|
/*
|
|
We need to re-enumerate sources and sinks every time the user makes a choice,
|
|
because they can change due to use interaction in other software (or policy
|
|
changes inside PulseAudio). Enumeration means PulseAudio will do a series of
|
|
callbacks, one for every source/sink.
|
|
Set the port when we find the correct source/sink.
|
|
*/
|
|
|
|
static void
|
|
sink_info_cb (pa_context *c,
|
|
const pa_sink_info *i,
|
|
int eol,
|
|
void *userdata)
|
|
{
|
|
PortStatusData *data = userdata;
|
|
pa_operation *o;
|
|
int j;
|
|
const char *s;
|
|
|
|
if (eol != 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;
|
|
int 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_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;
|
|
|
|
data = g_new0 (PortStatusData, 1);
|
|
data->port_name_to_set = g_strdup (port_name);
|
|
data->headset_card = id;
|
|
|
|
if (is_output)
|
|
o = pa_context_get_sink_info_list (control->priv->pa_context, sink_info_cb, data);
|
|
else
|
|
o = pa_context_get_source_info_list (control->priv->pa_context, source_info_cb, data);
|
|
|
|
g_clear_pointer (&o, pa_operation_unref);
|
|
}
|
|
#endif /* HAVE_ALSA */
|
|
|
|
void
|
|
gvc_mixer_control_set_headset_port (GvcMixerControl *control,
|
|
guint id,
|
|
GvcHeadsetPortChoice choice)
|
|
{
|
|
#ifdef HAVE_ALSA
|
|
switch (choice) {
|
|
case GVC_HEADSET_PORT_CHOICE_HEADPHONES:
|
|
gvc_mixer_control_set_port_status_for_headset (control, id, "analog-output-headphones", TRUE);
|
|
gvc_mixer_control_set_port_status_for_headset (control, id, "analog-input-internal-mic", FALSE);
|
|
break;
|
|
case GVC_HEADSET_PORT_CHOICE_HEADSET:
|
|
gvc_mixer_control_set_port_status_for_headset (control, id, "analog-output-headphones", TRUE);
|
|
gvc_mixer_control_set_port_status_for_headset (control, id, "analog-input-headset-mic", FALSE);
|
|
break;
|
|
case GVC_HEADSET_PORT_CHOICE_MIC:
|
|
gvc_mixer_control_set_port_status_for_headset (control, id, "analog-output-speaker", TRUE);
|
|
gvc_mixer_control_set_port_status_for_headset (control, id, "analog-input-headphone-mic", FALSE);
|
|
break;
|
|
default:
|
|
g_assert_not_reached ();
|
|
}
|
|
#else
|
|
g_warning ("BUG: libgnome-volume-control compiled without ALSA support");
|
|
#endif /* HAVE_ALSA */
|
|
}
|
|
|
|
#ifdef HAVE_ALSA
|
|
typedef struct {
|
|
const pa_card_port_info *headphones;
|
|
const pa_card_port_info *headsetmic;
|
|
const pa_card_port_info *headphonemic;
|
|
} headset_ports;
|
|
|
|
/*
|
|
TODO: Check if we still need this with the changed PA port names
|
|
|
|
In PulseAudio ports will show up with the following names:
|
|
Headphones - analog-output-headphones
|
|
Headset mic - analog-input-headset-mic (was: analog-input-microphone-headset)
|
|
Jack in mic-in mode - analog-input-headphone-mic (was: analog-input-microphone)
|
|
|
|
However, since regular mics also show up as analog-input-microphone,
|
|
we need to check for certain controls on alsa mixer level too, to know
|
|
if we deal with a separate mic jack, or a multi-function jack with a
|
|
mic-in mode (also called "headphone mic").
|
|
We check for the following names:
|
|
|
|
Headphone Mic Jack - indicates headphone and mic-in mode share the same jack,
|
|
i e, not two separate jacks. Hardware cannot distinguish between a
|
|
headphone and a mic.
|
|
Headset Mic Phantom Jack - indicates headset jack where hardware can not
|
|
distinguish between headphones and headsets
|
|
Headset Mic Jack - indicates headset jack where hardware can distinguish
|
|
between headphones and headsets. There is no use popping up a dialog in
|
|
this case, unless we already need to do this for the mic-in mode.
|
|
*/
|
|
|
|
static headset_ports *
|
|
get_headset_ports (const pa_card_info *c)
|
|
{
|
|
headset_ports *h;
|
|
guint i;
|
|
|
|
h = g_new0 (headset_ports, 1);
|
|
|
|
for (i = 0; i < c->n_ports; i++) {
|
|
pa_card_port_info *p = c->ports[i];
|
|
|
|
if (strcmp (p->name, "analog-output-headphones") == 0)
|
|
h->headphones = p;
|
|
else if (strcmp (p->name, "analog-input-headset-mic") == 0)
|
|
h->headsetmic = p;
|
|
else if (strcmp(p->name, "analog-input-headphone-mic") == 0)
|
|
h->headphonemic = p;
|
|
}
|
|
return h;
|
|
}
|
|
|
|
static gboolean
|
|
verify_alsa_card (int cardindex,
|
|
gboolean *headsetmic,
|
|
gboolean *headphonemic)
|
|
{
|
|
char *ctlstr;
|
|
snd_hctl_t *hctl;
|
|
snd_ctl_elem_id_t *id;
|
|
int err;
|
|
|
|
*headsetmic = FALSE;
|
|
*headphonemic = FALSE;
|
|
|
|
ctlstr = g_strdup_printf ("hw:%i", cardindex);
|
|
if ((err = snd_hctl_open (&hctl, ctlstr, 0)) < 0) {
|
|
g_warning ("snd_hctl_open failed: %s", snd_strerror(err));
|
|
g_free (ctlstr);
|
|
return FALSE;
|
|
}
|
|
g_free (ctlstr);
|
|
|
|
if ((err = snd_hctl_load (hctl)) < 0) {
|
|
g_warning ("snd_hctl_load failed: %s", snd_strerror(err));
|
|
snd_hctl_close (hctl);
|
|
return FALSE;
|
|
}
|
|
|
|
snd_ctl_elem_id_alloca (&id);
|
|
|
|
snd_ctl_elem_id_clear (id);
|
|
snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD);
|
|
snd_ctl_elem_id_set_name (id, "Headphone Mic Jack");
|
|
if (snd_hctl_find_elem (hctl, id))
|
|
*headphonemic = TRUE;
|
|
|
|
snd_ctl_elem_id_clear (id);
|
|
snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD);
|
|
snd_ctl_elem_id_set_name (id, "Headset Mic Phantom Jack");
|
|
if (snd_hctl_find_elem (hctl, id))
|
|
*headsetmic = TRUE;
|
|
|
|
if (*headphonemic) {
|
|
snd_ctl_elem_id_clear (id);
|
|
snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD);
|
|
snd_ctl_elem_id_set_name (id, "Headset Mic Jack");
|
|
if (snd_hctl_find_elem (hctl, id))
|
|
*headsetmic = TRUE;
|
|
}
|
|
|
|
snd_hctl_close (hctl);
|
|
return *headsetmic || *headphonemic;
|
|
}
|
|
|
|
static void
|
|
check_audio_device_selection_needed (GvcMixerControl *control,
|
|
const pa_card_info *info)
|
|
{
|
|
headset_ports *h;
|
|
gboolean start_dialog, stop_dialog;
|
|
|
|
start_dialog = FALSE;
|
|
stop_dialog = FALSE;
|
|
h = get_headset_ports (info);
|
|
|
|
if (!h->headphones ||
|
|
(!h->headsetmic && !h->headphonemic)) {
|
|
/* Not a headset jack */
|
|
goto out;
|
|
}
|
|
|
|
if (control->priv->headset_card != (int) info->index) {
|
|
int cardindex;
|
|
gboolean hsmic, hpmic;
|
|
const char *s;
|
|
|
|
s = pa_proplist_gets (info->proplist, "alsa.card");
|
|
if (!s)
|
|
goto out;
|
|
|
|
cardindex = strtol (s, NULL, 10);
|
|
if (cardindex == 0 && strcmp(s, "0") != 0)
|
|
goto out;
|
|
|
|
if (!verify_alsa_card(cardindex, &hsmic, &hpmic))
|
|
goto out;
|
|
|
|
control->priv->headset_card = info->index;
|
|
control->priv->has_headsetmic = hsmic && h->headsetmic;
|
|
control->priv->has_headphonemic = hpmic && h->headphonemic;
|
|
} else {
|
|
start_dialog = (h->headphones->available != PA_PORT_AVAILABLE_NO) && !control->priv->headset_plugged_in;
|
|
stop_dialog = (h->headphones->available == PA_PORT_AVAILABLE_NO) && control->priv->headset_plugged_in;
|
|
}
|
|
|
|
control->priv->headset_plugged_in = h->headphones->available != PA_PORT_AVAILABLE_NO;
|
|
|
|
if (!start_dialog &&
|
|
!stop_dialog)
|
|
goto out;
|
|
|
|
if (stop_dialog) {
|
|
g_signal_emit (G_OBJECT (control),
|
|
signals[AUDIO_DEVICE_SELECTION_NEEDED],
|
|
0,
|
|
info->index,
|
|
FALSE,
|
|
GVC_HEADSET_PORT_CHOICE_NONE);
|
|
} else {
|
|
GvcHeadsetPortChoice choices;
|
|
|
|
choices = GVC_HEADSET_PORT_CHOICE_HEADPHONES;
|
|
if (control->priv->has_headsetmic)
|
|
choices |= GVC_HEADSET_PORT_CHOICE_HEADSET;
|
|
if (control->priv->has_headphonemic)
|
|
choices |= GVC_HEADSET_PORT_CHOICE_MIC;
|
|
|
|
g_signal_emit (G_OBJECT (control),
|
|
signals[AUDIO_DEVICE_SELECTION_NEEDED],
|
|
0,
|
|
info->index,
|
|
TRUE,
|
|
choices);
|
|
}
|
|
|
|
out:
|
|
g_free (h);
|
|
}
|
|
#endif /* HAVE_ALSA */
|
|
|
|
/*
|
|
* At this point we can determine all devices available to us (besides network 'ports')
|
|
* This is done by the following:
|
|
*
|
|
* - 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 ("Udpating 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);
|
|
gvc_mixer_card_set_profiles (card, profile_list);
|
|
|
|
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_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));
|
|
}
|
|
|
|
max_volume = pa_cvolume_max (&info->volume);
|
|
|
|
gvc_mixer_stream_set_name (stream, _("System Sounds"));
|
|
gvc_mixer_stream_set_icon_name (stream, "multimedia-volume-control");
|
|
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 (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->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_streams (GvcMixerControl *control, GHashTable *hash_table)
|
|
{
|
|
GHashTableIter iter;
|
|
gpointer key, value;
|
|
|
|
g_hash_table_iter_init (&iter, hash_table);
|
|
while (g_hash_table_iter_next (&iter, &key, &value)) {
|
|
remove_stream (control, value);
|
|
g_hash_table_iter_remove (&iter);
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
idle_reconnect (gpointer data)
|
|
{
|
|
GvcMixerControl *control = GVC_MIXER_CONTROL (data);
|
|
GHashTableIter iter;
|
|
gpointer key, value;
|
|
|
|
g_return_val_if_fail (control, FALSE);
|
|
|
|
if (control->priv->pa_context) {
|
|
pa_context_unref (control->priv->pa_context);
|
|
control->priv->pa_context = NULL;
|
|
gvc_mixer_new_pa_context (control);
|
|
}
|
|
|
|
remove_all_streams (control, control->priv->sinks);
|
|
remove_all_streams (control, control->priv->sources);
|
|
remove_all_streams (control, control->priv->sink_inputs);
|
|
remove_all_streams (control, control->priv->source_outputs);
|
|
|
|
g_hash_table_iter_init (&iter, control->priv->clients);
|
|
while (g_hash_table_iter_next (&iter, &key, &value))
|
|
g_hash_table_iter_remove (&iter);
|
|
|
|
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;
|
|
}
|
|
|
|
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 (G_OBJECT (self), "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;
|
|
|
|
g_object_class_install_property (object_class,
|
|
PROP_NAME,
|
|
g_param_spec_string ("name",
|
|
"Name",
|
|
"Name to display for this mixer control",
|
|
NULL,
|
|
G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY));
|
|
|
|
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,
|
|
g_cclosure_marshal_VOID__UINT,
|
|
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,
|
|
g_cclosure_marshal_VOID__UINT,
|
|
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,
|
|
g_cclosure_marshal_VOID__UINT,
|
|
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,
|
|
g_cclosure_marshal_VOID__UINT,
|
|
G_TYPE_NONE, 1, G_TYPE_UINT);
|
|
signals [AUDIO_DEVICE_SELECTION_NEEDED] =
|
|
g_signal_new ("audio-device-selection-needed",
|
|
G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_LAST,
|
|
0,
|
|
NULL, NULL,
|
|
g_cclosure_marshal_generic,
|
|
G_TYPE_NONE, 3, G_TYPE_UINT, G_TYPE_BOOLEAN, G_TYPE_UINT);
|
|
signals [CARD_ADDED] =
|
|
g_signal_new ("card-added",
|
|
G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_LAST,
|
|
G_STRUCT_OFFSET (GvcMixerControlClass, card_added),
|
|
NULL, NULL,
|
|
g_cclosure_marshal_VOID__UINT,
|
|
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,
|
|
g_cclosure_marshal_VOID__UINT,
|
|
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,
|
|
g_cclosure_marshal_VOID__UINT,
|
|
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,
|
|
g_cclosure_marshal_VOID__UINT,
|
|
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,
|
|
g_cclosure_marshal_VOID__UINT,
|
|
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,
|
|
g_cclosure_marshal_VOID__UINT,
|
|
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,
|
|
g_cclosure_marshal_VOID__UINT,
|
|
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,
|
|
g_cclosure_marshal_VOID__UINT,
|
|
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,
|
|
g_cclosure_marshal_VOID__UINT,
|
|
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,
|
|
g_cclosure_marshal_VOID__UINT,
|
|
G_TYPE_NONE, 1, G_TYPE_UINT);
|
|
g_type_class_add_private (klass, sizeof (GvcMixerControlPrivate));
|
|
}
|
|
|
|
|
|
static void
|
|
gvc_mixer_control_init (GvcMixerControl *control)
|
|
{
|
|
control->priv = GVC_MIXER_CONTROL_GET_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)
|
|
{
|
|
return (gdouble) PA_VOLUME_NORM;
|
|
}
|
|
|
|
gdouble
|
|
gvc_mixer_control_get_vol_max_amplified (GvcMixerControl *control)
|
|
{
|
|
return (gdouble) PA_VOLUME_UI_MAX;
|
|
}
|