mirror of
https://gitlab.gnome.org/GNOME/libgnome-volume-control.git
synced 2024-11-23 09:20:41 -05:00
2a7a100a13
gvc_mixer_control_get_vol_max_norm() and gvc_mixer_control_get_vol_max_amplified() do not access any of the instance variables. In GNOME Settings, we even pass NULL to these functions. So checking the instance is not necessary. In fact, this is causing instance check failures in GNOME Settings, and the entire Sound panel is broken since those functions are returning zero. The reason that only the checks are being dropped, and not the parameter entirely, is to preserve API compatibility.
3755 lines
139 KiB
C
3755 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 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_WITH_PRIVATE (GvcMixerControl, gvc_mixer_control, G_TYPE_OBJECT)
|
|
|
|
pa_context *
|
|
gvc_mixer_control_get_pa_context (GvcMixerControl *control)
|
|
{
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
|
|
return control->priv->pa_context;
|
|
}
|
|
|
|
/**
|
|
* gvc_mixer_control_get_event_sink_input:
|
|
* @control:
|
|
*
|
|
* Returns: (transfer none):
|
|
*/
|
|
GvcMixerStream *
|
|
gvc_mixer_control_get_event_sink_input (GvcMixerControl *control)
|
|
{
|
|
GvcMixerStream *stream;
|
|
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
|
|
|
|
stream = g_hash_table_lookup (control->priv->all_streams,
|
|
GUINT_TO_POINTER (control->priv->event_sink_input_id));
|
|
|
|
return stream;
|
|
}
|
|
|
|
static void
|
|
gvc_mixer_control_stream_restore_cb (pa_context *c,
|
|
GvcMixerStream *new_stream,
|
|
const pa_ext_stream_restore_info *info,
|
|
GvcMixerControl *control)
|
|
{
|
|
pa_operation *o;
|
|
pa_ext_stream_restore_info new_info;
|
|
|
|
if (new_stream == NULL)
|
|
return;
|
|
|
|
new_info.name = info->name;
|
|
new_info.channel_map = info->channel_map;
|
|
new_info.volume = info->volume;
|
|
new_info.mute = info->mute;
|
|
|
|
new_info.device = gvc_mixer_stream_get_name (new_stream);
|
|
|
|
o = pa_ext_stream_restore_write (control->priv->pa_context,
|
|
PA_UPDATE_REPLACE,
|
|
&new_info, 1,
|
|
TRUE, NULL, NULL);
|
|
|
|
if (o == NULL) {
|
|
g_warning ("pa_ext_stream_restore_write() failed: %s",
|
|
pa_strerror (pa_context_errno (control->priv->pa_context)));
|
|
return;
|
|
}
|
|
|
|
g_debug ("Changed default device for %s to %s", info->name, new_info.device);
|
|
|
|
pa_operation_unref (o);
|
|
}
|
|
|
|
static void
|
|
gvc_mixer_control_stream_restore_sink_cb (pa_context *c,
|
|
const pa_ext_stream_restore_info *info,
|
|
int eol,
|
|
void *userdata)
|
|
{
|
|
GvcMixerControl *control = (GvcMixerControl *) userdata;
|
|
if (eol || info == NULL || !g_str_has_prefix(info->name, "sink-input-by"))
|
|
return;
|
|
gvc_mixer_control_stream_restore_cb (c, control->priv->new_default_sink_stream, info, control);
|
|
}
|
|
|
|
static void
|
|
gvc_mixer_control_stream_restore_source_cb (pa_context *c,
|
|
const pa_ext_stream_restore_info *info,
|
|
int eol,
|
|
void *userdata)
|
|
{
|
|
GvcMixerControl *control = (GvcMixerControl *) userdata;
|
|
if (eol || info == NULL || !g_str_has_prefix(info->name, "source-output-by"))
|
|
return;
|
|
gvc_mixer_control_stream_restore_cb (c, control->priv->new_default_source_stream, info, control);
|
|
}
|
|
|
|
/**
|
|
* gvc_mixer_control_lookup_device_from_stream:
|
|
* @control:
|
|
* @stream:
|
|
*
|
|
* Returns: (transfer none): a #GvcUIDevice or %NULL
|
|
*/
|
|
GvcMixerUIDevice *
|
|
gvc_mixer_control_lookup_device_from_stream (GvcMixerControl *control,
|
|
GvcMixerStream *stream)
|
|
{
|
|
GList *devices, *d;
|
|
gboolean is_network_stream;
|
|
const GList *ports;
|
|
GvcMixerUIDevice *ret;
|
|
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
|
|
g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
|
|
|
|
if (GVC_IS_MIXER_SOURCE (stream))
|
|
devices = g_hash_table_get_values (control->priv->ui_inputs);
|
|
else
|
|
devices = g_hash_table_get_values (control->priv->ui_outputs);
|
|
|
|
ret = NULL;
|
|
ports = gvc_mixer_stream_get_ports (stream);
|
|
is_network_stream = (ports == NULL);
|
|
|
|
for (d = devices; d != NULL; d = d->next) {
|
|
GvcMixerUIDevice *device = d->data;
|
|
guint stream_id = G_MAXUINT;
|
|
|
|
g_object_get (G_OBJECT (device),
|
|
"stream-id", &stream_id,
|
|
NULL);
|
|
|
|
if (is_network_stream &&
|
|
stream_id == gvc_mixer_stream_get_id (stream)) {
|
|
g_debug ("lookup device from stream - %s - it is a network_stream ",
|
|
gvc_mixer_ui_device_get_description (device));
|
|
ret = device;
|
|
break;
|
|
} else if (!is_network_stream) {
|
|
const GvcMixerStreamPort *port;
|
|
port = gvc_mixer_stream_get_port (stream);
|
|
|
|
if (stream_id == gvc_mixer_stream_get_id (stream) &&
|
|
g_strcmp0 (gvc_mixer_ui_device_get_port (device),
|
|
port->port) == 0) {
|
|
g_debug ("lookup-device-from-stream found device: device description '%s', device port = '%s', device stream id %i AND stream port = '%s' stream id '%u' and stream description '%s'",
|
|
gvc_mixer_ui_device_get_description (device),
|
|
gvc_mixer_ui_device_get_port (device),
|
|
stream_id,
|
|
port->port,
|
|
gvc_mixer_stream_get_id (stream),
|
|
gvc_mixer_stream_get_description (stream));
|
|
ret = device;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
g_debug ("gvc_mixer_control_lookup_device_from_stream - Could not find a device for stream '%s'",gvc_mixer_stream_get_description (stream));
|
|
|
|
g_list_free (devices);
|
|
|
|
return ret;
|
|
}
|
|
|
|
gboolean
|
|
gvc_mixer_control_set_default_sink (GvcMixerControl *control,
|
|
GvcMixerStream *stream)
|
|
{
|
|
pa_operation *o;
|
|
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
|
|
g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
|
|
|
|
g_debug ("about to set default sink on server");
|
|
o = pa_context_set_default_sink (control->priv->pa_context,
|
|
gvc_mixer_stream_get_name (stream),
|
|
NULL,
|
|
NULL);
|
|
if (o == NULL) {
|
|
g_warning ("pa_context_set_default_sink() failed: %s",
|
|
pa_strerror (pa_context_errno (control->priv->pa_context)));
|
|
return FALSE;
|
|
}
|
|
|
|
pa_operation_unref (o);
|
|
|
|
control->priv->new_default_sink_stream = stream;
|
|
g_object_add_weak_pointer (G_OBJECT (stream), (gpointer *) &control->priv->new_default_sink_stream);
|
|
|
|
o = pa_ext_stream_restore_read (control->priv->pa_context,
|
|
gvc_mixer_control_stream_restore_sink_cb,
|
|
control);
|
|
|
|
if (o == NULL) {
|
|
g_warning ("pa_ext_stream_restore_read() failed: %s",
|
|
pa_strerror (pa_context_errno (control->priv->pa_context)));
|
|
return FALSE;
|
|
}
|
|
|
|
pa_operation_unref (o);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
gvc_mixer_control_set_default_source (GvcMixerControl *control,
|
|
GvcMixerStream *stream)
|
|
{
|
|
GvcMixerUIDevice* input;
|
|
pa_operation *o;
|
|
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
|
|
g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
|
|
|
|
o = pa_context_set_default_source (control->priv->pa_context,
|
|
gvc_mixer_stream_get_name (stream),
|
|
NULL,
|
|
NULL);
|
|
if (o == NULL) {
|
|
g_warning ("pa_context_set_default_source() failed");
|
|
return FALSE;
|
|
}
|
|
|
|
pa_operation_unref (o);
|
|
|
|
control->priv->new_default_source_stream = stream;
|
|
g_object_add_weak_pointer (G_OBJECT (stream), (gpointer *) &control->priv->new_default_source_stream);
|
|
|
|
o = pa_ext_stream_restore_read (control->priv->pa_context,
|
|
gvc_mixer_control_stream_restore_source_cb,
|
|
control);
|
|
|
|
if (o == NULL) {
|
|
g_warning ("pa_ext_stream_restore_read() failed: %s",
|
|
pa_strerror (pa_context_errno (control->priv->pa_context)));
|
|
return FALSE;
|
|
}
|
|
|
|
pa_operation_unref (o);
|
|
|
|
/* source change successful, update the UI. */
|
|
input = gvc_mixer_control_lookup_device_from_stream (control, stream);
|
|
g_signal_emit (G_OBJECT (control),
|
|
signals[ACTIVE_INPUT_UPDATE],
|
|
0,
|
|
gvc_mixer_ui_device_get_id (input));
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* gvc_mixer_control_get_default_sink:
|
|
* @control:
|
|
*
|
|
* Returns: (transfer none):
|
|
*/
|
|
GvcMixerStream *
|
|
gvc_mixer_control_get_default_sink (GvcMixerControl *control)
|
|
{
|
|
GvcMixerStream *stream;
|
|
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
|
|
|
|
if (control->priv->default_sink_is_set) {
|
|
stream = g_hash_table_lookup (control->priv->all_streams,
|
|
GUINT_TO_POINTER (control->priv->default_sink_id));
|
|
} else {
|
|
stream = NULL;
|
|
}
|
|
|
|
return stream;
|
|
}
|
|
|
|
/**
|
|
* gvc_mixer_control_get_default_source:
|
|
* @control:
|
|
*
|
|
* Returns: (transfer none):
|
|
*/
|
|
GvcMixerStream *
|
|
gvc_mixer_control_get_default_source (GvcMixerControl *control)
|
|
{
|
|
GvcMixerStream *stream;
|
|
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
|
|
|
|
if (control->priv->default_source_is_set) {
|
|
stream = g_hash_table_lookup (control->priv->all_streams,
|
|
GUINT_TO_POINTER (control->priv->default_source_id));
|
|
} else {
|
|
stream = NULL;
|
|
}
|
|
|
|
return stream;
|
|
}
|
|
|
|
static gpointer
|
|
gvc_mixer_control_lookup_id (GHashTable *hash_table,
|
|
guint id)
|
|
{
|
|
return g_hash_table_lookup (hash_table,
|
|
GUINT_TO_POINTER (id));
|
|
}
|
|
|
|
/**
|
|
* gvc_mixer_control_lookup_stream_id:
|
|
* @control:
|
|
* @id:
|
|
*
|
|
* Returns: (transfer none):
|
|
*/
|
|
GvcMixerStream *
|
|
gvc_mixer_control_lookup_stream_id (GvcMixerControl *control,
|
|
guint id)
|
|
{
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
|
|
|
|
return gvc_mixer_control_lookup_id (control->priv->all_streams, id);
|
|
}
|
|
|
|
/**
|
|
* gvc_mixer_control_lookup_card_id:
|
|
* @control:
|
|
* @id:
|
|
*
|
|
* Returns: (transfer none):
|
|
*/
|
|
GvcMixerCard *
|
|
gvc_mixer_control_lookup_card_id (GvcMixerControl *control,
|
|
guint id)
|
|
{
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
|
|
|
|
return gvc_mixer_control_lookup_id (control->priv->cards, id);
|
|
}
|
|
|
|
/**
|
|
* gvc_mixer_control_lookup_output_id:
|
|
* @control:
|
|
* @id:
|
|
*
|
|
* Returns: (transfer none):
|
|
*/
|
|
GvcMixerUIDevice *
|
|
gvc_mixer_control_lookup_output_id (GvcMixerControl *control,
|
|
guint id)
|
|
{
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
|
|
|
|
return gvc_mixer_control_lookup_id (control->priv->ui_outputs, id);
|
|
}
|
|
|
|
/**
|
|
* gvc_mixer_control_lookup_input_id:
|
|
* @control:
|
|
* @id:
|
|
*
|
|
* Returns: (transfer none):
|
|
*/
|
|
GvcMixerUIDevice *
|
|
gvc_mixer_control_lookup_input_id (GvcMixerControl *control,
|
|
guint id)
|
|
{
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
|
|
|
|
return gvc_mixer_control_lookup_id (control->priv->ui_inputs, id);
|
|
}
|
|
|
|
/**
|
|
* gvc_mixer_control_get_stream_from_device:
|
|
* @control:
|
|
* @device:
|
|
*
|
|
* Returns: (transfer none):
|
|
*/
|
|
GvcMixerStream *
|
|
gvc_mixer_control_get_stream_from_device (GvcMixerControl *control,
|
|
GvcMixerUIDevice *device)
|
|
{
|
|
gint stream_id;
|
|
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
|
|
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
|
|
|
|
stream_id = gvc_mixer_ui_device_get_stream_id (device);
|
|
|
|
if (stream_id == GVC_MIXER_UI_DEVICE_INVALID) {
|
|
g_debug ("gvc_mixer_control_get_stream_from_device - device has a null stream");
|
|
return NULL;
|
|
}
|
|
return gvc_mixer_control_lookup_stream_id (control, stream_id);
|
|
}
|
|
|
|
/**
|
|
* gvc_mixer_control_change_profile_on_selected_device:
|
|
* @control:
|
|
* @device:
|
|
* @profile: (allow-none): Can be %NULL if any profile present on this port is okay
|
|
*
|
|
* Returns: This method will attempt to swap the profile on the card of
|
|
* the device with given profile name. If successfull it will set the
|
|
* preferred profile on that device so as we know the next time the user
|
|
* moves to that device it should have this profile active.
|
|
*/
|
|
gboolean
|
|
gvc_mixer_control_change_profile_on_selected_device (GvcMixerControl *control,
|
|
GvcMixerUIDevice *device,
|
|
const gchar *profile)
|
|
{
|
|
const gchar *best_profile;
|
|
GvcMixerCardProfile *current_profile;
|
|
GvcMixerCard *card;
|
|
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
|
|
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE);
|
|
|
|
g_object_get (G_OBJECT (device), "card", &card, NULL);
|
|
current_profile = gvc_mixer_card_get_profile (card);
|
|
|
|
if (current_profile)
|
|
best_profile = gvc_mixer_ui_device_get_best_profile (device, profile, current_profile->profile);
|
|
else
|
|
best_profile = profile;
|
|
|
|
g_assert (best_profile);
|
|
|
|
g_debug ("Selected '%s', moving to profile '%s' on card '%s' on stream id %i",
|
|
profile ? profile : "(any)", best_profile,
|
|
gvc_mixer_card_get_name (card),
|
|
gvc_mixer_ui_device_get_stream_id (device));
|
|
|
|
g_debug ("default sink name = %s and default sink id %u",
|
|
control->priv->default_sink_name,
|
|
control->priv->default_sink_id);
|
|
|
|
control->priv->profile_swapping_device_id = gvc_mixer_ui_device_get_id (device);
|
|
|
|
if (gvc_mixer_card_change_profile (card, best_profile)) {
|
|
gvc_mixer_ui_device_set_user_preferred_profile (device, best_profile);
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* gvc_mixer_control_change_output:
|
|
* @control:
|
|
* @output:
|
|
* This method is called from the UI when the user selects a previously unselected device.
|
|
* - Firstly it queries the stream from the device.
|
|
* - It assumes that if the stream is null that it cannot be a bluetooth or network stream (they never show unless they have valid sinks and sources)
|
|
* In the scenario of a NULL stream on the device
|
|
* - It fetches the device's preferred profile or if NUll the profile with the highest priority on that device.
|
|
* - It then caches this device in control->priv->cached_desired_output_id so that when the update_sink triggered
|
|
* from when we attempt to change profile we will know exactly what device to highlight on that stream.
|
|
* - It attempts to swap the profile on the card from that device and returns.
|
|
* - Next, it handles network or bluetooth streams that only require their stream to be made the default.
|
|
* - Next it deals with port changes so if the stream's active port is not the same as the port on the device
|
|
* it will attempt to change the port on that stream to be same as the device. If this fails it will return.
|
|
* - Finally it will set this new stream to be the default stream and emit a signal for the UI confirming the active output device.
|
|
*/
|
|
void
|
|
gvc_mixer_control_change_output (GvcMixerControl *control,
|
|
GvcMixerUIDevice* output)
|
|
{
|
|
GvcMixerStream *stream;
|
|
GvcMixerStream *default_stream;
|
|
const GvcMixerStreamPort *active_port;
|
|
const gchar *output_port;
|
|
|
|
g_return_if_fail (GVC_IS_MIXER_CONTROL (control));
|
|
g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (output));
|
|
|
|
g_debug ("control change output");
|
|
|
|
stream = gvc_mixer_control_get_stream_from_device (control, output);
|
|
if (stream == NULL) {
|
|
gvc_mixer_control_change_profile_on_selected_device (control,
|
|
output, NULL);
|
|
return;
|
|
}
|
|
|
|
/* Handle a network sink as a portless or cardless device */
|
|
if (!gvc_mixer_ui_device_has_ports (output)) {
|
|
g_debug ("Did we try to move to a software/bluetooth sink ?");
|
|
if (gvc_mixer_control_set_default_sink (control, stream)) {
|
|
/* sink change was successful, update the UI.*/
|
|
g_signal_emit (G_OBJECT (control),
|
|
signals[ACTIVE_OUTPUT_UPDATE],
|
|
0,
|
|
gvc_mixer_ui_device_get_id (output));
|
|
}
|
|
else {
|
|
g_warning ("Failed to set default sink with stream from output %s",
|
|
gvc_mixer_ui_device_get_description (output));
|
|
}
|
|
return;
|
|
}
|
|
|
|
active_port = gvc_mixer_stream_get_port (stream);
|
|
output_port = gvc_mixer_ui_device_get_port (output);
|
|
/* First ensure the correct port is active on the sink */
|
|
if (g_strcmp0 (active_port->port, output_port) != 0) {
|
|
g_debug ("Port change, switch to = %s", output_port);
|
|
if (gvc_mixer_stream_change_port (stream, output_port) == FALSE) {
|
|
g_warning ("Could not change port !");
|
|
return;
|
|
}
|
|
}
|
|
|
|
default_stream = gvc_mixer_control_get_default_sink (control);
|
|
|
|
/* Finally if we are not on the correct stream, swap over. */
|
|
if (stream != default_stream) {
|
|
GvcMixerUIDevice* device;
|
|
|
|
g_debug ("Attempting to swap over to stream %s ",
|
|
gvc_mixer_stream_get_description (stream));
|
|
if (gvc_mixer_control_set_default_sink (control, stream)) {
|
|
device = gvc_mixer_control_lookup_device_from_stream (control, stream);
|
|
g_signal_emit (G_OBJECT (control),
|
|
signals[ACTIVE_OUTPUT_UPDATE],
|
|
0,
|
|
gvc_mixer_ui_device_get_id (device));
|
|
} else {
|
|
/* If the move failed for some reason reset the UI. */
|
|
device = gvc_mixer_control_lookup_device_from_stream (control, default_stream);
|
|
g_signal_emit (G_OBJECT (control),
|
|
signals[ACTIVE_OUTPUT_UPDATE],
|
|
0,
|
|
gvc_mixer_ui_device_get_id (device));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* gvc_mixer_control_change_input:
|
|
* @control:
|
|
* @input:
|
|
* This method is called from the UI when the user selects a previously unselected device.
|
|
* - Firstly it queries the stream from the device.
|
|
* - It assumes that if the stream is null that it cannot be a bluetooth or network stream (they never show unless they have valid sinks and sources)
|
|
* In the scenario of a NULL stream on the device
|
|
* - It fetches the device's preferred profile or if NUll the profile with the highest priority on that device.
|
|
* - It then caches this device in control->priv->cached_desired_input_id so that when the update_source triggered
|
|
* from when we attempt to change profile we will know exactly what device to highlight on that stream.
|
|
* - It attempts to swap the profile on the card from that device and returns.
|
|
* - Next, it handles network or bluetooth streams that only require their stream to be made the default.
|
|
* - Next it deals with port changes so if the stream's active port is not the same as the port on the device
|
|
* it will attempt to change the port on that stream to be same as the device. If this fails it will return.
|
|
* - Finally it will set this new stream to be the default stream and emit a signal for the UI confirming the active input device.
|
|
*/
|
|
void
|
|
gvc_mixer_control_change_input (GvcMixerControl *control,
|
|
GvcMixerUIDevice* input)
|
|
{
|
|
GvcMixerStream *stream;
|
|
GvcMixerStream *default_stream;
|
|
const GvcMixerStreamPort *active_port;
|
|
const gchar *input_port;
|
|
|
|
g_return_if_fail (GVC_IS_MIXER_CONTROL (control));
|
|
g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (input));
|
|
|
|
stream = gvc_mixer_control_get_stream_from_device (control, input);
|
|
if (stream == NULL) {
|
|
gvc_mixer_control_change_profile_on_selected_device (control,
|
|
input, NULL);
|
|
return;
|
|
}
|
|
|
|
/* Handle a network sink as a portless/cardless device */
|
|
if (!gvc_mixer_ui_device_has_ports (input)) {
|
|
g_debug ("Did we try to move to a software/bluetooth source ?");
|
|
if (! gvc_mixer_control_set_default_source (control, stream)) {
|
|
g_warning ("Failed to set default source with stream from input %s",
|
|
gvc_mixer_ui_device_get_description (input));
|
|
}
|
|
return;
|
|
}
|
|
|
|
active_port = gvc_mixer_stream_get_port (stream);
|
|
input_port = gvc_mixer_ui_device_get_port (input);
|
|
/* First ensure the correct port is active on the sink */
|
|
if (g_strcmp0 (active_port->port, input_port) != 0) {
|
|
g_debug ("Port change, switch to = %s", input_port);
|
|
if (gvc_mixer_stream_change_port (stream, input_port) == FALSE) {
|
|
g_warning ("Could not change port!");
|
|
return;
|
|
}
|
|
}
|
|
|
|
default_stream = gvc_mixer_control_get_default_source (control);
|
|
|
|
/* Finally if we are not on the correct stream, swap over. */
|
|
if (stream != default_stream) {
|
|
g_debug ("change-input - attempting to swap over to stream %s",
|
|
gvc_mixer_stream_get_description (stream));
|
|
gvc_mixer_control_set_default_source (control, stream);
|
|
}
|
|
}
|
|
|
|
|
|
static void
|
|
listify_hash_values_hfunc (gpointer key,
|
|
gpointer value,
|
|
gpointer user_data)
|
|
{
|
|
GSList **list = user_data;
|
|
|
|
*list = g_slist_prepend (*list, value);
|
|
}
|
|
|
|
static int
|
|
gvc_name_collate (const char *namea,
|
|
const char *nameb)
|
|
{
|
|
if (nameb == NULL && namea == NULL)
|
|
return 0;
|
|
if (nameb == NULL)
|
|
return 1;
|
|
if (namea == NULL)
|
|
return -1;
|
|
|
|
return g_utf8_collate (namea, nameb);
|
|
}
|
|
|
|
static int
|
|
gvc_card_collate (GvcMixerCard *a,
|
|
GvcMixerCard *b)
|
|
{
|
|
const char *namea;
|
|
const char *nameb;
|
|
|
|
g_return_val_if_fail (a == NULL || GVC_IS_MIXER_CARD (a), 0);
|
|
g_return_val_if_fail (b == NULL || GVC_IS_MIXER_CARD (b), 0);
|
|
|
|
namea = gvc_mixer_card_get_name (a);
|
|
nameb = gvc_mixer_card_get_name (b);
|
|
|
|
return gvc_name_collate (namea, nameb);
|
|
}
|
|
|
|
/**
|
|
* gvc_mixer_control_get_cards:
|
|
* @control:
|
|
*
|
|
* Returns: (transfer container) (element-type Gvc.MixerCard):
|
|
*/
|
|
GSList *
|
|
gvc_mixer_control_get_cards (GvcMixerControl *control)
|
|
{
|
|
GSList *retval;
|
|
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
|
|
|
|
retval = NULL;
|
|
g_hash_table_foreach (control->priv->cards,
|
|
listify_hash_values_hfunc,
|
|
&retval);
|
|
return g_slist_sort (retval, (GCompareFunc) gvc_card_collate);
|
|
}
|
|
|
|
static int
|
|
gvc_stream_collate (GvcMixerStream *a,
|
|
GvcMixerStream *b)
|
|
{
|
|
const char *namea;
|
|
const char *nameb;
|
|
|
|
g_return_val_if_fail (a == NULL || GVC_IS_MIXER_STREAM (a), 0);
|
|
g_return_val_if_fail (b == NULL || GVC_IS_MIXER_STREAM (b), 0);
|
|
|
|
namea = gvc_mixer_stream_get_name (a);
|
|
nameb = gvc_mixer_stream_get_name (b);
|
|
|
|
return gvc_name_collate (namea, nameb);
|
|
}
|
|
|
|
/**
|
|
* gvc_mixer_control_get_streams:
|
|
* @control:
|
|
*
|
|
* Returns: (transfer container) (element-type Gvc.MixerStream):
|
|
*/
|
|
GSList *
|
|
gvc_mixer_control_get_streams (GvcMixerControl *control)
|
|
{
|
|
GSList *retval;
|
|
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
|
|
|
|
retval = NULL;
|
|
g_hash_table_foreach (control->priv->all_streams,
|
|
listify_hash_values_hfunc,
|
|
&retval);
|
|
return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
|
|
}
|
|
|
|
/**
|
|
* gvc_mixer_control_get_sinks:
|
|
* @control:
|
|
*
|
|
* Returns: (transfer container) (element-type Gvc.MixerSink):
|
|
*/
|
|
GSList *
|
|
gvc_mixer_control_get_sinks (GvcMixerControl *control)
|
|
{
|
|
GSList *retval;
|
|
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
|
|
|
|
retval = NULL;
|
|
g_hash_table_foreach (control->priv->sinks,
|
|
listify_hash_values_hfunc,
|
|
&retval);
|
|
return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
|
|
}
|
|
|
|
/**
|
|
* gvc_mixer_control_get_sources:
|
|
* @control:
|
|
*
|
|
* Returns: (transfer container) (element-type Gvc.MixerSource):
|
|
*/
|
|
GSList *
|
|
gvc_mixer_control_get_sources (GvcMixerControl *control)
|
|
{
|
|
GSList *retval;
|
|
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
|
|
|
|
retval = NULL;
|
|
g_hash_table_foreach (control->priv->sources,
|
|
listify_hash_values_hfunc,
|
|
&retval);
|
|
return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
|
|
}
|
|
|
|
/**
|
|
* gvc_mixer_control_get_sink_inputs:
|
|
* @control:
|
|
*
|
|
* Returns: (transfer container) (element-type Gvc.MixerSinkInput):
|
|
*/
|
|
GSList *
|
|
gvc_mixer_control_get_sink_inputs (GvcMixerControl *control)
|
|
{
|
|
GSList *retval;
|
|
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
|
|
|
|
retval = NULL;
|
|
g_hash_table_foreach (control->priv->sink_inputs,
|
|
listify_hash_values_hfunc,
|
|
&retval);
|
|
return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
|
|
}
|
|
|
|
/**
|
|
* gvc_mixer_control_get_source_outputs:
|
|
* @control:
|
|
*
|
|
* Returns: (transfer container) (element-type Gvc.MixerSourceOutput):
|
|
*/
|
|
GSList *
|
|
gvc_mixer_control_get_source_outputs (GvcMixerControl *control)
|
|
{
|
|
GSList *retval;
|
|
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
|
|
|
|
retval = NULL;
|
|
g_hash_table_foreach (control->priv->source_outputs,
|
|
listify_hash_values_hfunc,
|
|
&retval);
|
|
return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
|
|
}
|
|
|
|
static void
|
|
dec_outstanding (GvcMixerControl *control)
|
|
{
|
|
if (control->priv->n_outstanding <= 0) {
|
|
return;
|
|
}
|
|
|
|
if (--control->priv->n_outstanding <= 0) {
|
|
control->priv->state = GVC_STATE_READY;
|
|
g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_READY);
|
|
}
|
|
}
|
|
|
|
GvcMixerControlState
|
|
gvc_mixer_control_get_state (GvcMixerControl *control)
|
|
{
|
|
g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), GVC_STATE_CLOSED);
|
|
|
|
return control->priv->state;
|
|
}
|
|
|
|
static void
|
|
on_default_source_port_notify (GObject *object,
|
|
GParamSpec *pspec,
|
|
GvcMixerControl *control)
|
|
{
|
|
char *port;
|
|
GvcMixerUIDevice *input;
|
|
|
|
g_object_get (object, "port", &port, NULL);
|
|
input = gvc_mixer_control_lookup_device_from_stream (control,
|
|
GVC_MIXER_STREAM (object));
|
|
|
|
g_debug ("on_default_source_port_notify - moved to port '%s' which SHOULD ?? correspond to output '%s'",
|
|
port,
|
|
gvc_mixer_ui_device_get_description (input));
|
|
|
|
g_signal_emit (G_OBJECT (control),
|
|
signals[ACTIVE_INPUT_UPDATE],
|
|
0,
|
|
gvc_mixer_ui_device_get_id (input));
|
|
|
|
g_free (port);
|
|
}
|
|
|
|
|
|
static void
|
|
_set_default_source (GvcMixerControl *control,
|
|
GvcMixerStream *stream)
|
|
{
|
|
guint new_id;
|
|
|
|
if (stream == NULL) {
|
|
control->priv->default_source_id = 0;
|
|
control->priv->default_source_is_set = FALSE;
|
|
g_signal_emit (control,
|
|
signals[DEFAULT_SOURCE_CHANGED],
|
|
0,
|
|
PA_INVALID_INDEX);
|
|
return;
|
|
}
|
|
|
|
new_id = gvc_mixer_stream_get_id (stream);
|
|
|
|
if (control->priv->default_source_id != new_id) {
|
|
GvcMixerUIDevice *input;
|
|
control->priv->default_source_id = new_id;
|
|
control->priv->default_source_is_set = TRUE;
|
|
g_signal_emit (control,
|
|
signals[DEFAULT_SOURCE_CHANGED],
|
|
0,
|
|
new_id);
|
|
|
|
if (control->priv->default_source_is_set) {
|
|
g_signal_handlers_disconnect_by_func (gvc_mixer_control_get_default_source (control),
|
|
on_default_source_port_notify,
|
|
control);
|
|
}
|
|
|
|
g_signal_connect (stream,
|
|
"notify::port",
|
|
G_CALLBACK (on_default_source_port_notify),
|
|
control);
|
|
|
|
input = gvc_mixer_control_lookup_device_from_stream (control, stream);
|
|
|
|
g_signal_emit (G_OBJECT (control),
|
|
signals[ACTIVE_INPUT_UPDATE],
|
|
0,
|
|
gvc_mixer_ui_device_get_id (input));
|
|
}
|
|
}
|
|
|
|
static void
|
|
on_default_sink_port_notify (GObject *object,
|
|
GParamSpec *pspec,
|
|
GvcMixerControl *control)
|
|
{
|
|
char *port;
|
|
GvcMixerUIDevice *output;
|
|
|
|
g_object_get (object, "port", &port, NULL);
|
|
|
|
output = gvc_mixer_control_lookup_device_from_stream (control,
|
|
GVC_MIXER_STREAM (object));
|
|
if (output != NULL) {
|
|
g_debug ("on_default_sink_port_notify - moved to port %s - which SHOULD correspond to output %s",
|
|
port,
|
|
gvc_mixer_ui_device_get_description (output));
|
|
g_signal_emit (G_OBJECT (control),
|
|
signals[ACTIVE_OUTPUT_UPDATE],
|
|
0,
|
|
gvc_mixer_ui_device_get_id (output));
|
|
}
|
|
g_free (port);
|
|
}
|
|
|
|
static void
|
|
_set_default_sink (GvcMixerControl *control,
|
|
GvcMixerStream *stream)
|
|
{
|
|
guint new_id;
|
|
|
|
if (stream == NULL) {
|
|
/* Don't tell front-ends about an unset default
|
|
* sink if it's already unset */
|
|
if (control->priv->default_sink_is_set == FALSE)
|
|
return;
|
|
control->priv->default_sink_id = 0;
|
|
control->priv->default_sink_is_set = FALSE;
|
|
g_signal_emit (control,
|
|
signals[DEFAULT_SINK_CHANGED],
|
|
0,
|
|
PA_INVALID_INDEX);
|
|
return;
|
|
}
|
|
|
|
new_id = gvc_mixer_stream_get_id (stream);
|
|
|
|
if (control->priv->default_sink_id != new_id) {
|
|
GvcMixerUIDevice *output;
|
|
if (control->priv->default_sink_is_set) {
|
|
g_signal_handlers_disconnect_by_func (gvc_mixer_control_get_default_sink (control),
|
|
on_default_sink_port_notify,
|
|
control);
|
|
}
|
|
|
|
control->priv->default_sink_id = new_id;
|
|
|
|
control->priv->default_sink_is_set = TRUE;
|
|
g_signal_emit (control,
|
|
signals[DEFAULT_SINK_CHANGED],
|
|
0,
|
|
new_id);
|
|
|
|
g_signal_connect (stream,
|
|
"notify::port",
|
|
G_CALLBACK (on_default_sink_port_notify),
|
|
control);
|
|
|
|
output = gvc_mixer_control_lookup_device_from_stream (control, stream);
|
|
|
|
g_debug ("active_sink change");
|
|
|
|
g_signal_emit (G_OBJECT (control),
|
|
signals[ACTIVE_OUTPUT_UPDATE],
|
|
0,
|
|
gvc_mixer_ui_device_get_id (output));
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
_stream_has_name (gpointer key,
|
|
GvcMixerStream *stream,
|
|
const char *name)
|
|
{
|
|
const char *t_name;
|
|
|
|
t_name = gvc_mixer_stream_get_name (stream);
|
|
|
|
if (t_name != NULL
|
|
&& name != NULL
|
|
&& strcmp (t_name, name) == 0) {
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static GvcMixerStream *
|
|
find_stream_for_name (GvcMixerControl *control,
|
|
const char *name)
|
|
{
|
|
GvcMixerStream *stream;
|
|
|
|
stream = g_hash_table_find (control->priv->all_streams,
|
|
(GHRFunc)_stream_has_name,
|
|
(char *)name);
|
|
return stream;
|
|
}
|
|
|
|
static void
|
|
update_default_source_from_name (GvcMixerControl *control,
|
|
const char *name)
|
|
{
|
|
gboolean changed = FALSE;
|
|
|
|
if ((control->priv->default_source_name == NULL
|
|
&& name != NULL)
|
|
|| (control->priv->default_source_name != NULL
|
|
&& name == NULL)
|
|
|| (name != NULL && strcmp (control->priv->default_source_name, name) != 0)) {
|
|
changed = TRUE;
|
|
}
|
|
|
|
if (changed) {
|
|
GvcMixerStream *stream;
|
|
|
|
g_free (control->priv->default_source_name);
|
|
control->priv->default_source_name = g_strdup (name);
|
|
|
|
stream = find_stream_for_name (control, name);
|
|
_set_default_source (control, stream);
|
|
}
|
|
}
|
|
|
|
static void
|
|
update_default_sink_from_name (GvcMixerControl *control,
|
|
const char *name)
|
|
{
|
|
gboolean changed = FALSE;
|
|
|
|
if ((control->priv->default_sink_name == NULL
|
|
&& name != NULL)
|
|
|| (control->priv->default_sink_name != NULL
|
|
&& name == NULL)
|
|
|| (name != NULL && strcmp (control->priv->default_sink_name, name) != 0)) {
|
|
changed = TRUE;
|
|
}
|
|
|
|
if (changed) {
|
|
GvcMixerStream *stream;
|
|
g_free (control->priv->default_sink_name);
|
|
control->priv->default_sink_name = g_strdup (name);
|
|
|
|
stream = find_stream_for_name (control, name);
|
|
_set_default_sink (control, stream);
|
|
}
|
|
}
|
|
|
|
static void
|
|
update_server (GvcMixerControl *control,
|
|
const pa_server_info *info)
|
|
{
|
|
if (info->default_source_name != NULL) {
|
|
update_default_source_from_name (control, info->default_source_name);
|
|
}
|
|
if (info->default_sink_name != NULL) {
|
|
g_debug ("update server");
|
|
update_default_sink_from_name (control, info->default_sink_name);
|
|
}
|
|
}
|
|
|
|
static void
|
|
remove_stream (GvcMixerControl *control,
|
|
GvcMixerStream *stream)
|
|
{
|
|
guint id;
|
|
|
|
g_object_ref (stream);
|
|
|
|
id = gvc_mixer_stream_get_id (stream);
|
|
|
|
if (id == control->priv->default_sink_id) {
|
|
_set_default_sink (control, NULL);
|
|
} else if (id == control->priv->default_source_id) {
|
|
_set_default_source (control, NULL);
|
|
}
|
|
|
|
g_hash_table_remove (control->priv->all_streams,
|
|
GUINT_TO_POINTER (id));
|
|
g_signal_emit (G_OBJECT (control),
|
|
signals[STREAM_REMOVED],
|
|
0,
|
|
gvc_mixer_stream_get_id (stream));
|
|
g_object_unref (stream);
|
|
}
|
|
|
|
static void
|
|
add_stream (GvcMixerControl *control,
|
|
GvcMixerStream *stream)
|
|
{
|
|
g_hash_table_insert (control->priv->all_streams,
|
|
GUINT_TO_POINTER (gvc_mixer_stream_get_id (stream)),
|
|
stream);
|
|
g_signal_emit (G_OBJECT (control),
|
|
signals[STREAM_ADDED],
|
|
0,
|
|
gvc_mixer_stream_get_id (stream));
|
|
}
|
|
|
|
/* This method will match individual stream ports against its corresponding device
|
|
* It does this by:
|
|
* - iterates through our devices and finds the one where the card-id on the device is the same as the card-id on the stream
|
|
* and the port-name on the device is the same as the streamport-name.
|
|
* This should always find a match and is used exclusively by sync_devices().
|
|
*/
|
|
static gboolean
|
|
match_stream_with_devices (GvcMixerControl *control,
|
|
GvcMixerStreamPort *stream_port,
|
|
GvcMixerStream *stream)
|
|
{
|
|
GList *devices, *d;
|
|
guint stream_card_id;
|
|
guint stream_id;
|
|
gboolean in_possession = FALSE;
|
|
|
|
stream_id = gvc_mixer_stream_get_id (stream);
|
|
stream_card_id = gvc_mixer_stream_get_card_index (stream);
|
|
|
|
devices = g_hash_table_get_values (GVC_IS_MIXER_SOURCE (stream) ? control->priv->ui_inputs : control->priv->ui_outputs);
|
|
|
|
for (d = devices; d != NULL; d = d->next) {
|
|
GvcMixerUIDevice *device;
|
|
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)
|
|
{
|
|
g_return_if_fail (GVC_IS_MIXER_CONTROL (control));
|
|
|
|
#ifdef HAVE_ALSA
|
|
switch (choice) {
|
|
case GVC_HEADSET_PORT_CHOICE_HEADPHONES:
|
|
gvc_mixer_control_set_port_status_for_headset (control, id, "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 ("Updating card %s (index: %u driver: %s):",
|
|
info->name, info->index, info->driver);
|
|
|
|
for (i = 0; i < info->n_profiles; i++) {
|
|
struct pa_card_profile_info pi = info->profiles[i];
|
|
gboolean is_default;
|
|
|
|
is_default = (g_strcmp0 (pi.name, info->active_profile->name) == 0);
|
|
g_debug ("\tProfile '%s': %d sources %d sinks%s",
|
|
pi.name, pi.n_sources, pi.n_sinks,
|
|
is_default ? " (Current)" : "");
|
|
}
|
|
state = NULL;
|
|
key = pa_proplist_iterate (info->proplist, &state);
|
|
while (key != NULL) {
|
|
g_debug ("\tProperty: '%s' = '%s'",
|
|
key, pa_proplist_gets (info->proplist, key));
|
|
key = pa_proplist_iterate (info->proplist, &state);
|
|
}
|
|
#endif
|
|
card = g_hash_table_lookup (control->priv->cards,
|
|
GUINT_TO_POINTER (info->index));
|
|
if (card == NULL) {
|
|
GList *profile_list = NULL;
|
|
GList *port_list = NULL;
|
|
|
|
for (i = 0; i < info->n_profiles; i++) {
|
|
GvcMixerCardProfile *profile;
|
|
struct pa_card_profile_info pi = info->profiles[i];
|
|
|
|
profile = g_new0 (GvcMixerCardProfile, 1);
|
|
profile->profile = g_strdup (pi.name);
|
|
profile->human_profile = g_strdup (pi.description);
|
|
profile->status = card_num_streams_to_status (pi.n_sinks, pi.n_sources);
|
|
profile->n_sinks = pi.n_sinks;
|
|
profile->n_sources = pi.n_sources;
|
|
profile->priority = pi.priority;
|
|
profile_list = g_list_prepend (profile_list, profile);
|
|
}
|
|
card = gvc_mixer_card_new (control->priv->pa_context,
|
|
info->index);
|
|
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, "emblem-system-symbolic");
|
|
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);
|
|
}
|
|
|
|
|
|
static void
|
|
gvc_mixer_control_init (GvcMixerControl *control)
|
|
{
|
|
control->priv = gvc_mixer_control_get_instance_private (control);
|
|
|
|
control->priv->pa_mainloop = pa_glib_mainloop_new (g_main_context_default ());
|
|
g_assert (control->priv->pa_mainloop);
|
|
|
|
control->priv->pa_api = pa_glib_mainloop_get_api (control->priv->pa_mainloop);
|
|
g_assert (control->priv->pa_api);
|
|
|
|
control->priv->all_streams = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
|
|
control->priv->sinks = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
|
|
control->priv->sources = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
|
|
control->priv->sink_inputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
|
|
control->priv->source_outputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
|
|
control->priv->cards = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
|
|
control->priv->ui_outputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
|
|
control->priv->ui_inputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
|
|
|
|
control->priv->clients = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_free);
|
|
|
|
#ifdef HAVE_ALSA
|
|
control->priv->headset_card = -1;
|
|
#endif /* HAVE_ALSA */
|
|
|
|
control->priv->state = GVC_STATE_CLOSED;
|
|
}
|
|
|
|
static void
|
|
gvc_mixer_control_finalize (GObject *object)
|
|
{
|
|
GvcMixerControl *mixer_control;
|
|
|
|
g_return_if_fail (object != NULL);
|
|
g_return_if_fail (GVC_IS_MIXER_CONTROL (object));
|
|
|
|
mixer_control = GVC_MIXER_CONTROL (object);
|
|
g_free (mixer_control->priv->name);
|
|
mixer_control->priv->name = NULL;
|
|
|
|
g_return_if_fail (mixer_control->priv != NULL);
|
|
G_OBJECT_CLASS (gvc_mixer_control_parent_class)->finalize (object);
|
|
}
|
|
|
|
GvcMixerControl *
|
|
gvc_mixer_control_new (const char *name)
|
|
{
|
|
GObject *control;
|
|
control = g_object_new (GVC_TYPE_MIXER_CONTROL,
|
|
"name", name,
|
|
NULL);
|
|
return GVC_MIXER_CONTROL (control);
|
|
}
|
|
|
|
gdouble
|
|
gvc_mixer_control_get_vol_max_norm (GvcMixerControl *control)
|
|
{
|
|
return (gdouble) PA_VOLUME_NORM;
|
|
}
|
|
|
|
gdouble
|
|
gvc_mixer_control_get_vol_max_amplified (GvcMixerControl *control)
|
|
{
|
|
return (gdouble) PA_VOLUME_UI_MAX;
|
|
}
|