diff options
-rw-r--r-- | Makefile.am | 65 | ||||
-rw-r--r-- | gvc-channel-bar.c | 974 | ||||
-rw-r--r-- | gvc-channel-bar.h | 89 | ||||
-rw-r--r-- | gvc-channel-map-private.h | 39 | ||||
-rw-r--r-- | gvc-channel-map.c | 254 | ||||
-rw-r--r-- | gvc-channel-map.h | 73 | ||||
-rw-r--r-- | gvc-mixer-card-private.h | 35 | ||||
-rw-r--r-- | gvc-mixer-card.c | 570 | ||||
-rw-r--r-- | gvc-mixer-card.h | 101 | ||||
-rw-r--r-- | gvc-mixer-control-private.h | 35 | ||||
-rw-r--r-- | gvc-mixer-control.c | 3358 | ||||
-rw-r--r-- | gvc-mixer-control.h | 136 | ||||
-rw-r--r-- | gvc-mixer-dialog.c | 1977 | ||||
-rw-r--r-- | gvc-mixer-dialog.h | 56 | ||||
-rw-r--r-- | gvc-mixer-event-role.c | 235 | ||||
-rw-r--r-- | gvc-mixer-event-role.h | 57 | ||||
-rw-r--r-- | gvc-mixer-sink-input.c | 166 | ||||
-rw-r--r-- | gvc-mixer-sink-input.h | 57 | ||||
-rw-r--r-- | gvc-mixer-sink.c | 196 | ||||
-rw-r--r-- | gvc-mixer-sink.h | 57 | ||||
-rw-r--r-- | gvc-mixer-source-output.c | 121 | ||||
-rw-r--r-- | gvc-mixer-source-output.h | 57 | ||||
-rw-r--r-- | gvc-mixer-source.c | 196 | ||||
-rw-r--r-- | gvc-mixer-source.h | 57 | ||||
-rw-r--r-- | gvc-mixer-stream-private.h | 34 | ||||
-rw-r--r-- | gvc-mixer-stream.c | 1006 | ||||
-rw-r--r-- | gvc-mixer-stream.h | 131 | ||||
-rw-r--r-- | gvc-mixer-ui-device.c | 656 | ||||
-rw-r--r-- | gvc-mixer-ui-device.h | 82 | ||||
-rw-r--r-- | gvc-pulseaudio-fake.h | 34 |
30 files changed, 10904 insertions, 0 deletions
diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..08ea266 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,65 @@ + +noinst_LTLIBRARIES = libgvc.la + +INTROSPECTION_SCANNER_ARGS = --warn-all + +libgvc_la_CPPFLAGS = \ + $(WARN_CFLAGS) \ + $(GVC_CFLAGS) \ + -I$(srcdir)/gvc/ \ + -DWITH_INTROSPECTION \ + -DG_LOG_DOMAIN="\"Gvc\"" + +libgvc_la_gir_sources = \ + gvc-mixer-card.h \ + gvc-mixer-card.c \ + gvc-mixer-stream.h \ + gvc-mixer-stream.c \ + gvc-channel-map.h \ + gvc-channel-map.c \ + gvc-mixer-ui-device.h \ + gvc-mixer-ui-device.c \ + gvc-mixer-sink.h \ + gvc-mixer-sink.c \ + gvc-mixer-source.h \ + gvc-mixer-source.c \ + gvc-mixer-sink-input.h \ + gvc-mixer-sink-input.c \ + gvc-mixer-source-output.h \ + gvc-mixer-source-output.c \ + gvc-mixer-event-role.h \ + gvc-mixer-event-role.c \ + gvc-mixer-control.h \ + gvc-mixer-control.c \ + gvc-channel-bar.h \ + gvc-channel-bar.c \ + $(NULL) + +libgvc_la_SOURCES = \ + $(libgvc_la_gir_sources) \ + gvc-mixer-card-private.h \ + gvc-mixer-stream-private.h \ + gvc-channel-map-private.h \ + gvc-mixer-control-private.h \ + gvc-pulseaudio-fake.h \ + $(NULL) + +libgvc_la_LIBADD = \ + $(GVC_LIBS) \ + $(NULL) + +if HAVE_INTROSPECTION +include $(INTROSPECTION_MAKEFILE) + +Gvc-1.0.gir: libgvc.la +Gvc_1_0_gir_INCLUDES = GObject-2.0 Gio-2.0 Gtk-3.0 +Gvc_1_0_gir_CFLAGS = $(INCLUDES) -I$(srcdir)/gvc -DWITH_INTROSPECTION +Gvc_1_0_gir_LIBS = libgvc.la +Gvc_1_0_gir_FILES = $(addprefix $(srcdir)/,$(libgvc_la_gir_sources)) +INTROSPECTION_GIRS = Gvc-1.0.gir +CLEANFILES = Gvc-1.0.gir + +typelibdir = $(pkglibdir) +typelib_DATA = $(INTROSPECTION_GIRS:.gir=.typelib) + +endif diff --git a/gvc-channel-bar.c b/gvc-channel-bar.c new file mode 100644 index 0000000..3d854c9 --- /dev/null +++ b/gvc-channel-bar.c @@ -0,0 +1,974 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * 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 <pulse/pulseaudio.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> +#include <gtk/gtk.h> +#include <canberra-gtk.h> + +#include "gvc-channel-bar.h" +#include "gvc-mixer-control.h" + +#define SCALE_SIZE 128 +#define ADJUSTMENT_MAX_NORMAL gvc_mixer_control_get_vol_max_norm(NULL) +#define ADJUSTMENT_MAX_AMPLIFIED gvc_mixer_control_get_vol_max_amplified(NULL) +#define ADJUSTMENT_MAX (bar->priv->is_amplified ? ADJUSTMENT_MAX_AMPLIFIED : ADJUSTMENT_MAX_NORMAL) +#define SCROLLSTEP (ADJUSTMENT_MAX / 100.0 * 5.0) + +#define GVC_CHANNEL_BAR_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GVC_TYPE_CHANNEL_BAR, GvcChannelBarPrivate)) + +struct GvcChannelBarPrivate +{ + GtkOrientation orientation; + GtkWidget *scale_box; + GtkWidget *start_box; + GtkWidget *end_box; + GtkWidget *image; + GtkWidget *label; + GtkWidget *low_image; + GtkWidget *scale; + GtkWidget *high_image; + GtkWidget *mute_box; + GtkWidget *mute_switch; + GtkAdjustment *adjustment; + GtkAdjustment *zero_adjustment; + gboolean show_mute; + gboolean is_muted; + char *name; + char *icon_name; + char *low_icon_name; + char *high_icon_name; + GtkSizeGroup *size_group; + gboolean symmetric; + gboolean click_lock; + gboolean is_amplified; + guint32 base_volume; +}; + +enum +{ + PROP_0, + PROP_ORIENTATION, + PROP_SHOW_MUTE, + PROP_IS_MUTED, + PROP_ADJUSTMENT, + PROP_NAME, + PROP_ICON_NAME, + PROP_LOW_ICON_NAME, + PROP_HIGH_ICON_NAME, + PROP_IS_AMPLIFIED, + PROP_ELLIPSIZE +}; + +static void gvc_channel_bar_class_init (GvcChannelBarClass *klass); +static void gvc_channel_bar_init (GvcChannelBar *channel_bar); +static void gvc_channel_bar_finalize (GObject *object); + +static gboolean on_scale_button_press_event (GtkWidget *widget, + GdkEventButton *event, + GvcChannelBar *bar); +static gboolean on_scale_button_release_event (GtkWidget *widget, + GdkEventButton *event, + GvcChannelBar *bar); +static gboolean on_scale_scroll_event (GtkWidget *widget, + GdkEventScroll *event, + GvcChannelBar *bar); + +G_DEFINE_TYPE (GvcChannelBar, gvc_channel_bar, GTK_TYPE_HBOX) + +static GtkWidget * +_scale_box_new (GvcChannelBar *bar) +{ + GvcChannelBarPrivate *priv = bar->priv; + GtkWidget *box; + GtkWidget *sbox; + GtkWidget *ebox; + + if (priv->orientation == GTK_ORIENTATION_VERTICAL) { + bar->priv->scale_box = box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); + + priv->scale = gtk_scale_new (GTK_ORIENTATION_VERTICAL, priv->adjustment); + + gtk_widget_set_size_request (priv->scale, -1, SCALE_SIZE); + gtk_range_set_inverted (GTK_RANGE (priv->scale), TRUE); + + bar->priv->start_box = sbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); + gtk_box_pack_start (GTK_BOX (box), sbox, FALSE, FALSE, 0); + + gtk_box_pack_start (GTK_BOX (sbox), priv->image, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (sbox), priv->label, FALSE, FALSE, 0); + + gtk_box_pack_start (GTK_BOX (sbox), priv->high_image, FALSE, FALSE, 0); + gtk_widget_hide (priv->high_image); + gtk_box_pack_start (GTK_BOX (box), priv->scale, TRUE, TRUE, 0); + + bar->priv->end_box = ebox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); + gtk_box_pack_start (GTK_BOX (box), ebox, FALSE, FALSE, 0); + + gtk_box_pack_start (GTK_BOX (ebox), priv->low_image, FALSE, FALSE, 0); + gtk_widget_hide (priv->low_image); + + gtk_box_pack_start (GTK_BOX (ebox), priv->mute_box, FALSE, FALSE, 0); + } else { + bar->priv->scale_box = box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start (GTK_BOX (box), priv->image, FALSE, FALSE, 0); + + priv->scale = gtk_scale_new (GTK_ORIENTATION_HORIZONTAL, priv->adjustment); + + gtk_widget_set_size_request (priv->scale, SCALE_SIZE, -1); + + bar->priv->start_box = sbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start (GTK_BOX (box), sbox, FALSE, FALSE, 0); + + gtk_box_pack_end (GTK_BOX (sbox), priv->low_image, FALSE, FALSE, 0); + gtk_widget_show (priv->low_image); + + gtk_box_pack_start (GTK_BOX (sbox), priv->label, TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (box), priv->scale, TRUE, TRUE, 0); + + bar->priv->end_box = ebox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start (GTK_BOX (box), ebox, FALSE, FALSE, 0); + + gtk_box_pack_start (GTK_BOX (ebox), priv->high_image, FALSE, FALSE, 0); + gtk_widget_show (priv->high_image); + gtk_box_pack_start (GTK_BOX (ebox), priv->mute_box, FALSE, FALSE, 0); + } + + ca_gtk_widget_disable_sounds (bar->priv->scale, FALSE); + gtk_widget_add_events (bar->priv->scale, GDK_SCROLL_MASK); + + g_signal_connect (G_OBJECT (bar->priv->scale), "button-press-event", + G_CALLBACK (on_scale_button_press_event), bar); + g_signal_connect (G_OBJECT (bar->priv->scale), "button-release-event", + G_CALLBACK (on_scale_button_release_event), bar); + g_signal_connect (G_OBJECT (bar->priv->scale), "scroll-event", + G_CALLBACK (on_scale_scroll_event), bar); + + if (bar->priv->size_group != NULL) { + gtk_size_group_add_widget (bar->priv->size_group, sbox); + + if (bar->priv->symmetric) { + gtk_size_group_add_widget (bar->priv->size_group, ebox); + } + } + + gtk_scale_set_draw_value (GTK_SCALE (priv->scale), FALSE); + + return box; +} + +static void +update_image (GvcChannelBar *bar) +{ + gtk_image_set_from_icon_name (GTK_IMAGE (bar->priv->image), + bar->priv->icon_name, + GTK_ICON_SIZE_DIALOG); + + if (bar->priv->icon_name != NULL) { + gtk_widget_show (bar->priv->image); + } else { + gtk_widget_hide (bar->priv->image); + } +} + +static void +update_label (GvcChannelBar *bar) +{ + if (bar->priv->name != NULL) { + gtk_label_set_text_with_mnemonic (GTK_LABEL (bar->priv->label), + bar->priv->name); + gtk_label_set_mnemonic_widget (GTK_LABEL (bar->priv->label), + bar->priv->scale); + gtk_widget_show (bar->priv->label); + } else { + gtk_label_set_text (GTK_LABEL (bar->priv->label), NULL); + gtk_widget_hide (bar->priv->label); + } +} + +static void +update_layout (GvcChannelBar *bar) +{ + GtkWidget *box; + GtkWidget *frame; + + if (bar->priv->scale == NULL) { + return; + } + + box = bar->priv->scale_box; + frame = gtk_widget_get_parent (box); + + g_object_ref (bar->priv->image); + g_object_ref (bar->priv->label); + g_object_ref (bar->priv->mute_box); + g_object_ref (bar->priv->low_image); + g_object_ref (bar->priv->high_image); + + gtk_container_remove (GTK_CONTAINER (bar->priv->start_box), bar->priv->image); + gtk_container_remove (GTK_CONTAINER (bar->priv->start_box), bar->priv->label); + gtk_container_remove (GTK_CONTAINER (bar->priv->end_box), bar->priv->mute_box); + + if (bar->priv->orientation == GTK_ORIENTATION_VERTICAL) { + gtk_container_remove (GTK_CONTAINER (bar->priv->start_box), bar->priv->low_image); + gtk_container_remove (GTK_CONTAINER (bar->priv->end_box), bar->priv->high_image); + } else { + gtk_container_remove (GTK_CONTAINER (bar->priv->end_box), bar->priv->low_image); + gtk_container_remove (GTK_CONTAINER (bar->priv->start_box), bar->priv->high_image); + } + + gtk_container_remove (GTK_CONTAINER (box), bar->priv->start_box); + gtk_container_remove (GTK_CONTAINER (box), bar->priv->scale); + gtk_container_remove (GTK_CONTAINER (box), bar->priv->end_box); + gtk_container_remove (GTK_CONTAINER (frame), box); + + bar->priv->scale_box = _scale_box_new (bar); + gtk_container_add (GTK_CONTAINER (frame), bar->priv->scale_box); + + g_object_unref (bar->priv->image); + g_object_unref (bar->priv->label); + g_object_unref (bar->priv->mute_box); + g_object_unref (bar->priv->low_image); + g_object_unref (bar->priv->high_image); + + gtk_widget_show_all (frame); +} + +void +gvc_channel_bar_set_size_group (GvcChannelBar *bar, + GtkSizeGroup *group, + gboolean symmetric) +{ + g_return_if_fail (GVC_IS_CHANNEL_BAR (bar)); + + bar->priv->size_group = group; + bar->priv->symmetric = symmetric; + + if (bar->priv->size_group != NULL) { + gtk_size_group_add_widget (bar->priv->size_group, + bar->priv->start_box); + + if (bar->priv->symmetric) { + gtk_size_group_add_widget (bar->priv->size_group, + bar->priv->end_box); + } + } + gtk_widget_queue_draw (GTK_WIDGET (bar)); +} + +void +gvc_channel_bar_set_name (GvcChannelBar *bar, + const char *name) +{ + g_return_if_fail (GVC_IS_CHANNEL_BAR (bar)); + + g_free (bar->priv->name); + bar->priv->name = g_strdup (name); + update_label (bar); + g_object_notify (G_OBJECT (bar), "name"); +} + +void +gvc_channel_bar_set_icon_name (GvcChannelBar *bar, + const char *name) +{ + g_return_if_fail (GVC_IS_CHANNEL_BAR (bar)); + + g_free (bar->priv->icon_name); + bar->priv->icon_name = g_strdup (name); + update_image (bar); + g_object_notify (G_OBJECT (bar), "icon-name"); +} + +void +gvc_channel_bar_set_low_icon_name (GvcChannelBar *bar, + const char *name) +{ + g_return_if_fail (GVC_IS_CHANNEL_BAR (bar)); + + if (name != NULL && strcmp (bar->priv->low_icon_name, name) != 0) { + g_free (bar->priv->low_icon_name); + bar->priv->low_icon_name = g_strdup (name); + gtk_image_set_from_icon_name (GTK_IMAGE (bar->priv->low_image), + bar->priv->low_icon_name, + GTK_ICON_SIZE_MENU); + g_object_notify (G_OBJECT (bar), "low-icon-name"); + } +} + +void +gvc_channel_bar_set_high_icon_name (GvcChannelBar *bar, + const char *name) +{ + g_return_if_fail (GVC_IS_CHANNEL_BAR (bar)); + + if (name != NULL && strcmp (bar->priv->high_icon_name, name) != 0) { + g_free (bar->priv->high_icon_name); + bar->priv->high_icon_name = g_strdup (name); + gtk_image_set_from_icon_name (GTK_IMAGE (bar->priv->high_image), + bar->priv->high_icon_name, + GTK_ICON_SIZE_MENU); + g_object_notify (G_OBJECT (bar), "high-icon-name"); + } +} + +void +gvc_channel_bar_set_orientation (GvcChannelBar *bar, + GtkOrientation orientation) +{ + g_return_if_fail (GVC_IS_CHANNEL_BAR (bar)); + + if (orientation != bar->priv->orientation) { + bar->priv->orientation = orientation; + update_layout (bar); + g_object_notify (G_OBJECT (bar), "orientation"); + } +} + +static void +gvc_channel_bar_set_adjustment (GvcChannelBar *bar, + GtkAdjustment *adjustment) +{ + g_return_if_fail (GVC_CHANNEL_BAR (bar)); + g_return_if_fail (GTK_IS_ADJUSTMENT (adjustment)); + + if (bar->priv->adjustment != NULL) { + g_object_unref (bar->priv->adjustment); + } + bar->priv->adjustment = g_object_ref_sink (adjustment); + + if (bar->priv->scale != NULL) { + gtk_range_set_adjustment (GTK_RANGE (bar->priv->scale), adjustment); + } + + g_object_notify (G_OBJECT (bar), "adjustment"); +} + +GtkAdjustment * +gvc_channel_bar_get_adjustment (GvcChannelBar *bar) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_BAR (bar), NULL); + + return bar->priv->adjustment; +} + +static gboolean +on_scale_button_press_event (GtkWidget *widget, + GdkEventButton *event, + GvcChannelBar *bar) +{ + bar->priv->click_lock = TRUE; + + return FALSE; +} + +static gboolean +on_scale_button_release_event (GtkWidget *widget, + GdkEventButton *event, + GvcChannelBar *bar) +{ + GtkAdjustment *adj; + gdouble value; + + bar->priv->click_lock = FALSE; + + adj = gtk_range_get_adjustment (GTK_RANGE (widget)); + + value = gtk_adjustment_get_value (adj); + + /* this means the adjustment moved away from zero and + * therefore we should unmute and set the volume. */ + gvc_channel_bar_set_is_muted (bar, (value == 0.0)); + + /* Play a sound! */ + ca_gtk_play_for_widget (GTK_WIDGET (bar), 0, + CA_PROP_EVENT_ID, "audio-volume-change", + CA_PROP_EVENT_DESCRIPTION, "foobar event happened", + CA_PROP_APPLICATION_ID, "org.gnome.VolumeControl", + NULL); + + return FALSE; +} + +gboolean +gvc_channel_bar_scroll (GvcChannelBar *bar, GdkEventScroll *event) +{ + GtkAdjustment *adj; + gdouble value; + GdkScrollDirection direction; + gdouble dx, dy; + + g_return_val_if_fail (bar != NULL, FALSE); + g_return_val_if_fail (GVC_IS_CHANNEL_BAR (bar), FALSE); + + direction = event->direction; + + if (bar->priv->orientation == GTK_ORIENTATION_VERTICAL) { + if (direction == GDK_SCROLL_LEFT || direction == GDK_SCROLL_RIGHT) + return FALSE; + } else { + /* Switch direction for RTL */ + if (gtk_widget_get_direction (GTK_WIDGET (bar)) == GTK_TEXT_DIR_RTL) { + if (direction == GDK_SCROLL_RIGHT) + direction = GDK_SCROLL_LEFT; + else if (direction == GDK_SCROLL_LEFT) + direction = GDK_SCROLL_RIGHT; + } + /* Switch side scroll to vertical */ + if (direction == GDK_SCROLL_RIGHT) + direction = GDK_SCROLL_UP; + else if (direction == GDK_SCROLL_LEFT) + direction = GDK_SCROLL_DOWN; + } + + if (!gdk_event_get_scroll_deltas ((GdkEvent*)event, &dx, &dy)) { + dx = 0.0; + dy = 0.0; + + switch (direction) { + case GDK_SCROLL_UP: + case GDK_SCROLL_LEFT: + dy = 1.0; + break; + case GDK_SCROLL_DOWN: + case GDK_SCROLL_RIGHT: + dy = -1.0; + break; + default: + ; + } + } + + adj = gtk_range_get_adjustment (GTK_RANGE (bar->priv->scale)); + if (adj == bar->priv->zero_adjustment) { + if (dy > 0) + gvc_channel_bar_set_is_muted (bar, FALSE); + return TRUE; + } + + value = gtk_adjustment_get_value (adj); + + if (dy > 0) { + if (value + dy * SCROLLSTEP > ADJUSTMENT_MAX) + value = ADJUSTMENT_MAX; + else + value = value + dy * SCROLLSTEP; + } else if (dy < 0) { + if (value + dy * SCROLLSTEP < 0) + value = 0.0; + else + value = value + dy * SCROLLSTEP; + } + + gvc_channel_bar_set_is_muted (bar, (value == 0.0)); + adj = gtk_range_get_adjustment (GTK_RANGE (bar->priv->scale)); + gtk_adjustment_set_value (adj, value); + + return TRUE; +} + +static gboolean +on_scale_scroll_event (GtkWidget *widget, + GdkEventScroll *event, + GvcChannelBar *bar) +{ + return gvc_channel_bar_scroll (bar, event); +} + +static void +on_zero_adjustment_value_changed (GtkAdjustment *adjustment, + GvcChannelBar *bar) +{ + gdouble value; + + if (bar->priv->click_lock != FALSE) { + return; + } + + value = gtk_adjustment_get_value (bar->priv->zero_adjustment); + gtk_adjustment_set_value (bar->priv->adjustment, value); + + + if (bar->priv->show_mute == FALSE) { + /* this means the adjustment moved away from zero and + * therefore we should unmute and set the volume. */ + gvc_channel_bar_set_is_muted (bar, value > 0.0); + } +} + +static void +update_mute_switch (GvcChannelBar *bar) +{ + if (bar->priv->show_mute) { + gtk_widget_show (bar->priv->mute_switch); + gtk_switch_set_active (GTK_SWITCH (bar->priv->mute_switch), + !bar->priv->is_muted); + } else { + gtk_widget_hide (bar->priv->mute_switch); + } + + if (bar->priv->is_muted) { + /* If we aren't showing the mute button then + * move slider to the zero. But we don't want to + * change the adjustment. */ + g_signal_handlers_block_by_func (bar->priv->zero_adjustment, + on_zero_adjustment_value_changed, + bar); + gtk_adjustment_set_value (bar->priv->zero_adjustment, 0); + g_signal_handlers_unblock_by_func (bar->priv->zero_adjustment, + on_zero_adjustment_value_changed, + bar); + gtk_range_set_adjustment (GTK_RANGE (bar->priv->scale), + bar->priv->zero_adjustment); + } else { + /* no longer muted so restore the original adjustment + * and tell the front-end that the value changed */ + gtk_range_set_adjustment (GTK_RANGE (bar->priv->scale), + bar->priv->adjustment); + gtk_adjustment_value_changed (bar->priv->adjustment); + } +} + +void +gvc_channel_bar_set_is_muted (GvcChannelBar *bar, + gboolean is_muted) +{ + g_return_if_fail (GVC_IS_CHANNEL_BAR (bar)); + + if (is_muted != bar->priv->is_muted) { + /* Update our internal state before telling the + * front-end about our changes */ + bar->priv->is_muted = is_muted; + update_mute_switch (bar); + g_object_notify (G_OBJECT (bar), "is-muted"); + } +} + +gboolean +gvc_channel_bar_get_is_muted (GvcChannelBar *bar) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_BAR (bar), FALSE); + return bar->priv->is_muted; +} + +void +gvc_channel_bar_set_show_mute (GvcChannelBar *bar, + gboolean show_mute) +{ + g_return_if_fail (GVC_IS_CHANNEL_BAR (bar)); + + if (show_mute != bar->priv->show_mute) { + bar->priv->show_mute = show_mute; + g_object_notify (G_OBJECT (bar), "show-mute"); + update_mute_switch (bar); + } +} + +gboolean +gvc_channel_bar_get_show_mute (GvcChannelBar *bar) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_BAR (bar), FALSE); + return bar->priv->show_mute; +} + +void +gvc_channel_bar_set_is_amplified (GvcChannelBar *bar, gboolean amplified) +{ + g_return_if_fail (GVC_IS_CHANNEL_BAR (bar)); + + bar->priv->is_amplified = amplified; + gtk_adjustment_set_upper (bar->priv->adjustment, ADJUSTMENT_MAX); + gtk_adjustment_set_upper (bar->priv->zero_adjustment, ADJUSTMENT_MAX); + gtk_scale_clear_marks (GTK_SCALE (bar->priv->scale)); + + if (amplified) { + char *str; + + if (bar->priv->base_volume == ADJUSTMENT_MAX_NORMAL) { + str = g_strdup_printf ("<small>%s</small>", C_("volume", "100%")); + gtk_scale_add_mark (GTK_SCALE (bar->priv->scale), ADJUSTMENT_MAX_NORMAL, + GTK_POS_BOTTOM, str); + } else { + str = g_strdup_printf ("<small>%s</small>", C_("volume", "Unamplified")); + gtk_scale_add_mark (GTK_SCALE (bar->priv->scale), bar->priv->base_volume, + GTK_POS_BOTTOM, str); + /* Only show 100% if it's higher than the base volume */ + if (bar->priv->base_volume < ADJUSTMENT_MAX_NORMAL) { + str = g_strdup_printf ("<small>%s</small>", C_("volume", "100%")); + gtk_scale_add_mark (GTK_SCALE (bar->priv->scale), ADJUSTMENT_MAX_NORMAL, + GTK_POS_BOTTOM, str); + } + } + + g_free (str); + gtk_alignment_set (GTK_ALIGNMENT (bar->priv->mute_box), 0.5, 0, 0, 0); + gtk_misc_set_alignment (GTK_MISC (bar->priv->low_image), 0.5, 0.15); + gtk_misc_set_alignment (GTK_MISC (bar->priv->high_image), 0.5, 0.15); + gtk_misc_set_alignment (GTK_MISC (bar->priv->label), 0, 0); + } else { + gtk_alignment_set (GTK_ALIGNMENT (bar->priv->mute_box), 0.5, 0.5, 0, 0); + gtk_misc_set_alignment (GTK_MISC (bar->priv->low_image), 0.5, 0.5); + gtk_misc_set_alignment (GTK_MISC (bar->priv->high_image), 0.5, 0.5); + gtk_misc_set_alignment (GTK_MISC (bar->priv->label), 0, 0.5); + } +} + +gboolean +gvc_channel_bar_get_ellipsize (GvcChannelBar *bar) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_BAR (bar), FALSE); + + return gtk_label_get_ellipsize (GTK_LABEL (bar->priv->label)) != PANGO_ELLIPSIZE_NONE; +} + +void +gvc_channel_bar_set_ellipsize (GvcChannelBar *bar, + gboolean ellipsized) +{ + g_return_if_fail (GVC_IS_CHANNEL_BAR (bar)); + + if (ellipsized) + gtk_label_set_ellipsize (GTK_LABEL (bar->priv->label), PANGO_ELLIPSIZE_END); + else + gtk_label_set_ellipsize (GTK_LABEL (bar->priv->label), PANGO_ELLIPSIZE_NONE); +} + +void +gvc_channel_bar_set_base_volume (GvcChannelBar *bar, + pa_volume_t base_volume) +{ + g_return_if_fail (GVC_IS_CHANNEL_BAR (bar)); + + if (base_volume == 0) { + bar->priv->base_volume = ADJUSTMENT_MAX_NORMAL; + return; + } + + /* Note that you need to call _is_amplified() afterwards to update the marks */ + bar->priv->base_volume = base_volume; +} + +static void +gvc_channel_bar_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GvcChannelBar *self = GVC_CHANNEL_BAR (object); + + switch (prop_id) { + case PROP_ORIENTATION: + gvc_channel_bar_set_orientation (self, g_value_get_enum (value)); + break; + case PROP_IS_MUTED: + gvc_channel_bar_set_is_muted (self, g_value_get_boolean (value)); + break; + case PROP_SHOW_MUTE: + gvc_channel_bar_set_show_mute (self, g_value_get_boolean (value)); + break; + case PROP_NAME: + gvc_channel_bar_set_name (self, g_value_get_string (value)); + break; + case PROP_ICON_NAME: + gvc_channel_bar_set_icon_name (self, g_value_get_string (value)); + break; + case PROP_LOW_ICON_NAME: + gvc_channel_bar_set_low_icon_name (self, g_value_get_string (value)); + break; + case PROP_HIGH_ICON_NAME: + gvc_channel_bar_set_high_icon_name (self, g_value_get_string (value)); + break; + case PROP_ADJUSTMENT: + gvc_channel_bar_set_adjustment (self, g_value_get_object (value)); + break; + case PROP_IS_AMPLIFIED: + gvc_channel_bar_set_is_amplified (self, g_value_get_boolean (value)); + break; + case PROP_ELLIPSIZE: + gvc_channel_bar_set_ellipsize (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gvc_channel_bar_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GvcChannelBar *self = GVC_CHANNEL_BAR (object); + GvcChannelBarPrivate *priv = self->priv; + + switch (prop_id) { + case PROP_ORIENTATION: + g_value_set_enum (value, priv->orientation); + break; + case PROP_IS_MUTED: + g_value_set_boolean (value, priv->is_muted); + break; + case PROP_SHOW_MUTE: + g_value_set_boolean (value, priv->show_mute); + break; + case PROP_NAME: + g_value_set_string (value, priv->name); + break; + case PROP_ICON_NAME: + g_value_set_string (value, priv->icon_name); + break; + case PROP_LOW_ICON_NAME: + g_value_set_string (value, priv->low_icon_name); + break; + case PROP_HIGH_ICON_NAME: + g_value_set_string (value, priv->high_icon_name); + break; + case PROP_ADJUSTMENT: + g_value_set_object (value, gvc_channel_bar_get_adjustment (self)); + break; + case PROP_IS_AMPLIFIED: + g_value_set_boolean (value, priv->is_amplified); + break; + case PROP_ELLIPSIZE: + g_value_set_boolean (value, gvc_channel_bar_get_ellipsize (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static GObject * +gvc_channel_bar_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_params) +{ + GObject *object; + GvcChannelBar *self; + + object = G_OBJECT_CLASS (gvc_channel_bar_parent_class)->constructor (type, n_construct_properties, construct_params); + + self = GVC_CHANNEL_BAR (object); + + update_mute_switch (self); + + return object; +} + +static void +gvc_channel_bar_class_init (GvcChannelBarClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructor = gvc_channel_bar_constructor; + object_class->finalize = gvc_channel_bar_finalize; + object_class->set_property = gvc_channel_bar_set_property; + object_class->get_property = gvc_channel_bar_get_property; + + g_object_class_install_property (object_class, + PROP_ORIENTATION, + g_param_spec_enum ("orientation", + "Orientation", + "The orientation of the scale", + GTK_TYPE_ORIENTATION, + GTK_ORIENTATION_VERTICAL, + G_PARAM_READWRITE)); + g_object_class_install_property (object_class, + PROP_IS_MUTED, + g_param_spec_boolean ("is-muted", + "is muted", + "Whether stream is muted", + FALSE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (object_class, + PROP_SHOW_MUTE, + g_param_spec_boolean ("show-mute", + "show mute", + "Whether stream is muted", + FALSE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + + g_object_class_install_property (object_class, + PROP_ADJUSTMENT, + g_param_spec_object ("adjustment", + "Adjustment", + "The GtkAdjustment that contains the current value of this scale button object", + GTK_TYPE_ADJUSTMENT, + G_PARAM_READWRITE)); + g_object_class_install_property (object_class, + PROP_NAME, + g_param_spec_string ("name", + "Name", + "Name to display for this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (object_class, + PROP_ICON_NAME, + g_param_spec_string ("icon-name", + "Icon Name", + "Name of icon to display for this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (object_class, + PROP_LOW_ICON_NAME, + g_param_spec_string ("low-icon-name", + "Icon Name", + "Name of icon to display for this stream", + "audio-volume-low-symbolic", + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (object_class, + PROP_HIGH_ICON_NAME, + g_param_spec_string ("high-icon-name", + "Icon Name", + "Name of icon to display for this stream", + "audio-volume-high-symbolic", + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (object_class, + PROP_IS_AMPLIFIED, + g_param_spec_boolean ("is-amplified", + "Is amplified", + "Whether the stream is digitally amplified", + FALSE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (object_class, + PROP_ELLIPSIZE, + g_param_spec_boolean ("ellipsize", + "Label is ellipsized", + "Whether the label is ellipsized", + FALSE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_type_class_add_private (klass, sizeof (GvcChannelBarPrivate)); +} + +static void +on_mute_switch_toggled (GtkSwitch *sw, + GParamSpec *pspec, + GvcChannelBar *bar) +{ + gboolean is_muted; + is_muted = gtk_switch_get_active (sw); + gvc_channel_bar_set_is_muted (bar, !is_muted); +} + +static void +gvc_channel_bar_init (GvcChannelBar *bar) +{ + GtkWidget *frame; + + bar->priv = GVC_CHANNEL_BAR_GET_PRIVATE (bar); + + bar->priv->base_volume = ADJUSTMENT_MAX_NORMAL; + bar->priv->low_icon_name = g_strdup ("audio-volume-low-symbolic"); + bar->priv->high_icon_name = g_strdup ("audio-volume-high-symbolic"); + + bar->priv->orientation = GTK_ORIENTATION_VERTICAL; + bar->priv->adjustment = GTK_ADJUSTMENT (gtk_adjustment_new (0.0, + 0.0, + ADJUSTMENT_MAX_NORMAL, + ADJUSTMENT_MAX_NORMAL/100.0, + ADJUSTMENT_MAX_NORMAL/10.0, + 0.0)); + g_object_ref_sink (bar->priv->adjustment); + + bar->priv->zero_adjustment = GTK_ADJUSTMENT (gtk_adjustment_new (0.0, + 0.0, + ADJUSTMENT_MAX_NORMAL, + ADJUSTMENT_MAX_NORMAL/100.0, + ADJUSTMENT_MAX_NORMAL/10.0, + 0.0)); + g_object_ref_sink (bar->priv->zero_adjustment); + + g_signal_connect (bar->priv->zero_adjustment, + "value-changed", + G_CALLBACK (on_zero_adjustment_value_changed), + bar); + + bar->priv->mute_switch = gtk_switch_new (); + gtk_widget_set_no_show_all (bar->priv->mute_switch, TRUE); + g_signal_connect (bar->priv->mute_switch, + "notify::active", + G_CALLBACK (on_mute_switch_toggled), + bar); + bar->priv->mute_box = gtk_alignment_new (0.5, 0.5, 0, 0); + gtk_container_add (GTK_CONTAINER (bar->priv->mute_box), bar->priv->mute_switch); + + bar->priv->low_image = gtk_image_new_from_icon_name ("audio-volume-low-symbolic", + GTK_ICON_SIZE_MENU); + gtk_widget_set_no_show_all (bar->priv->low_image, TRUE); + bar->priv->high_image = gtk_image_new_from_icon_name ("audio-volume-high-symbolic", + GTK_ICON_SIZE_MENU); + gtk_widget_set_no_show_all (bar->priv->high_image, TRUE); + + bar->priv->image = gtk_image_new (); + gtk_widget_set_no_show_all (bar->priv->image, TRUE); + + bar->priv->label = gtk_label_new (NULL); + gtk_misc_set_alignment (GTK_MISC (bar->priv->label), 0.0, 0.5); + gtk_widget_set_no_show_all (bar->priv->label, TRUE); + + /* frame */ + frame = gtk_frame_new (NULL); + gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_NONE); + gtk_container_add (GTK_CONTAINER (bar), frame); + gtk_widget_show_all (frame); + + /* box with scale */ + bar->priv->scale_box = _scale_box_new (bar); + + gtk_container_add (GTK_CONTAINER (frame), bar->priv->scale_box); +} + +static void +gvc_channel_bar_finalize (GObject *object) +{ + GvcChannelBar *channel_bar; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_CHANNEL_BAR (object)); + + channel_bar = GVC_CHANNEL_BAR (object); + + g_return_if_fail (channel_bar->priv != NULL); + + g_free (channel_bar->priv->name); + g_free (channel_bar->priv->icon_name); + g_free (channel_bar->priv->low_icon_name); + g_free (channel_bar->priv->high_icon_name); + + G_OBJECT_CLASS (gvc_channel_bar_parent_class)->finalize (object); +} + +GtkWidget * +gvc_channel_bar_new (void) +{ + GObject *bar; + bar = g_object_new (GVC_TYPE_CHANNEL_BAR, + NULL); + return GTK_WIDGET (bar); +} diff --git a/gvc-channel-bar.h b/gvc-channel-bar.h new file mode 100644 index 0000000..0b607e8 --- /dev/null +++ b/gvc-channel-bar.h @@ -0,0 +1,89 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * 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. + * + */ + +#ifndef __GVC_CHANNEL_BAR_H +#define __GVC_CHANNEL_BAR_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GVC_TYPE_CHANNEL_BAR (gvc_channel_bar_get_type ()) +#define GVC_CHANNEL_BAR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_CHANNEL_BAR, GvcChannelBar)) +#define GVC_CHANNEL_BAR_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_CHANNEL_BAR, GvcChannelBarClass)) +#define GVC_IS_CHANNEL_BAR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_CHANNEL_BAR)) +#define GVC_IS_CHANNEL_BAR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_CHANNEL_BAR)) +#define GVC_CHANNEL_BAR_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_CHANNEL_BAR, GvcChannelBarClass)) + +typedef struct GvcChannelBarPrivate GvcChannelBarPrivate; + +typedef struct +{ + GtkHBox parent; + GvcChannelBarPrivate *priv; +} GvcChannelBar; + +typedef struct +{ + GtkHBoxClass parent_class; +} GvcChannelBarClass; + +GType gvc_channel_bar_get_type (void); + +GtkWidget * gvc_channel_bar_new (void); + +void gvc_channel_bar_set_name (GvcChannelBar *bar, + const char *name); +void gvc_channel_bar_set_icon_name (GvcChannelBar *bar, + const char *icon_name); +void gvc_channel_bar_set_low_icon_name (GvcChannelBar *bar, + const char *icon_name); +void gvc_channel_bar_set_high_icon_name (GvcChannelBar *bar, + const char *icon_name); + +void gvc_channel_bar_set_orientation (GvcChannelBar *bar, + GtkOrientation orientation); +GtkOrientation gvc_channel_bar_get_orientation (GvcChannelBar *bar); + +GtkAdjustment * gvc_channel_bar_get_adjustment (GvcChannelBar *bar); + +gboolean gvc_channel_bar_get_is_muted (GvcChannelBar *bar); +void gvc_channel_bar_set_is_muted (GvcChannelBar *bar, + gboolean is_muted); +gboolean gvc_channel_bar_get_show_mute (GvcChannelBar *bar); +void gvc_channel_bar_set_show_mute (GvcChannelBar *bar, + gboolean show_mute); +void gvc_channel_bar_set_size_group (GvcChannelBar *bar, + GtkSizeGroup *group, + gboolean symmetric); +void gvc_channel_bar_set_is_amplified (GvcChannelBar *bar, + gboolean amplified); +void gvc_channel_bar_set_base_volume (GvcChannelBar *bar, + guint32 base_volume); +gboolean gvc_channel_bar_get_ellipsize (GvcChannelBar *bar); +void gvc_channel_bar_set_ellipsize (GvcChannelBar *bar, + gboolean ellipsized); + +gboolean gvc_channel_bar_scroll (GvcChannelBar *bar, + GdkEventScroll *event); + +G_END_DECLS + +#endif /* __GVC_CHANNEL_BAR_H */ diff --git a/gvc-channel-map-private.h b/gvc-channel-map-private.h new file mode 100644 index 0000000..3949de3 --- /dev/null +++ b/gvc-channel-map-private.h @@ -0,0 +1,39 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * 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. + * + */ + +#ifndef __GVC_CHANNEL_MAP_PRIVATE_H +#define __GVC_CHANNEL_MAP_PRIVATE_H + +#include <glib-object.h> +#include <pulse/pulseaudio.h> + +G_BEGIN_DECLS + +GvcChannelMap * gvc_channel_map_new_from_pa_channel_map (const pa_channel_map *map); +const pa_channel_map * gvc_channel_map_get_pa_channel_map (const GvcChannelMap *map); + +void gvc_channel_map_volume_changed (GvcChannelMap *map, + const pa_cvolume *cv, + gboolean set); +const pa_cvolume * gvc_channel_map_get_cvolume (const GvcChannelMap *map); + +G_END_DECLS + +#endif /* __GVC_CHANNEL_MAP_PRIVATE_H */ diff --git a/gvc-channel-map.c b/gvc-channel-map.c new file mode 100644 index 0000000..a2073fd --- /dev/null +++ b/gvc-channel-map.c @@ -0,0 +1,254 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * 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 "gvc-channel-map.h" +#include "gvc-channel-map-private.h" + +#define GVC_CHANNEL_MAP_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GVC_TYPE_CHANNEL_MAP, GvcChannelMapPrivate)) + +struct GvcChannelMapPrivate +{ + pa_channel_map pa_map; + gboolean pa_volume_is_set; + pa_cvolume pa_volume; + gdouble extern_volume[NUM_TYPES]; /* volume, balance, fade, lfe */ + gboolean can_balance; + gboolean can_fade; +}; + +enum { + VOLUME_CHANGED, + LAST_SIGNAL +}; + +static guint signals [LAST_SIGNAL] = { 0, }; + +static void gvc_channel_map_class_init (GvcChannelMapClass *klass); +static void gvc_channel_map_init (GvcChannelMap *channel_map); +static void gvc_channel_map_finalize (GObject *object); + +G_DEFINE_TYPE (GvcChannelMap, gvc_channel_map, G_TYPE_OBJECT) + +guint +gvc_channel_map_get_num_channels (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), 0); + + if (!pa_channel_map_valid(&map->priv->pa_map)) + return 0; + + return map->priv->pa_map.channels; +} + +const gdouble * +gvc_channel_map_get_volume (GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL); + + if (!pa_channel_map_valid(&map->priv->pa_map)) + return NULL; + + map->priv->extern_volume[VOLUME] = (gdouble) pa_cvolume_max (&map->priv->pa_volume); + if (gvc_channel_map_can_balance (map)) + map->priv->extern_volume[BALANCE] = (gdouble) pa_cvolume_get_balance (&map->priv->pa_volume, &map->priv->pa_map); + else + map->priv->extern_volume[BALANCE] = 0; + if (gvc_channel_map_can_fade (map)) + map->priv->extern_volume[FADE] = (gdouble) pa_cvolume_get_fade (&map->priv->pa_volume, &map->priv->pa_map); + else + map->priv->extern_volume[FADE] = 0; + if (gvc_channel_map_has_lfe (map)) + map->priv->extern_volume[LFE] = (gdouble) pa_cvolume_get_position (&map->priv->pa_volume, &map->priv->pa_map, PA_CHANNEL_POSITION_LFE); + else + map->priv->extern_volume[LFE] = 0; + + return map->priv->extern_volume; +} + +gboolean +gvc_channel_map_can_balance (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), FALSE); + + return map->priv->can_balance; +} + +gboolean +gvc_channel_map_can_fade (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), FALSE); + + return map->priv->can_fade; +} + +const char * +gvc_channel_map_get_mapping (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL); + + if (!pa_channel_map_valid(&map->priv->pa_map)) + return NULL; + + return pa_channel_map_to_pretty_name (&map->priv->pa_map); +} + +/** + * gvc_channel_map_has_position: (skip) + * + * @map: + * @position: + * + * Returns: + */ +gboolean +gvc_channel_map_has_position (const GvcChannelMap *map, + pa_channel_position_t position) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), FALSE); + + return pa_channel_map_has_position (&(map->priv->pa_map), position); +} + +const pa_channel_map * +gvc_channel_map_get_pa_channel_map (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL); + + if (!pa_channel_map_valid(&map->priv->pa_map)) + return NULL; + + return &map->priv->pa_map; +} + +const pa_cvolume * +gvc_channel_map_get_cvolume (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL); + + if (!pa_channel_map_valid(&map->priv->pa_map)) + return NULL; + + return &map->priv->pa_volume; +} + +static void +gvc_channel_map_class_init (GvcChannelMapClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = gvc_channel_map_finalize; + + signals [VOLUME_CHANGED] = + g_signal_new ("volume-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcChannelMapClass, volume_changed), + NULL, NULL, + g_cclosure_marshal_VOID__BOOLEAN, + G_TYPE_NONE, 1, G_TYPE_BOOLEAN); + + g_type_class_add_private (klass, sizeof (GvcChannelMapPrivate)); +} + +void +gvc_channel_map_volume_changed (GvcChannelMap *map, + const pa_cvolume *cv, + gboolean set) +{ + g_return_if_fail (GVC_IS_CHANNEL_MAP (map)); + g_return_if_fail (cv != NULL); + g_return_if_fail (pa_cvolume_compatible_with_channel_map(cv, &map->priv->pa_map)); + + if (pa_cvolume_equal(cv, &map->priv->pa_volume)) + return; + + map->priv->pa_volume = *cv; + + if (map->priv->pa_volume_is_set == FALSE) { + map->priv->pa_volume_is_set = TRUE; + return; + } + g_signal_emit (map, signals[VOLUME_CHANGED], 0, set); +} + +static void +gvc_channel_map_init (GvcChannelMap *map) +{ + map->priv = GVC_CHANNEL_MAP_GET_PRIVATE (map); + map->priv->pa_volume_is_set = FALSE; +} + +static void +gvc_channel_map_finalize (GObject *object) +{ + GvcChannelMap *channel_map; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_CHANNEL_MAP (object)); + + channel_map = GVC_CHANNEL_MAP (object); + + g_return_if_fail (channel_map->priv != NULL); + + G_OBJECT_CLASS (gvc_channel_map_parent_class)->finalize (object); +} + +GvcChannelMap * +gvc_channel_map_new (void) +{ + GObject *map; + map = g_object_new (GVC_TYPE_CHANNEL_MAP, NULL); + return GVC_CHANNEL_MAP (map); +} + +static void +set_from_pa_map (GvcChannelMap *map, + const pa_channel_map *pa_map) +{ + g_assert (pa_channel_map_valid(pa_map)); + + map->priv->can_balance = pa_channel_map_can_balance (pa_map); + map->priv->can_fade = pa_channel_map_can_fade (pa_map); + + map->priv->pa_map = *pa_map; + pa_cvolume_set(&map->priv->pa_volume, pa_map->channels, PA_VOLUME_NORM); +} + +GvcChannelMap * +gvc_channel_map_new_from_pa_channel_map (const pa_channel_map *pa_map) +{ + GObject *map; + map = g_object_new (GVC_TYPE_CHANNEL_MAP, NULL); + + set_from_pa_map (GVC_CHANNEL_MAP (map), pa_map); + + return GVC_CHANNEL_MAP (map); +} diff --git a/gvc-channel-map.h b/gvc-channel-map.h new file mode 100644 index 0000000..85c5772 --- /dev/null +++ b/gvc-channel-map.h @@ -0,0 +1,73 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * 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. + * + */ + +#ifndef __GVC_CHANNEL_MAP_H +#define __GVC_CHANNEL_MAP_H + +#include <glib-object.h> +#include <gvc-pulseaudio-fake.h> + +G_BEGIN_DECLS + +#define GVC_TYPE_CHANNEL_MAP (gvc_channel_map_get_type ()) +#define GVC_CHANNEL_MAP(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_CHANNEL_MAP, GvcChannelMap)) +#define GVC_CHANNEL_MAP_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_CHANNEL_MAP, GvcChannelMapClass)) +#define GVC_IS_CHANNEL_MAP(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_CHANNEL_MAP)) +#define GVC_IS_CHANNEL_MAP_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_CHANNEL_MAP)) +#define GVC_CHANNEL_MAP_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_CHANNEL_MAP, GvcChannelMapClass)) + +typedef struct GvcChannelMapPrivate GvcChannelMapPrivate; + +typedef struct +{ + GObject parent; + GvcChannelMapPrivate *priv; +} GvcChannelMap; + +typedef struct +{ + GObjectClass parent_class; + void (*volume_changed) (GvcChannelMap *channel_map, gboolean set); +} GvcChannelMapClass; + +enum { + VOLUME, + BALANCE, + FADE, + LFE, + NUM_TYPES +}; + +GType gvc_channel_map_get_type (void); + +GvcChannelMap * gvc_channel_map_new (void); +guint gvc_channel_map_get_num_channels (const GvcChannelMap *map); +const gdouble * gvc_channel_map_get_volume (GvcChannelMap *map); +gboolean gvc_channel_map_can_balance (const GvcChannelMap *map); +gboolean gvc_channel_map_can_fade (const GvcChannelMap *map); +gboolean gvc_channel_map_has_position (const GvcChannelMap *map, + pa_channel_position_t position); +#define gvc_channel_map_has_lfe(x) gvc_channel_map_has_position (x, PA_CHANNEL_POSITION_LFE) + +const char * gvc_channel_map_get_mapping (const GvcChannelMap *map); + +G_END_DECLS + +#endif /* __GVC_CHANNEL_MAP_H */ diff --git a/gvc-mixer-card-private.h b/gvc-mixer-card-private.h new file mode 100644 index 0000000..e190f7f --- /dev/null +++ b/gvc-mixer-card-private.h @@ -0,0 +1,35 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008-2009 Red Hat, Inc. + * + * 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. + * + */ + +#ifndef __GVC_MIXER_CARD_PRIVATE_H +#define __GVC_MIXER_CARD_PRIVATE_H + +#include <pulse/pulseaudio.h> +#include "gvc-mixer-card.h" + +G_BEGIN_DECLS + +GvcMixerCard * gvc_mixer_card_new (pa_context *context, + guint index); +pa_context * gvc_mixer_card_get_pa_context (GvcMixerCard *card); + +G_END_DECLS + +#endif /* __GVC_MIXER_CARD_PRIVATE_H */ diff --git a/gvc-mixer-card.c b/gvc-mixer-card.c new file mode 100644 index 0000000..a9b246f --- /dev/null +++ b/gvc-mixer-card.c @@ -0,0 +1,570 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * Copyright (C) 2009 Bastien Nocera + * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com> + * + * 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 "gvc-mixer-card.h" +#include "gvc-mixer-card-private.h" + +#define GVC_MIXER_CARD_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GVC_TYPE_MIXER_CARD, GvcMixerCardPrivate)) + +static guint32 card_serial = 1; + +struct GvcMixerCardPrivate +{ + pa_context *pa_context; + guint id; + guint index; + char *name; + char *icon_name; + char *profile; + char *target_profile; + char *human_profile; + GList *profiles; + pa_operation *profile_op; + GList *ports; +}; + +enum +{ + PROP_0, + PROP_ID, + PROP_PA_CONTEXT, + PROP_INDEX, + PROP_NAME, + PROP_ICON_NAME, + PROP_PROFILE, + PROP_HUMAN_PROFILE, +}; + +static void gvc_mixer_card_class_init (GvcMixerCardClass *klass); +static void gvc_mixer_card_init (GvcMixerCard *mixer_card); +static void gvc_mixer_card_finalize (GObject *object); + +G_DEFINE_TYPE (GvcMixerCard, gvc_mixer_card, G_TYPE_OBJECT) + +static guint32 +get_next_card_serial (void) +{ + guint32 serial; + + serial = card_serial++; + + if ((gint32)card_serial < 0) { + card_serial = 1; + } + + return serial; +} + +pa_context * +gvc_mixer_card_get_pa_context (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), 0); + return card->priv->pa_context; +} + +guint +gvc_mixer_card_get_index (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), 0); + return card->priv->index; +} + +guint +gvc_mixer_card_get_id (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), 0); + return card->priv->id; +} + +const char * +gvc_mixer_card_get_name (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + return card->priv->name; +} + +gboolean +gvc_mixer_card_set_name (GvcMixerCard *card, + const char *name) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + + g_free (card->priv->name); + card->priv->name = g_strdup (name); + g_object_notify (G_OBJECT (card), "name"); + + return TRUE; +} + +const char * +gvc_mixer_card_get_icon_name (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + return card->priv->icon_name; +} + +gboolean +gvc_mixer_card_set_icon_name (GvcMixerCard *card, + const char *icon_name) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + + g_free (card->priv->icon_name); + card->priv->icon_name = g_strdup (icon_name); + g_object_notify (G_OBJECT (card), "icon-name"); + + return TRUE; +} + +/** + * gvc_mixer_card_get_profile: (skip) + * + * @card: + * + * Returns: + */ +GvcMixerCardProfile * +gvc_mixer_card_get_profile (GvcMixerCard *card) +{ + GList *l; + + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + g_return_val_if_fail (card->priv->profiles != NULL, NULL); + + for (l = card->priv->profiles; l != NULL; l = l->next) { + GvcMixerCardProfile *p = l->data; + if (g_str_equal (card->priv->profile, p->profile)) { + return p; + } + } + + g_assert_not_reached (); + + return NULL; +} + +gboolean +gvc_mixer_card_set_profile (GvcMixerCard *card, + const char *profile) +{ + GList *l; + + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + g_return_val_if_fail (card->priv->profiles != NULL, FALSE); + + g_free (card->priv->profile); + card->priv->profile = g_strdup (profile); + + g_free (card->priv->human_profile); + card->priv->human_profile = NULL; + + for (l = card->priv->profiles; l != NULL; l = l->next) { + GvcMixerCardProfile *p = l->data; + if (g_str_equal (card->priv->profile, p->profile)) { + card->priv->human_profile = g_strdup (p->human_profile); + break; + } + } + + g_object_notify (G_OBJECT (card), "profile"); + + return TRUE; +} + +static void +_pa_context_set_card_profile_by_index_cb (pa_context *context, + int success, + void *userdata) +{ + GvcMixerCard *card = GVC_MIXER_CARD (userdata); + + g_assert (card->priv->target_profile); + + if (success > 0) { + gvc_mixer_card_set_profile (card, card->priv->target_profile); + } else { + g_debug ("Failed to switch profile on '%s' from '%s' to '%s'", + card->priv->name, + card->priv->profile, + card->priv->target_profile); + } + g_free (card->priv->target_profile); + card->priv->target_profile = NULL; + + pa_operation_unref (card->priv->profile_op); + card->priv->profile_op = NULL; +} + +gboolean +gvc_mixer_card_change_profile (GvcMixerCard *card, + const char *profile) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + g_return_val_if_fail (card->priv->profiles != NULL, FALSE); + + /* Same profile, or already requested? */ + if (g_strcmp0 (card->priv->profile, profile) == 0) + return TRUE; + if (g_strcmp0 (profile, card->priv->target_profile) == 0) + return TRUE; + if (card->priv->profile_op != NULL) { + pa_operation_cancel (card->priv->profile_op); + pa_operation_unref (card->priv->profile_op); + card->priv->profile_op = NULL; + } + + if (card->priv->profile != NULL) { + g_free (card->priv->target_profile); + card->priv->target_profile = g_strdup (profile); + + card->priv->profile_op = pa_context_set_card_profile_by_index (card->priv->pa_context, + card->priv->index, + card->priv->target_profile, + _pa_context_set_card_profile_by_index_cb, + card); + + if (card->priv->profile_op == NULL) { + g_warning ("pa_context_set_card_profile_by_index() failed"); + return FALSE; + } + } else { + g_assert (card->priv->human_profile == NULL); + card->priv->profile = g_strdup (profile); + } + + return TRUE; +} + +/** + * gvc_mixer_card_get_profiles: + * + * Return value: (transfer none) (element-type GvcMixerCardProfile): + */ +const GList * +gvc_mixer_card_get_profiles (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + return card->priv->profiles; +} + + +/** + * gvc_mixer_card_get_ports: + * + * Return value: (transfer none) (element-type GvcMixerCardPort): + */ +const GList * +gvc_mixer_card_get_ports (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + return card->priv->ports; +} + +/** + * gvc_mixer_card_profile_compare: + * + * Return value: 1 if @a has a higher priority, -1 if @b has a higher + * priority, 0 if @a and @b have the same priority. + */ +int +gvc_mixer_card_profile_compare (GvcMixerCardProfile *a, + GvcMixerCardProfile *b) +{ + if (a->priority == b->priority) + return 0; + if (a->priority > b->priority) + return 1; + return -1; +} + +/** + * gvc_mixer_card_set_profiles: + * @profiles: (transfer full) (element-type GvcMixerCardProfile): + */ +gboolean +gvc_mixer_card_set_profiles (GvcMixerCard *card, + GList *profiles) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + g_return_val_if_fail (card->priv->profiles == NULL, FALSE); + + card->priv->profiles = g_list_sort (profiles, (GCompareFunc) gvc_mixer_card_profile_compare); + + return TRUE; +} + +/** + * gvc_mixer_card_get_gicon: + * + * @card: + * + * Return value: (transfer full) (element-type GIcon): + */ +GIcon * +gvc_mixer_card_get_gicon (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + + if (card->priv->icon_name == NULL) + return NULL; + + return g_themed_icon_new_with_default_fallbacks (card->priv->icon_name); +} + +/** + * gvc_mixer_card_set_ports: + * @profiles: (transfer full) (element-type GvcMixerCardPort): + */ +gboolean +gvc_mixer_card_set_ports (GvcMixerCard *card, + GList *ports) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + g_return_val_if_fail (card->priv->ports == NULL, FALSE); + + card->priv->ports = ports; + + return TRUE; +} + +static void +gvc_mixer_card_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GvcMixerCard *self = GVC_MIXER_CARD (object); + + switch (prop_id) { + case PROP_PA_CONTEXT: + self->priv->pa_context = g_value_get_pointer (value); + break; + case PROP_INDEX: + self->priv->index = g_value_get_ulong (value); + break; + case PROP_ID: + self->priv->id = g_value_get_ulong (value); + break; + case PROP_NAME: + gvc_mixer_card_set_name (self, g_value_get_string (value)); + break; + case PROP_ICON_NAME: + gvc_mixer_card_set_icon_name (self, g_value_get_string (value)); + break; + case PROP_PROFILE: + gvc_mixer_card_set_profile (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gvc_mixer_card_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GvcMixerCard *self = GVC_MIXER_CARD (object); + + switch (prop_id) { + case PROP_PA_CONTEXT: + g_value_set_pointer (value, self->priv->pa_context); + break; + case PROP_INDEX: + g_value_set_ulong (value, self->priv->index); + break; + case PROP_ID: + g_value_set_ulong (value, self->priv->id); + break; + case PROP_NAME: + g_value_set_string (value, self->priv->name); + break; + case PROP_ICON_NAME: + g_value_set_string (value, self->priv->icon_name); + break; + case PROP_PROFILE: + g_value_set_string (value, self->priv->profile); + break; + case PROP_HUMAN_PROFILE: + g_value_set_string (value, self->priv->human_profile); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static GObject * +gvc_mixer_card_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_params) +{ + GObject *object; + GvcMixerCard *self; + + object = G_OBJECT_CLASS (gvc_mixer_card_parent_class)->constructor (type, n_construct_properties, construct_params); + + self = GVC_MIXER_CARD (object); + + self->priv->id = get_next_card_serial (); + + return object; +} + +static void +gvc_mixer_card_class_init (GvcMixerCardClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->constructor = gvc_mixer_card_constructor; + gobject_class->finalize = gvc_mixer_card_finalize; + + gobject_class->set_property = gvc_mixer_card_set_property; + gobject_class->get_property = gvc_mixer_card_get_property; + + g_object_class_install_property (gobject_class, + PROP_INDEX, + g_param_spec_ulong ("index", + "Index", + "The index for this card", + 0, G_MAXULONG, 0, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property (gobject_class, + PROP_ID, + g_param_spec_ulong ("id", + "id", + "The id for this card", + 0, G_MAXULONG, 0, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property (gobject_class, + PROP_PA_CONTEXT, + g_param_spec_pointer ("pa-context", + "PulseAudio context", + "The PulseAudio context for this card", + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property (gobject_class, + PROP_NAME, + g_param_spec_string ("name", + "Name", + "Name to display for this card", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_ICON_NAME, + g_param_spec_string ("icon-name", + "Icon Name", + "Name of icon to display for this card", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_PROFILE, + g_param_spec_string ("profile", + "Profile", + "Name of current profile for this card", + NULL, + G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, + PROP_HUMAN_PROFILE, + g_param_spec_string ("human-profile", + "Profile (Human readable)", + "Name of current profile for this card in human readable form", + NULL, + G_PARAM_READABLE)); + + g_type_class_add_private (klass, sizeof (GvcMixerCardPrivate)); +} + +static void +gvc_mixer_card_init (GvcMixerCard *card) +{ + card->priv = GVC_MIXER_CARD_GET_PRIVATE (card); +} + +GvcMixerCard * +gvc_mixer_card_new (pa_context *context, + guint index) +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_CARD, + "index", index, + "pa-context", context, + NULL); + return GVC_MIXER_CARD (object); +} + +static void +free_profile (GvcMixerCardProfile *p) +{ + g_free (p->profile); + g_free (p->human_profile); + g_free (p->status); + g_free (p); +} + +static void +gvc_mixer_card_finalize (GObject *object) +{ + GvcMixerCard *mixer_card; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_CARD (object)); + + mixer_card = GVC_MIXER_CARD (object); + + g_return_if_fail (mixer_card->priv != NULL); + + g_free (mixer_card->priv->name); + mixer_card->priv->name = NULL; + + g_free (mixer_card->priv->icon_name); + mixer_card->priv->icon_name = NULL; + + g_free (mixer_card->priv->target_profile); + mixer_card->priv->target_profile = NULL; + + g_free (mixer_card->priv->profile); + mixer_card->priv->profile = NULL; + + g_free (mixer_card->priv->human_profile); + mixer_card->priv->human_profile = NULL; + + g_list_foreach (mixer_card->priv->profiles, (GFunc) free_profile, NULL); + g_list_free (mixer_card->priv->profiles); + mixer_card->priv->profiles = NULL; + + G_OBJECT_CLASS (gvc_mixer_card_parent_class)->finalize (object); +} + diff --git a/gvc-mixer-card.h b/gvc-mixer-card.h new file mode 100644 index 0000000..7fd6d4c --- /dev/null +++ b/gvc-mixer-card.h @@ -0,0 +1,101 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008-2009 Red Hat, Inc. + * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com> + * + * 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. + * + */ + +#ifndef __GVC_MIXER_CARD_H +#define __GVC_MIXER_CARD_H + +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_CARD (gvc_mixer_card_get_type ()) +#define GVC_MIXER_CARD(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_CARD, GvcMixerCard)) +#define GVC_MIXER_CARD_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_CARD, GvcMixerCardClass)) +#define GVC_IS_MIXER_CARD(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_CARD)) +#define GVC_IS_MIXER_CARD_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_CARD)) +#define GVC_MIXER_CARD_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_CARD, GvcMixerCardClass)) + +typedef struct GvcMixerCardPrivate GvcMixerCardPrivate; + +typedef struct +{ + GObject parent; + GvcMixerCardPrivate *priv; +} GvcMixerCard; + +typedef struct +{ + GObjectClass parent_class; + + /* vtable */ +} GvcMixerCardClass; + +typedef struct +{ + char *profile; + char *human_profile; + char *status; + guint priority; + guint n_sinks, n_sources; +} GvcMixerCardProfile; + +typedef struct +{ + char *port; + char *human_port; + guint priority; + gint available; + gint direction; + GList *profiles; +} GvcMixerCardPort; + +GType gvc_mixer_card_get_type (void); + +guint gvc_mixer_card_get_id (GvcMixerCard *card); +guint gvc_mixer_card_get_index (GvcMixerCard *card); +const char * gvc_mixer_card_get_name (GvcMixerCard *card); +const char * gvc_mixer_card_get_icon_name (GvcMixerCard *card); +GvcMixerCardProfile * gvc_mixer_card_get_profile (GvcMixerCard *card); +const GList * gvc_mixer_card_get_profiles (GvcMixerCard *card); +const GList * gvc_mixer_card_get_ports (GvcMixerCard *card); +gboolean gvc_mixer_card_change_profile (GvcMixerCard *card, + const char *profile); +GIcon * gvc_mixer_card_get_gicon (GvcMixerCard *card); + +int gvc_mixer_card_profile_compare (GvcMixerCardProfile *a, + GvcMixerCardProfile *b); + +/* private */ +gboolean gvc_mixer_card_set_name (GvcMixerCard *card, + const char *name); +gboolean gvc_mixer_card_set_icon_name (GvcMixerCard *card, + const char *name); +gboolean gvc_mixer_card_set_profile (GvcMixerCard *card, + const char *profile); +gboolean gvc_mixer_card_set_profiles (GvcMixerCard *card, + GList *profiles); +gboolean gvc_mixer_card_set_ports (GvcMixerCard *stream, + GList *ports); + +G_END_DECLS + +#endif /* __GVC_MIXER_CARD_H */ diff --git a/gvc-mixer-control-private.h b/gvc-mixer-control-private.h new file mode 100644 index 0000000..ac79975 --- /dev/null +++ b/gvc-mixer-control-private.h @@ -0,0 +1,35 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * 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. + * + */ + +#ifndef __GVC_MIXER_CONTROL_PRIVATE_H +#define __GVC_MIXER_CONTROL_PRIVATE_H + +#include <glib-object.h> +#include <pulse/pulseaudio.h> +#include "gvc-mixer-stream.h" +#include "gvc-mixer-card.h" + +G_BEGIN_DECLS + +pa_context * gvc_mixer_control_get_pa_context (GvcMixerControl *control); + +G_END_DECLS + +#endif /* __GVC_MIXER_CONTROL_PRIVATE_H */ diff --git a/gvc-mixer-control.c b/gvc-mixer-control.c new file mode 100644 index 0000000..34ddc0c --- /dev/null +++ b/gvc-mixer-control.c @@ -0,0 +1,3358 @@ +/* -*- 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> + +#include "gvc-mixer-control.h" +#include "gvc-mixer-sink.h" +#include "gvc-mixer-source.h" +#include "gvc-mixer-sink-input.h" +#include "gvc-mixer-source-output.h" +#include "gvc-mixer-event-role.h" +#include "gvc-mixer-card.h" +#include "gvc-mixer-card-private.h" +#include "gvc-channel-map-private.h" +#include "gvc-mixer-control-private.h" +#include "gvc-mixer-ui-device.h" + +#define GVC_MIXER_CONTROL_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GVC_TYPE_MIXER_CONTROL, GvcMixerControlPrivate)) + +#define RECONNECT_DELAY 5 + +enum { + PROP_0, + PROP_NAME +}; + +struct GvcMixerControlPrivate +{ + pa_glib_mainloop *pa_mainloop; + pa_mainloop_api *pa_api; + pa_context *pa_context; + int n_outstanding; + guint reconnect_id; + char *name; + + gboolean default_sink_is_set; + guint default_sink_id; + char *default_sink_name; + gboolean default_source_is_set; + guint default_source_id; + char *default_source_name; + + gboolean event_sink_input_is_set; + guint event_sink_input_id; + + GHashTable *all_streams; + GHashTable *sinks; /* fixed outputs */ + GHashTable *sources; /* fixed inputs */ + GHashTable *sink_inputs; /* routable output streams */ + GHashTable *source_outputs; /* routable input streams */ + GHashTable *clients; + GHashTable *cards; + + GvcMixerStream *new_default_sink_stream; /* new default sink stream, used in gvc_mixer_control_set_default_sink () */ + GvcMixerStream *new_default_source_stream; /* new default source stream, used in gvc_mixer_control_set_default_source () */ + + GHashTable *ui_outputs; /* UI visible outputs */ + GHashTable *ui_inputs; /* UI visible inputs */ + + /* When we change profile on a device that is not the server default sink, + * it will jump back to the default sink set by the server to prevent the + * audio setup from being 'outputless'. + * + * All well and good but then when we get the new stream created for the + * new profile how do we know that this is the intended default or selected + * device the user wishes to use. */ + guint profile_swapping_device_id; + + GvcMixerControlState state; +}; + +enum { + STATE_CHANGED, + STREAM_ADDED, + STREAM_REMOVED, + CARD_ADDED, + CARD_REMOVED, + DEFAULT_SINK_CHANGED, + DEFAULT_SOURCE_CHANGED, + ACTIVE_OUTPUT_UPDATE, + ACTIVE_INPUT_UPDATE, + OUTPUT_ADDED, + INPUT_ADDED, + OUTPUT_REMOVED, + INPUT_REMOVED, + LAST_SIGNAL +}; + +static guint signals [LAST_SIGNAL] = { 0, }; + +static void gvc_mixer_control_class_init (GvcMixerControlClass *klass); +static void gvc_mixer_control_init (GvcMixerControl *mixer_control); +static void gvc_mixer_control_finalize (GObject *object); + +G_DEFINE_TYPE (GvcMixerControl, gvc_mixer_control, G_TYPE_OBJECT) + +pa_context * +gvc_mixer_control_get_pa_context (GvcMixerControl *control) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + return control->priv->pa_context; +} + +/** + * gvc_mixer_control_get_event_sink_input: + * + * @control: + * + * Returns: (transfer none): + */ +GvcMixerStream * +gvc_mixer_control_get_event_sink_input (GvcMixerControl *control) +{ + GvcMixerStream *stream; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + stream = g_hash_table_lookup (control->priv->all_streams, + GUINT_TO_POINTER (control->priv->event_sink_input_id)); + + return stream; +} + +static void +gvc_mixer_control_stream_restore_cb (pa_context *c, + GvcMixerStream *new_stream, + const pa_ext_stream_restore_info *info, + GvcMixerControl *control) +{ + pa_operation *o; + pa_ext_stream_restore_info new_info; + + if (new_stream == NULL) + return; + + new_info.name = info->name; + new_info.channel_map = info->channel_map; + new_info.volume = info->volume; + new_info.mute = info->mute; + + new_info.device = gvc_mixer_stream_get_name (new_stream); + + o = pa_ext_stream_restore_write (control->priv->pa_context, + PA_UPDATE_REPLACE, + &new_info, 1, + TRUE, NULL, NULL); + + if (o == NULL) { + g_warning ("pa_ext_stream_restore_write() failed: %s", + pa_strerror (pa_context_errno (control->priv->pa_context))); + return; + } + + g_debug ("Changed default device for %s to %s", info->name, new_info.device); + + pa_operation_unref (o); +} + +static void +gvc_mixer_control_stream_restore_sink_cb (pa_context *c, + const pa_ext_stream_restore_info *info, + int eol, + void *userdata) +{ + GvcMixerControl *control = (GvcMixerControl *) userdata; + if (eol || info == NULL || !g_str_has_prefix(info->name, "sink-input-by")) + return; + gvc_mixer_control_stream_restore_cb (c, control->priv->new_default_sink_stream, info, control); +} + +static void +gvc_mixer_control_stream_restore_source_cb (pa_context *c, + const pa_ext_stream_restore_info *info, + int eol, + void *userdata) +{ + GvcMixerControl *control = (GvcMixerControl *) userdata; + if (eol || info == NULL || !g_str_has_prefix(info->name, "source-output-by")) + return; + gvc_mixer_control_stream_restore_cb (c, control->priv->new_default_source_stream, info, control); +} + +/** + * gvc_mixer_control_lookup_device_from_stream: + * @control: + * @stream: + * Returns: (transfer none): a #GvcUIDevice or %NULL + */ +GvcMixerUIDevice * +gvc_mixer_control_lookup_device_from_stream (GvcMixerControl *control, + GvcMixerStream *stream) +{ + GList *devices, *d; + gboolean is_network_stream; + const GList *ports; + GvcMixerUIDevice *ret; + + if (GVC_IS_MIXER_SOURCE (stream)) + devices = g_hash_table_get_values (control->priv->ui_inputs); + else + devices = g_hash_table_get_values (control->priv->ui_outputs); + + ret = NULL; + ports = gvc_mixer_stream_get_ports (stream); + is_network_stream = (ports == NULL); + + for (d = devices; d != NULL; d = d->next) { + GvcMixerUIDevice *device = d->data; + gint stream_id = G_MAXINT; + + g_object_get (G_OBJECT (device), + "stream-id", &stream_id, + NULL); + + if (is_network_stream && + stream_id == gvc_mixer_stream_get_id (stream)) { + g_debug ("lookup device from stream - %s - it is a network_stream ", + gvc_mixer_ui_device_get_description (device)); + ret = device; + break; + } else if (!is_network_stream) { + const GvcMixerStreamPort *port; + port = gvc_mixer_stream_get_port (stream); + + if (stream_id == gvc_mixer_stream_get_id (stream) && + g_strcmp0 (gvc_mixer_ui_device_get_port (device), + port->port) == 0) { + g_debug ("lookup-device-from-stream found device: device description '%s', device port = '%s', device stream id %i AND stream port = '%s' stream id '%u' and stream description '%s'", + gvc_mixer_ui_device_get_description (device), + gvc_mixer_ui_device_get_port (device), + stream_id, + port->port, + gvc_mixer_stream_get_id (stream), + gvc_mixer_stream_get_description (stream)); + ret = device; + break; + } + } + } + + g_debug ("gvc_mixer_control_lookup_device_from_stream - Could not find a device for stream '%s'",gvc_mixer_stream_get_description (stream)); + + g_list_free (devices); + + return ret; +} + +gboolean +gvc_mixer_control_set_default_sink (GvcMixerControl *control, + GvcMixerStream *stream) +{ + pa_operation *o; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE); + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_debug ("about to set default sink on server"); + o = pa_context_set_default_sink (control->priv->pa_context, + gvc_mixer_stream_get_name (stream), + NULL, + NULL); + if (o == NULL) { + g_warning ("pa_context_set_default_sink() failed: %s", + pa_strerror (pa_context_errno (control->priv->pa_context))); + return FALSE; + } + + pa_operation_unref (o); + + control->priv->new_default_sink_stream = stream; + g_object_add_weak_pointer (G_OBJECT (stream), (gpointer *) &control->priv->new_default_sink_stream); + + o = pa_ext_stream_restore_read (control->priv->pa_context, + gvc_mixer_control_stream_restore_sink_cb, + control); + + if (o == NULL) { + g_warning ("pa_ext_stream_restore_read() failed: %s", + pa_strerror (pa_context_errno (control->priv->pa_context))); + return FALSE; + } + + pa_operation_unref (o); + + return TRUE; +} + +gboolean +gvc_mixer_control_set_default_source (GvcMixerControl *control, + GvcMixerStream *stream) +{ + GvcMixerUIDevice* input; + pa_operation *o; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE); + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + o = pa_context_set_default_source (control->priv->pa_context, + gvc_mixer_stream_get_name (stream), + NULL, + NULL); + if (o == NULL) { + g_warning ("pa_context_set_default_source() failed"); + return FALSE; + } + + pa_operation_unref (o); + + control->priv->new_default_source_stream = stream; + g_object_add_weak_pointer (G_OBJECT (stream), (gpointer *) &control->priv->new_default_source_stream); + + o = pa_ext_stream_restore_read (control->priv->pa_context, + gvc_mixer_control_stream_restore_source_cb, + control); + + if (o == NULL) { + g_warning ("pa_ext_stream_restore_read() failed: %s", + pa_strerror (pa_context_errno (control->priv->pa_context))); + return FALSE; + } + + pa_operation_unref (o); + + /* source change successful, update the UI. */ + input = gvc_mixer_control_lookup_device_from_stream (control, stream); + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_INPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (input)); + + return TRUE; +} + +/** + * gvc_mixer_control_get_default_sink: + * + * @control: + * + * Returns: (transfer none): + */ +GvcMixerStream * +gvc_mixer_control_get_default_sink (GvcMixerControl *control) +{ + GvcMixerStream *stream; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + if (control->priv->default_sink_is_set) { + stream = g_hash_table_lookup (control->priv->all_streams, + GUINT_TO_POINTER (control->priv->default_sink_id)); + } else { + stream = NULL; + } + + return stream; +} + +/** + * gvc_mixer_control_get_default_source: + * + * @control: + * + * Returns: (transfer none): + */ +GvcMixerStream * +gvc_mixer_control_get_default_source (GvcMixerControl *control) +{ + GvcMixerStream *stream; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + if (control->priv->default_source_is_set) { + stream = g_hash_table_lookup (control->priv->all_streams, + GUINT_TO_POINTER (control->priv->default_source_id)); + } else { + stream = NULL; + } + + return stream; +} + +static gpointer +gvc_mixer_control_lookup_id (GHashTable *hash_table, + guint id) +{ + return g_hash_table_lookup (hash_table, + GUINT_TO_POINTER (id)); +} + +/** + * gvc_mixer_control_lookup_stream_id: + * + * @control: + * @id: + * + * Returns: (transfer none): + */ +GvcMixerStream * +gvc_mixer_control_lookup_stream_id (GvcMixerControl *control, + guint id) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + return gvc_mixer_control_lookup_id (control->priv->all_streams, id); +} + +/** + * gvc_mixer_control_lookup_card_id: + * + * @control: + * @id: + * + * Returns: (transfer none): + */ +GvcMixerCard * +gvc_mixer_control_lookup_card_id (GvcMixerControl *control, + guint id) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + return gvc_mixer_control_lookup_id (control->priv->cards, id); +} + +/** + * gvc_mixer_control_lookup_output_id: + * @control: + * @id: + * Returns: (transfer none): + */ +GvcMixerUIDevice * +gvc_mixer_control_lookup_output_id (GvcMixerControl *control, + guint id) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + return gvc_mixer_control_lookup_id (control->priv->ui_outputs, id); +} + +/** + * gvc_mixer_control_lookup_input_id: + * @control: + * @id: + * Returns: (transfer none): + */ +GvcMixerUIDevice * +gvc_mixer_control_lookup_input_id (GvcMixerControl *control, + guint id) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + return gvc_mixer_control_lookup_id (control->priv->ui_inputs, id); +} + +/** + * gvc_mixer_control_get_stream_from_device: + * @control: + * @device: + * Returns: (transfer none): + */ +GvcMixerStream * +gvc_mixer_control_get_stream_from_device (GvcMixerControl *control, + GvcMixerUIDevice *device) +{ + gint stream_id; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + stream_id = gvc_mixer_ui_device_get_stream_id (device); + + if (stream_id == GVC_MIXER_UI_DEVICE_INVALID) { + g_debug ("gvc_mixer_control_get_stream_from_device - device has a null stream"); + return NULL; + } + return gvc_mixer_control_lookup_stream_id (control, stream_id); +} + +/** + * gvc_mixer_control_change_profile_on_selected_device: + * @control: + * @device: + * @profile: Can be null if any profile present on this port is okay + * Returns: This method will attempt to swap the profile on the card of + * the device with given profile name. If successfull it will set the + * preferred profile on that device so as we know the next time the user + * moves to that device it should have this profile active. + */ +gboolean +gvc_mixer_control_change_profile_on_selected_device (GvcMixerControl *control, + GvcMixerUIDevice *device, + const gchar *profile) +{ + const gchar *best_profile; + GvcMixerCardProfile *current_profile; + GvcMixerCard *card; + + g_object_get (G_OBJECT (device), "card", &card, NULL); + current_profile = gvc_mixer_card_get_profile (card); + + if (current_profile) + best_profile = gvc_mixer_ui_device_get_best_profile (device, profile, current_profile->profile); + else + best_profile = profile; + + g_assert (best_profile); + + g_debug ("Selected '%s', moving to profile '%s' on card '%s' on stream id %i", + profile ? profile : "(any)", best_profile, + gvc_mixer_card_get_name (card), + gvc_mixer_ui_device_get_stream_id (device)); + + g_debug ("default sink name = %s and default sink id %u", + control->priv->default_sink_name, + control->priv->default_sink_id); + + control->priv->profile_swapping_device_id = gvc_mixer_ui_device_get_id (device); + + if (gvc_mixer_card_change_profile (card, best_profile)) { + gvc_mixer_ui_device_set_user_preferred_profile (device, best_profile); + return TRUE; + } + return FALSE; +} + +/** + * gvc_mixer_control_change_output: + * @control: + * @output: + * This method is called from the UI when the user selects a previously unselected device. + * - Firstly it queries the stream from the device. + * - It assumes that if the stream is null that it cannot be a bluetooth or network stream (they never show unless they have valid sinks and sources) + * In the scenario of a NULL stream on the device + * - It fetches the device's preferred profile or if NUll the profile with the highest priority on that device. + * - It then caches this device in control->priv->cached_desired_output_id so that when the update_sink triggered + * from when we attempt to change profile we will know exactly what device to highlight on that stream. + * - It attempts to swap the profile on the card from that device and returns. + * - Next, it handles network or bluetooth streams that only require their stream to be made the default. + * - Next it deals with port changes so if the stream's active port is not the same as the port on the device + * it will attempt to change the port on that stream to be same as the device. If this fails it will return. + * - Finally it will set this new stream to be the default stream and emit a signal for the UI confirming the active output device. + */ +void +gvc_mixer_control_change_output (GvcMixerControl *control, + GvcMixerUIDevice* output) +{ + GvcMixerStream *stream; + GvcMixerStream *default_stream; + const GvcMixerStreamPort *active_port; + const gchar *output_port; + + g_debug ("control change output"); + + stream = gvc_mixer_control_get_stream_from_device (control, output); + if (stream == NULL) { + gvc_mixer_control_change_profile_on_selected_device (control, + output, NULL); + return; + } + + /* Handle a network sink as a portless or cardless device */ + if (!gvc_mixer_ui_device_has_ports (output)) { + g_debug ("Did we try to move to a software/bluetooth sink ?"); + if (gvc_mixer_control_set_default_sink (control, stream)) { + /* sink change was successful, update the UI.*/ + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_OUTPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (output)); + } + else { + g_warning ("Failed to set default sink with stream from output %s", + gvc_mixer_ui_device_get_description (output)); + } + return; + } + + active_port = gvc_mixer_stream_get_port (stream); + output_port = gvc_mixer_ui_device_get_port (output); + /* First ensure the correct port is active on the sink */ + if (g_strcmp0 (active_port->port, output_port) != 0) { + g_debug ("Port change, switch to = %s", output_port); + if (gvc_mixer_stream_change_port (stream, output_port) == FALSE) { + g_warning ("Could not change port !"); + return; + } + } + + default_stream = gvc_mixer_control_get_default_sink (control); + + /* Finally if we are not on the correct stream, swap over. */ + if (stream != default_stream) { + GvcMixerUIDevice* output; + + g_debug ("Attempting to swap over to stream %s ", + gvc_mixer_stream_get_description (stream)); + if (gvc_mixer_control_set_default_sink (control, stream)) { + output = 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 (output)); + } else { + /* If the move failed for some reason reset the UI. */ + output = 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 (output)); + } + } +} + + +/** + * gvc_mixer_control_change_input: + * @control: + * @input: + * This method is called from the UI when the user selects a previously unselected device. + * - Firstly it queries the stream from the device. + * - It assumes that if the stream is null that it cannot be a bluetooth or network stream (they never show unless they have valid sinks and sources) + * In the scenario of a NULL stream on the device + * - It fetches the device's preferred profile or if NUll the profile with the highest priority on that device. + * - It then caches this device in control->priv->cached_desired_input_id so that when the update_source triggered + * from when we attempt to change profile we will know exactly what device to highlight on that stream. + * - It attempts to swap the profile on the card from that device and returns. + * - Next, it handles network or bluetooth streams that only require their stream to be made the default. + * - Next it deals with port changes so if the stream's active port is not the same as the port on the device + * it will attempt to change the port on that stream to be same as the device. If this fails it will return. + * - Finally it will set this new stream to be the default stream and emit a signal for the UI confirming the active input device. + */ +void +gvc_mixer_control_change_input (GvcMixerControl *control, + GvcMixerUIDevice* input) +{ + GvcMixerStream *stream; + GvcMixerStream *default_stream; + const GvcMixerStreamPort *active_port; + const gchar *input_port; + + stream = gvc_mixer_control_get_stream_from_device (control, input); + if (stream == NULL) { + gvc_mixer_control_change_profile_on_selected_device (control, + input, NULL); + return; + } + + /* Handle a network sink as a portless/cardless device */ + if (!gvc_mixer_ui_device_has_ports (input)) { + g_debug ("Did we try to move to a software/bluetooth source ?"); + if (! gvc_mixer_control_set_default_source (control, stream)) { + g_warning ("Failed to set default source with stream from input %s", + gvc_mixer_ui_device_get_description (input)); + } + return; + } + + active_port = gvc_mixer_stream_get_port (stream); + input_port = gvc_mixer_ui_device_get_port (input); + /* First ensure the correct port is active on the sink */ + if (g_strcmp0 (active_port->port, input_port) != 0) { + g_debug ("Port change, switch to = %s", input_port); + if (gvc_mixer_stream_change_port (stream, input_port) == FALSE) { + g_warning ("Could not change port!"); + return; + } + } + + default_stream = gvc_mixer_control_get_default_source (control); + + /* Finally if we are not on the correct stream, swap over. */ + if (stream != default_stream) { + g_debug ("change-input - attempting to swap over to stream %s", + gvc_mixer_stream_get_description (stream)); + gvc_mixer_control_set_default_source (control, stream); + } +} + + +static void +listify_hash_values_hfunc (gpointer key, + gpointer value, + gpointer user_data) +{ + GSList **list = user_data; + + *list = g_slist_prepend (*list, value); +} + +static int +gvc_name_collate (const char *namea, + const char *nameb) +{ + if (nameb == NULL && namea == NULL) + return 0; + if (nameb == NULL) + return 1; + if (namea == NULL) + return -1; + + return g_utf8_collate (namea, nameb); +} + +static int +gvc_card_collate (GvcMixerCard *a, + GvcMixerCard *b) +{ + const char *namea; + const char *nameb; + + g_return_val_if_fail (a == NULL || GVC_IS_MIXER_CARD (a), 0); + g_return_val_if_fail (b == NULL || GVC_IS_MIXER_CARD (b), 0); + + namea = gvc_mixer_card_get_name (a); + nameb = gvc_mixer_card_get_name (b); + + return gvc_name_collate (namea, nameb); +} + +/** + * gvc_mixer_control_get_cards: + * + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerCard): + */ +GSList * +gvc_mixer_control_get_cards (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->cards, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_card_collate); +} + +static int +gvc_stream_collate (GvcMixerStream *a, + GvcMixerStream *b) +{ + const char *namea; + const char *nameb; + + g_return_val_if_fail (a == NULL || GVC_IS_MIXER_STREAM (a), 0); + g_return_val_if_fail (b == NULL || GVC_IS_MIXER_STREAM (b), 0); + + namea = gvc_mixer_stream_get_name (a); + nameb = gvc_mixer_stream_get_name (b); + + return gvc_name_collate (namea, nameb); +} + +/** + * gvc_mixer_control_get_streams: + * + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerStream): + */ +GSList * +gvc_mixer_control_get_streams (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->all_streams, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate); +} + +/** + * gvc_mixer_control_get_sinks: + * + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerSink): + */ +GSList * +gvc_mixer_control_get_sinks (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->sinks, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate); +} + +/** + * gvc_mixer_control_get_sources: + * + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerSource): + */ +GSList * +gvc_mixer_control_get_sources (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->sources, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate); +} + +/** + * gvc_mixer_control_get_sink_inputs: + * + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerSinkInput): + */ +GSList * +gvc_mixer_control_get_sink_inputs (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->sink_inputs, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate); +} + +/** + * gvc_mixer_control_get_source_outputs: + * + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerSourceOutput): + */ +GSList * +gvc_mixer_control_get_source_outputs (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->source_outputs, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate); +} + +static void +dec_outstanding (GvcMixerControl *control) +{ + if (control->priv->n_outstanding <= 0) { + return; + } + + if (--control->priv->n_outstanding <= 0) { + control->priv->state = GVC_STATE_READY; + g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_READY); + } +} + +GvcMixerControlState +gvc_mixer_control_get_state (GvcMixerControl *control) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE); + + return control->priv->state; +} + +static void +on_default_source_port_notify (GObject *object, + GParamSpec *pspec, + GvcMixerControl *control) +{ + char *port; + GvcMixerUIDevice *input; + + g_object_get (object, "port", &port, NULL); + input = gvc_mixer_control_lookup_device_from_stream (control, + GVC_MIXER_STREAM (object)); + + g_debug ("on_default_source_port_notify - moved to port '%s' which SHOULD ?? correspond to output '%s'", + port, + gvc_mixer_ui_device_get_description (input)); + + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_INPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (input)); + + g_free (port); +} + + +static void +_set_default_source (GvcMixerControl *control, + GvcMixerStream *stream) +{ + guint new_id; + + if (stream == NULL) { + control->priv->default_source_id = 0; + control->priv->default_source_is_set = FALSE; + g_signal_emit (control, + signals[DEFAULT_SOURCE_CHANGED], + 0, + PA_INVALID_INDEX); + return; + } + + new_id = gvc_mixer_stream_get_id (stream); + + if (control->priv->default_source_id != new_id) { + GvcMixerUIDevice *input; + control->priv->default_source_id = new_id; + control->priv->default_source_is_set = TRUE; + g_signal_emit (control, + signals[DEFAULT_SOURCE_CHANGED], + 0, + new_id); + + if (control->priv->default_source_is_set) { + g_signal_handlers_disconnect_by_func (gvc_mixer_control_get_default_source (control), + on_default_source_port_notify, + control); + } + + g_signal_connect (stream, + "notify::port", + G_CALLBACK (on_default_source_port_notify), + control); + + input = gvc_mixer_control_lookup_device_from_stream (control, stream); + + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_INPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (input)); + } +} + +static void +on_default_sink_port_notify (GObject *object, + GParamSpec *pspec, + GvcMixerControl *control) +{ + char *port; + GvcMixerUIDevice *output; + + g_object_get (object, "port", &port, NULL); + + output = gvc_mixer_control_lookup_device_from_stream (control, + GVC_MIXER_STREAM (object)); + if (output != NULL) { + g_debug ("on_default_sink_port_notify - moved to port %s - which SHOULD correspond to output %s", + port, + gvc_mixer_ui_device_get_description (output)); + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_OUTPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (output)); + } + g_free (port); +} + +static void +_set_default_sink (GvcMixerControl *control, + GvcMixerStream *stream) +{ + guint new_id; + + if (stream == NULL) { + /* Don't tell front-ends about an unset default + * sink if it's already unset */ + if (control->priv->default_sink_is_set == FALSE) + return; + control->priv->default_sink_id = 0; + control->priv->default_sink_is_set = FALSE; + g_signal_emit (control, + signals[DEFAULT_SINK_CHANGED], + 0, + PA_INVALID_INDEX); + return; + } + + new_id = gvc_mixer_stream_get_id (stream); + + if (control->priv->default_sink_id != new_id) { + GvcMixerUIDevice *output; + if (control->priv->default_sink_is_set) { + g_signal_handlers_disconnect_by_func (gvc_mixer_control_get_default_sink (control), + on_default_sink_port_notify, + control); + } + + control->priv->default_sink_id = new_id; + + control->priv->default_sink_is_set = TRUE; + g_signal_emit (control, + signals[DEFAULT_SINK_CHANGED], + 0, + new_id); + + g_signal_connect (stream, + "notify::port", + G_CALLBACK (on_default_sink_port_notify), + control); + + output = gvc_mixer_control_lookup_device_from_stream (control, stream); + + g_debug ("active_sink change"); + + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_OUTPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (output)); + } +} + +static gboolean +_stream_has_name (gpointer key, + GvcMixerStream *stream, + const char *name) +{ + const char *t_name; + + t_name = gvc_mixer_stream_get_name (stream); + + if (t_name != NULL + && name != NULL + && strcmp (t_name, name) == 0) { + return TRUE; + } + + return FALSE; +} + +static GvcMixerStream * +find_stream_for_name (GvcMixerControl *control, + const char *name) +{ + GvcMixerStream *stream; + + stream = g_hash_table_find (control->priv->all_streams, + (GHRFunc)_stream_has_name, + (char *)name); + return stream; +} + +static void +update_default_source_from_name (GvcMixerControl *control, + const char *name) +{ + gboolean changed = FALSE; + + if ((control->priv->default_source_name == NULL + && name != NULL) + || (control->priv->default_source_name != NULL + && name == NULL) + || (name != NULL && strcmp (control->priv->default_source_name, name) != 0)) { + changed = TRUE; + } + + if (changed) { + GvcMixerStream *stream; + + g_free (control->priv->default_source_name); + control->priv->default_source_name = g_strdup (name); + + stream = find_stream_for_name (control, name); + _set_default_source (control, stream); + } +} + +static void +update_default_sink_from_name (GvcMixerControl *control, + const char *name) +{ + gboolean changed = FALSE; + + if ((control->priv->default_sink_name == NULL + && name != NULL) + || (control->priv->default_sink_name != NULL + && name == NULL) + || (name != NULL && strcmp (control->priv->default_sink_name, name) != 0)) { + changed = TRUE; + } + + if (changed) { + GvcMixerStream *stream; + g_free (control->priv->default_sink_name); + control->priv->default_sink_name = g_strdup (name); + + stream = find_stream_for_name (control, name); + _set_default_sink (control, stream); + } +} + +static void +update_server (GvcMixerControl *control, + const pa_server_info *info) +{ + if (info->default_source_name != NULL) { + update_default_source_from_name (control, info->default_source_name); + } + if (info->default_sink_name != NULL) { + g_debug ("update server"); + update_default_sink_from_name (control, info->default_sink_name); + } +} + +static void +remove_stream (GvcMixerControl *control, + GvcMixerStream *stream) +{ + guint id; + + g_object_ref (stream); + + id = gvc_mixer_stream_get_id (stream); + + if (id == control->priv->default_sink_id) { + _set_default_sink (control, NULL); + } else if (id == control->priv->default_source_id) { + _set_default_source (control, NULL); + } + + g_hash_table_remove (control->priv->all_streams, + GUINT_TO_POINTER (id)); + g_signal_emit (G_OBJECT (control), + signals[STREAM_REMOVED], + 0, + gvc_mixer_stream_get_id (stream)); + g_object_unref (stream); +} + +static void +add_stream (GvcMixerControl *control, + GvcMixerStream *stream) +{ + g_hash_table_insert (control->priv->all_streams, + GUINT_TO_POINTER (gvc_mixer_stream_get_id (stream)), + stream); + g_signal_emit (G_OBJECT (control), + signals[STREAM_ADDED], + 0, + gvc_mixer_stream_get_id (stream)); +} + +/* This method will match individual stream ports against its corresponding device + * It does this by: + * - iterates through our devices and finds the one where the card-id on the device is the same as the card-id on the stream + * and the port-name on the device is the same as the streamport-name. + * This should always find a match and is used exclusively by sync_devices(). + */ +static gboolean +match_stream_with_devices (GvcMixerControl *control, + GvcMixerStreamPort *stream_port, + GvcMixerStream *stream) +{ + GList *devices, *d; + guint stream_card_id; + guint stream_id; + gboolean in_possession = FALSE; + + stream_id = gvc_mixer_stream_get_id (stream); + stream_card_id = gvc_mixer_stream_get_card_index (stream); + + devices = g_hash_table_get_values (GVC_IS_MIXER_SOURCE (stream) ? control->priv->ui_inputs : control->priv->ui_outputs); + + for (d = devices; d != NULL; d = d->next) { + GvcMixerUIDevice *device; + gint device_stream_id; + gchar *device_port_name; + gchar *origin; + gchar *description; + GvcMixerCard *card; + gint 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; + gint 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_new0 (GvcMixerStreamPort, 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 != 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_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); + } + + /* + * 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_new0 (GvcMixerStreamPort, 1); + 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_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); + } + + 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 sink"); + 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); + } +} + +static void +update_source_output (GvcMixerControl *control, + const pa_source_output_info *info) +{ + GvcMixerStream *stream; + gboolean is_new; + 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)); + + 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, "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); + } +} + +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) +{ + gint 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", (uint)direction, + "card", card, + "port-name", port->port, + "description", port->human_port, + "origin", gvc_mixer_card_get_name (card), + "port-available", available, + 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)), + g_object_ref (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)); +} + +/* + * At this point we can determine all devices available to us (besides network 'ports') + * This is done by the following: + * + * - gvc_mixer_card and gvc_mixer_card_ports are created and relevant setters are called. + * - First it checks to see if it's a portless card. Bluetooth devices are portless AFAIHS. + * If so it creates two devices, an input and an output. + * - If it's a 'normal' card with ports it will create a new ui-device or + * synchronise port availability with the existing device cached for that port on this card. */ + +static void +update_card (GvcMixerControl *control, + const pa_card_info *info) +{ + const GList *card_ports = NULL; + const GList *m = NULL; + GvcMixerCard *card; + gboolean is_new = FALSE; +#if 1 + guint i; + const char *key; + void *state; + + g_debug ("Udpating card %s (index: %u driver: %s):", + info->name, info->index, info->driver); + + for (i = 0; i < info->n_profiles; i++) { + struct pa_card_profile_info pi = info->profiles[i]; + gboolean is_default; + + is_default = (g_strcmp0 (pi.name, info->active_profile->name) == 0); + g_debug ("\tProfile '%s': %d sources %d sinks%s", + pi.name, pi.n_sources, pi.n_sinks, + is_default ? " (Current)" : ""); + } + state = NULL; + key = pa_proplist_iterate (info->proplist, &state); + while (key != NULL) { + g_debug ("\tProperty: '%s' = '%s'", + key, pa_proplist_gets (info->proplist, key)); + key = pa_proplist_iterate (info->proplist, &state); + } +#endif + card = g_hash_table_lookup (control->priv->cards, + GUINT_TO_POINTER (info->index)); + if (card == NULL) { + GList *profile_list = NULL; + GList *port_list = NULL; + + for (i = 0; i < info->n_profiles; i++) { + GvcMixerCardProfile *profile; + struct pa_card_profile_info pi = info->profiles[i]; + + profile = g_new0 (GvcMixerCardProfile, 1); + profile->profile = g_strdup (pi.name); + profile->human_profile = g_strdup (pi.description); + profile->status = card_num_streams_to_status (pi.n_sinks, pi.n_sources); + profile->n_sinks = pi.n_sinks; + profile->n_sources = pi.n_sources; + profile->priority = pi.priority; + profile_list = g_list_prepend (profile_list, profile); + } + card = gvc_mixer_card_new (control->priv->pa_context, + info->index); + gvc_mixer_card_set_profiles (card, profile_list); + + for (i = 0; i < info->n_ports; i++) { + GvcMixerCardPort *port; + port = g_new0 (GvcMixerCardPort, 1); + port->port = g_strdup (info->ports[i]->name); + port->human_port = g_strdup (info->ports[i]->description); + port->priority = info->ports[i]->priority; + port->available = info->ports[i]->available; + port->direction = info->ports[i]->direction; + port->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), + g_object_ref (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 != info->ports[i]->available) { + 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); + } + } + } + } + } + g_signal_emit (G_OBJECT (control), + signals[CARD_ADDED], + 0, + info->index); +} + +static void +_pa_context_get_sink_info_cb (pa_context *context, + const pa_sink_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) { + return; + } + + g_warning ("Sink callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_sink (control, i); +} + +static void +_pa_context_get_source_info_cb (pa_context *context, + const pa_source_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) { + return; + } + + g_warning ("Source callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_source (control, i); +} + +static void +_pa_context_get_sink_input_info_cb (pa_context *context, + const pa_sink_input_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) { + return; + } + + g_warning ("Sink input callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_sink_input (control, i); +} + +static void +_pa_context_get_source_output_info_cb (pa_context *context, + const pa_source_output_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) { + return; + } + + g_warning ("Source output callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_source_output (control, i); +} + +static void +_pa_context_get_client_info_cb (pa_context *context, + const pa_client_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) { + return; + } + + g_warning ("Client callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_client (control, i); +} + +static void +_pa_context_get_card_info_by_index_cb (pa_context *context, + const pa_card_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) + return; + + g_warning ("Card callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_card (control, i); +} + +static void +_pa_context_get_server_info_cb (pa_context *context, + const pa_server_info *i, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (i == NULL) { + g_warning ("Server info callback failure"); + return; + } + g_debug ("get server info"); + update_server (control, i); + dec_outstanding (control); +} + +static void +remove_event_role_stream (GvcMixerControl *control) +{ + g_debug ("Removing event role"); +} + +static void +update_event_role_stream (GvcMixerControl *control, + const pa_ext_stream_restore_info *info) +{ + GvcMixerStream *stream; + gboolean is_new; + pa_volume_t max_volume; + + if (strcmp (info->name, "sink-input-by-media-role:event") != 0) { + return; + } + +#if 0 + g_debug ("Updating event role: name='%s' device='%s'", + info->name, + info->device); +#endif + + is_new = FALSE; + + if (!control->priv->event_sink_input_is_set) { + pa_channel_map pa_map; + GvcChannelMap *map; + + pa_map.channels = 1; + pa_map.map[0] = PA_CHANNEL_POSITION_MONO; + map = gvc_channel_map_new_from_pa_channel_map (&pa_map); + + stream = gvc_mixer_event_role_new (control->priv->pa_context, + info->device, + map); + control->priv->event_sink_input_id = gvc_mixer_stream_get_id (stream); + control->priv->event_sink_input_is_set = TRUE; + + is_new = TRUE; + } else { + stream = g_hash_table_lookup (control->priv->all_streams, + GUINT_TO_POINTER (control->priv->event_sink_input_id)); + } + + max_volume = pa_cvolume_max (&info->volume); + + gvc_mixer_stream_set_name (stream, _("System Sounds")); + gvc_mixer_stream_set_icon_name (stream, "multimedia-volume-control"); + gvc_mixer_stream_set_volume (stream, (guint)max_volume); + gvc_mixer_stream_set_is_muted (stream, info->mute); + + if (is_new) { + add_stream (control, stream); + } +} + +static void +_pa_ext_stream_restore_read_cb (pa_context *context, + const pa_ext_stream_restore_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + g_debug ("Failed to initialized stream_restore extension: %s", + pa_strerror (pa_context_errno (context))); + remove_event_role_stream (control); + return; + } + + if (eol > 0) { + dec_outstanding (control); + /* If we don't have an event stream to restore, then + * set one up with a default 100% volume */ + if (!control->priv->event_sink_input_is_set) { + pa_ext_stream_restore_info info; + + memset (&info, 0, sizeof(info)); + info.name = "sink-input-by-media-role:event"; + info.volume.channels = 1; + info.volume.values[0] = PA_VOLUME_NORM; + update_event_role_stream (control, &info); + } + return; + } + + update_event_role_stream (control, i); +} + +static void +_pa_ext_stream_restore_subscribe_cb (pa_context *context, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + pa_operation *o; + + o = pa_ext_stream_restore_read (context, + _pa_ext_stream_restore_read_cb, + control); + if (o == NULL) { + g_warning ("pa_ext_stream_restore_read() failed"); + return; + } + + pa_operation_unref (o); +} + +static void +req_update_server_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + o = pa_context_get_server_info (control->priv->pa_context, + _pa_context_get_server_info_cb, + control); + if (o == NULL) { + g_warning ("pa_context_get_server_info() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_client_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_client_info_list (control->priv->pa_context, + _pa_context_get_client_info_cb, + control); + } else { + o = pa_context_get_client_info (control->priv->pa_context, + index, + _pa_context_get_client_info_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_client_info_list() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_card (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_card_info_list (control->priv->pa_context, + _pa_context_get_card_info_by_index_cb, + control); + } else { + o = pa_context_get_card_info_by_index (control->priv->pa_context, + index, + _pa_context_get_card_info_by_index_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_get_card_info_by_index() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_sink_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_sink_info_list (control->priv->pa_context, + _pa_context_get_sink_info_cb, + control); + } else { + o = pa_context_get_sink_info_by_index (control->priv->pa_context, + index, + _pa_context_get_sink_info_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_get_sink_info_list() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_source_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_source_info_list (control->priv->pa_context, + _pa_context_get_source_info_cb, + control); + } else { + o = pa_context_get_source_info_by_index(control->priv->pa_context, + index, + _pa_context_get_source_info_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_get_source_info_list() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_sink_input_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_sink_input_info_list (control->priv->pa_context, + _pa_context_get_sink_input_info_cb, + control); + } else { + o = pa_context_get_sink_input_info (control->priv->pa_context, + index, + _pa_context_get_sink_input_info_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_get_sink_input_info_list() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_source_output_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_source_output_info_list (control->priv->pa_context, + _pa_context_get_source_output_info_cb, + control); + } else { + o = pa_context_get_source_output_info (control->priv->pa_context, + index, + _pa_context_get_source_output_info_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_get_source_output_info_list() failed"); + return; + } + pa_operation_unref (o); +} + +static void +remove_client (GvcMixerControl *control, + guint index) +{ + g_hash_table_remove (control->priv->clients, + GUINT_TO_POINTER (index)); +} + +static void +remove_card (GvcMixerControl *control, + guint index) +{ + + GList *devices, *d; + + devices = g_list_concat (g_hash_table_get_values (control->priv->ui_inputs), + g_hash_table_get_values (control->priv->ui_outputs)); + + for (d = devices; d != NULL; d = d->next) { + GvcMixerCard *card; + GvcMixerUIDevice *device = d->data; + + g_object_get (G_OBJECT (device), "card", &card, NULL); + + if (gvc_mixer_card_get_index (card) == index) { + g_signal_emit (G_OBJECT (control), + signals[gvc_mixer_ui_device_is_output (device) ? OUTPUT_REMOVED : INPUT_REMOVED], + 0, + gvc_mixer_ui_device_get_id (device)); + g_debug ("Card removal remove device %s", + gvc_mixer_ui_device_get_description (device)); + g_hash_table_remove (gvc_mixer_ui_device_is_output (device) ? control->priv->ui_outputs : control->priv->ui_inputs, + GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (device))); + } + } + + g_list_free (devices); + + g_hash_table_remove (control->priv->cards, + GUINT_TO_POINTER (index)); + + g_signal_emit (G_OBJECT (control), + signals[CARD_REMOVED], + 0, + index); +} + +static void +remove_sink (GvcMixerControl *control, + guint index) +{ + GvcMixerStream *stream; + GvcMixerUIDevice *device; + + g_debug ("Removing sink: index=%u", index); + + stream = g_hash_table_lookup (control->priv->sinks, + GUINT_TO_POINTER (index)); + if (stream == NULL) + return; + + device = gvc_mixer_control_lookup_device_from_stream (control, stream); + + if (device != NULL) { + gvc_mixer_ui_device_invalidate_stream (device); + if (!gvc_mixer_ui_device_has_ports (device)) { + g_signal_emit (G_OBJECT (control), + signals[OUTPUT_REMOVED], + 0, + gvc_mixer_ui_device_get_id (device)); + } else { + GList *devices, *d; + + devices = g_hash_table_get_values (control->priv->ui_outputs); + + for (d = devices; d != NULL; d = d->next) { + gint 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) { + gint 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; + } +} + +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 [CARD_ADDED] = + g_signal_new ("card-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, card_added), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [CARD_REMOVED] = + g_signal_new ("card-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, card_removed), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [DEFAULT_SINK_CHANGED] = + g_signal_new ("default-sink-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, default_sink_changed), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [DEFAULT_SOURCE_CHANGED] = + g_signal_new ("default-source-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, default_source_changed), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [ACTIVE_OUTPUT_UPDATE] = + g_signal_new ("active-output-update", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, active_output_update), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [ACTIVE_INPUT_UPDATE] = + g_signal_new ("active-input-update", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, active_input_update), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [OUTPUT_ADDED] = + g_signal_new ("output-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, output_added), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [INPUT_ADDED] = + g_signal_new ("input-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, input_added), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [OUTPUT_REMOVED] = + g_signal_new ("output-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, output_removed), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [INPUT_REMOVED] = + g_signal_new ("input-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, input_removed), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + g_type_class_add_private (klass, sizeof (GvcMixerControlPrivate)); +} + + +static void +gvc_mixer_control_init (GvcMixerControl *control) +{ + control->priv = GVC_MIXER_CONTROL_GET_PRIVATE (control); + + control->priv->pa_mainloop = pa_glib_mainloop_new (g_main_context_default ()); + g_assert (control->priv->pa_mainloop); + + control->priv->pa_api = pa_glib_mainloop_get_api (control->priv->pa_mainloop); + g_assert (control->priv->pa_api); + + control->priv->all_streams = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->sinks = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->sources = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->sink_inputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->source_outputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->cards = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->ui_outputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->ui_inputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + + control->priv->clients = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_free); + + 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; +} diff --git a/gvc-mixer-control.h b/gvc-mixer-control.h new file mode 100644 index 0000000..e5e2f74 --- /dev/null +++ b/gvc-mixer-control.h @@ -0,0 +1,136 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * 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. + * + */ + +#ifndef __GVC_MIXER_CONTROL_H +#define __GVC_MIXER_CONTROL_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" +#include "gvc-mixer-card.h" +#include "gvc-mixer-ui-device.h" + +G_BEGIN_DECLS + +typedef enum +{ + GVC_STATE_CLOSED, + GVC_STATE_READY, + GVC_STATE_CONNECTING, + GVC_STATE_FAILED +} GvcMixerControlState; + +#define GVC_TYPE_MIXER_CONTROL (gvc_mixer_control_get_type ()) +#define GVC_MIXER_CONTROL(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_CONTROL, GvcMixerControl)) +#define GVC_MIXER_CONTROL_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_CONTROL, GvcMixerControlClass)) +#define GVC_IS_MIXER_CONTROL(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_CONTROL)) +#define GVC_IS_MIXER_CONTROL_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_CONTROL)) +#define GVC_MIXER_CONTROL_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_CONTROL, GvcMixerControlClass)) + +typedef struct GvcMixerControlPrivate GvcMixerControlPrivate; + +typedef struct +{ + GObject parent; + GvcMixerControlPrivate *priv; +} GvcMixerControl; + +typedef struct +{ + GObjectClass parent_class; + + void (*state_changed) (GvcMixerControl *control, + GvcMixerControlState new_state); + void (*stream_added) (GvcMixerControl *control, + guint id); + void (*stream_removed) (GvcMixerControl *control, + guint id); + void (*card_added) (GvcMixerControl *control, + guint id); + void (*card_removed) (GvcMixerControl *control, + guint id); + void (*default_sink_changed) (GvcMixerControl *control, + guint id); + void (*default_source_changed) (GvcMixerControl *control, + guint id); + void (*active_output_update) (GvcMixerControl *control, + guint id); + void (*active_input_update) (GvcMixerControl *control, + guint id); + void (*output_added) (GvcMixerControl *control, + guint id); + void (*input_added) (GvcMixerControl *control, + guint id); + void (*output_removed) (GvcMixerControl *control, + guint id); + void (*input_removed) (GvcMixerControl *control, + guint id); +} GvcMixerControlClass; + +GType gvc_mixer_control_get_type (void); + +GvcMixerControl * gvc_mixer_control_new (const char *name); + +gboolean gvc_mixer_control_open (GvcMixerControl *control); +gboolean gvc_mixer_control_close (GvcMixerControl *control); + +GSList * gvc_mixer_control_get_cards (GvcMixerControl *control); +GSList * gvc_mixer_control_get_streams (GvcMixerControl *control); +GSList * gvc_mixer_control_get_sinks (GvcMixerControl *control); +GSList * gvc_mixer_control_get_sources (GvcMixerControl *control); +GSList * gvc_mixer_control_get_sink_inputs (GvcMixerControl *control); +GSList * gvc_mixer_control_get_source_outputs (GvcMixerControl *control); + +GvcMixerStream * gvc_mixer_control_lookup_stream_id (GvcMixerControl *control, + guint id); +GvcMixerCard * gvc_mixer_control_lookup_card_id (GvcMixerControl *control, + guint id); +GvcMixerUIDevice * gvc_mixer_control_lookup_output_id (GvcMixerControl *control, + guint id); +GvcMixerUIDevice * gvc_mixer_control_lookup_input_id (GvcMixerControl *control, + guint id); +GvcMixerUIDevice * gvc_mixer_control_lookup_device_from_stream (GvcMixerControl *control, + GvcMixerStream *stream); + +GvcMixerStream * gvc_mixer_control_get_default_sink (GvcMixerControl *control); +GvcMixerStream * gvc_mixer_control_get_default_source (GvcMixerControl *control); +GvcMixerStream * gvc_mixer_control_get_event_sink_input (GvcMixerControl *control); + +gboolean gvc_mixer_control_set_default_sink (GvcMixerControl *control, + GvcMixerStream *stream); +gboolean gvc_mixer_control_set_default_source (GvcMixerControl *control, + GvcMixerStream *stream); + +gdouble gvc_mixer_control_get_vol_max_norm (GvcMixerControl *control); +gdouble gvc_mixer_control_get_vol_max_amplified (GvcMixerControl *control); +void gvc_mixer_control_change_output (GvcMixerControl *control, + GvcMixerUIDevice* output); +void gvc_mixer_control_change_input (GvcMixerControl *control, + GvcMixerUIDevice* input); +GvcMixerStream* gvc_mixer_control_get_stream_from_device (GvcMixerControl *control, + GvcMixerUIDevice *dev); +gboolean gvc_mixer_control_change_profile_on_selected_device (GvcMixerControl *control, + GvcMixerUIDevice *device, + const gchar* profile); + +GvcMixerControlState gvc_mixer_control_get_state (GvcMixerControl *control); + +G_END_DECLS + +#endif /* __GVC_MIXER_CONTROL_H */ diff --git a/gvc-mixer-dialog.c b/gvc-mixer-dialog.c new file mode 100644 index 0000000..ea95825 --- /dev/null +++ b/gvc-mixer-dialog.c @@ -0,0 +1,1977 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * 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 <math.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> +#include <gdk/gdkkeysyms.h> +#include <gtk/gtk.h> +#include <pulse/pulseaudio.h> + +#include "gvc-channel-bar.h" +#include "gvc-balance-bar.h" +#include "gvc-combo-box.h" +#include "gvc-mixer-control.h" +#include "gvc-mixer-card.h" +#include "gvc-mixer-sink.h" +#include "gvc-mixer-source.h" +#include "gvc-mixer-source-output.h" +#include "gvc-mixer-dialog.h" +#include "gvc-sound-theme-chooser.h" +#include "gvc-level-bar.h" +#include "gvc-speaker-test.h" +#include "gvc-mixer-control-private.h" + +#define SCALE_SIZE 128 + +#define GVC_MIXER_DIALOG_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GVC_TYPE_MIXER_DIALOG, GvcMixerDialogPrivate)) + +struct GvcMixerDialogPrivate +{ + GvcMixerControl *mixer_control; + GHashTable *bars; /* Application and event bars only */ + GtkWidget *notebook; + GtkWidget *output_bar; + GtkWidget *input_bar; + GtkWidget *input_level_bar; + GtkWidget *effects_bar; + GtkWidget *output_stream_box; + GtkWidget *sound_effects_box; + GtkWidget *input_box; + GtkWidget *output_box; + GtkWidget *applications_box; + GtkWidget *no_apps_label; + GtkWidget *output_treeview; + GtkWidget *output_settings_box; + GtkWidget *output_balance_bar; + GtkWidget *output_fade_bar; + GtkWidget *output_lfe_bar; + GtkWidget *output_profile_combo; + GtkWidget *input_treeview; + GtkWidget *input_profile_combo; + GtkWidget *input_settings_box; + GtkWidget *sound_theme_chooser; + GtkWidget *click_feedback_button; + GtkWidget *audible_bell_button; + GtkSizeGroup *size_group; + + gdouble last_input_peak; + guint num_apps; +}; + +enum { + NAME_COLUMN, + DEVICE_COLUMN, + ACTIVE_COLUMN, + ID_COLUMN, + ICON_COLUMN, + NUM_COLUMNS +}; + +enum { + HW_ID_COLUMN, + HW_ICON_COLUMN, + HW_NAME_COLUMN, + HW_STATUS_COLUMN, + HW_PROFILE_COLUMN, + HW_PROFILE_HUMAN_COLUMN, + HW_SENSITIVE_COLUMN, + HW_NUM_COLUMNS +}; + +enum +{ + PROP_0, + PROP_MIXER_CONTROL +}; + +static void gvc_mixer_dialog_class_init (GvcMixerDialogClass *klass); +static void gvc_mixer_dialog_init (GvcMixerDialog *mixer_dialog); +static void gvc_mixer_dialog_finalize (GObject *object); + +static void bar_set_stream (GvcMixerDialog *dialog, + GtkWidget *bar, + GvcMixerStream *stream); + +static void on_adjustment_value_changed (GtkAdjustment *adjustment, + GvcMixerDialog *dialog); +static void on_control_active_output_update (GvcMixerControl *control, + guint id, + GvcMixerDialog *dialog); + +static void on_control_active_input_update (GvcMixerControl *control, + guint id, + GvcMixerDialog *dialog); + +static void on_test_speakers_clicked (GvcComboBox *widget, + gpointer user_data); + + +G_DEFINE_TYPE (GvcMixerDialog, gvc_mixer_dialog, GTK_TYPE_VBOX) + +static void +profile_selection_changed (GvcComboBox *combo_box, + const char *profile, + GvcMixerDialog *dialog) +{ + GvcMixerUIDevice *output; + + g_debug ("profile_selection_changed() to %s", profile); + + output = g_object_get_data (G_OBJECT (combo_box), "uidevice"); + + if (output == NULL) { + g_warning ("Could not find Output for profile combo box"); + return; + } + + g_debug ("on profile selection changed on output '%s' (origin: %s, id: %i)", + gvc_mixer_ui_device_get_description (output), + gvc_mixer_ui_device_get_origin (output), + gvc_mixer_ui_device_get_id (output)); + + if (gvc_mixer_control_change_profile_on_selected_device (dialog->priv->mixer_control, output, profile) == FALSE) { + g_warning ("Could not change profile on device %s", + gvc_mixer_ui_device_get_description (output)); + } +} + +static void +update_output_settings (GvcMixerDialog *dialog, + GvcMixerUIDevice *device) +{ + GvcMixerStream *stream; + const GvcChannelMap *map; + const GList *profiles; + GtkAdjustment *adj; + + g_debug ("Updating output settings"); + if (dialog->priv->output_balance_bar != NULL) { + gtk_container_remove (GTK_CONTAINER (dialog->priv->output_settings_box), + dialog->priv->output_balance_bar); + dialog->priv->output_balance_bar = NULL; + } + if (dialog->priv->output_fade_bar != NULL) { + gtk_container_remove (GTK_CONTAINER (dialog->priv->output_settings_box), + dialog->priv->output_fade_bar); + dialog->priv->output_fade_bar = NULL; + } + if (dialog->priv->output_lfe_bar != NULL) { + gtk_container_remove (GTK_CONTAINER (dialog->priv->output_settings_box), + dialog->priv->output_lfe_bar); + dialog->priv->output_lfe_bar = NULL; + } + if (dialog->priv->output_profile_combo != NULL) { + gtk_container_remove (GTK_CONTAINER (dialog->priv->output_settings_box), + dialog->priv->output_profile_combo); + dialog->priv->output_profile_combo = NULL; + } + + stream = gvc_mixer_control_get_stream_from_device (dialog->priv->mixer_control, + device); + if (stream == NULL) { + g_warning ("Default sink stream not found"); + return; + } + + gvc_channel_bar_set_base_volume (GVC_CHANNEL_BAR (dialog->priv->output_bar), + gvc_mixer_stream_get_base_volume (stream)); + gvc_channel_bar_set_is_amplified (GVC_CHANNEL_BAR (dialog->priv->output_bar), + gvc_mixer_stream_get_can_decibel (stream)); + + /* Update the adjustment in case the previous bar wasn't decibel + * capable, and we clipped it */ + adj = GTK_ADJUSTMENT (gvc_channel_bar_get_adjustment (GVC_CHANNEL_BAR (dialog->priv->output_bar))); + gtk_adjustment_set_value (adj, + gvc_mixer_stream_get_volume (stream)); + + map = gvc_mixer_stream_get_channel_map (stream); + if (map == NULL) { + g_warning ("Default sink stream has no channel map"); + return; + } + + dialog->priv->output_balance_bar = gvc_balance_bar_new (map, BALANCE_TYPE_RL); + if (dialog->priv->size_group != NULL) { + gvc_balance_bar_set_size_group (GVC_BALANCE_BAR (dialog->priv->output_balance_bar), + dialog->priv->size_group, + TRUE); + } + gtk_box_pack_start (GTK_BOX (dialog->priv->output_settings_box), + dialog->priv->output_balance_bar, + FALSE, FALSE, 6); + gtk_widget_show (dialog->priv->output_balance_bar); + + if (gvc_channel_map_can_fade (map)) { + dialog->priv->output_fade_bar = gvc_balance_bar_new (map, BALANCE_TYPE_FR); + if (dialog->priv->size_group != NULL) { + gvc_balance_bar_set_size_group (GVC_BALANCE_BAR (dialog->priv->output_fade_bar), + dialog->priv->size_group, + TRUE); + } + gtk_box_pack_start (GTK_BOX (dialog->priv->output_settings_box), + dialog->priv->output_fade_bar, + FALSE, FALSE, 6); + gtk_widget_show (dialog->priv->output_fade_bar); + } + + if (gvc_channel_map_has_lfe (map)) { + dialog->priv->output_lfe_bar = gvc_balance_bar_new (map, BALANCE_TYPE_LFE); + if (dialog->priv->size_group != NULL) { + gvc_balance_bar_set_size_group (GVC_BALANCE_BAR (dialog->priv->output_lfe_bar), + dialog->priv->size_group, + TRUE); + } + gtk_box_pack_start (GTK_BOX (dialog->priv->output_settings_box), + dialog->priv->output_lfe_bar, + FALSE, FALSE, 6); + gtk_widget_show (dialog->priv->output_lfe_bar); + } + + profiles = gvc_mixer_ui_device_get_profiles (device); + /* FIXME: How do we make sure the "Test speakers" button is shown + * even when there are no profiles to choose between? */ + if (TRUE /*g_list_length((GList *) profiles) >= 2 */) { + const gchar *active_profile; + + dialog->priv->output_profile_combo = gvc_combo_box_new (_("_Profile:")); + + g_object_set (G_OBJECT (dialog->priv->output_profile_combo), "button-label", _("_Test Speakers"), NULL); + g_object_set (G_OBJECT (dialog->priv->output_profile_combo), + "show-button", TRUE, NULL); + g_signal_connect (G_OBJECT (dialog->priv->output_profile_combo), "button-clicked", + G_CALLBACK (on_test_speakers_clicked), dialog); + + if (profiles) + gvc_combo_box_set_profiles (GVC_COMBO_BOX (dialog->priv->output_profile_combo), + profiles); + gtk_box_pack_start (GTK_BOX (dialog->priv->output_settings_box), + dialog->priv->output_profile_combo, + TRUE, FALSE, 6); + + if (dialog->priv->size_group != NULL) { + gvc_combo_box_set_size_group (GVC_COMBO_BOX (dialog->priv->output_profile_combo), + dialog->priv->size_group, FALSE); + } + + active_profile = gvc_mixer_ui_device_get_active_profile (device); + if (active_profile) + gvc_combo_box_set_active (GVC_COMBO_BOX (dialog->priv->output_profile_combo), active_profile); + + g_object_set_data (G_OBJECT (dialog->priv->output_profile_combo), + "uidevice", + device); + if (g_list_length((GList *) profiles)) + g_signal_connect (G_OBJECT (dialog->priv->output_profile_combo), "changed", + G_CALLBACK (profile_selection_changed), dialog); + + gtk_widget_show (dialog->priv->output_profile_combo); + } + + /* FIXME: We could make this into a "No settings" label instead */ + gtk_widget_set_sensitive (dialog->priv->output_balance_bar, gvc_channel_map_can_balance (map)); +} + +#define DECAY_STEP .15 + +static void +update_input_peak (GvcMixerDialog *dialog, + gdouble v) +{ + GtkAdjustment *adj; + + if (dialog->priv->last_input_peak >= DECAY_STEP) { + if (v < dialog->priv->last_input_peak - DECAY_STEP) { + v = dialog->priv->last_input_peak - DECAY_STEP; + } + } + + dialog->priv->last_input_peak = v; + + adj = gvc_level_bar_get_peak_adjustment (GVC_LEVEL_BAR (dialog->priv->input_level_bar)); + if (v >= 0) { + gtk_adjustment_set_value (adj, v); + } else { + gtk_adjustment_set_value (adj, 0.0); + } +} + +static void +update_input_meter (GvcMixerDialog *dialog, + uint32_t source_index, + uint32_t sink_input_idx, + double v) +{ + update_input_peak (dialog, v); +} + +static void +on_monitor_suspended_callback (pa_stream *s, + void *userdata) +{ + GvcMixerDialog *dialog; + + dialog = userdata; + + if (pa_stream_is_suspended (s)) { + g_debug ("Stream suspended"); + update_input_meter (dialog, + pa_stream_get_device_index (s), + PA_INVALID_INDEX, + -1); + } +} + +static void +on_monitor_read_callback (pa_stream *s, + size_t length, + void *userdata) +{ + GvcMixerDialog *dialog; + const void *data; + double v; + + dialog = userdata; + + if (pa_stream_peek (s, &data, &length) < 0) { + g_warning ("Failed to read data from stream"); + return; + } + + assert (length > 0); + assert (length % sizeof (float) == 0); + + v = ((const float *) data)[length / sizeof (float) -1]; + + pa_stream_drop (s); + + if (v < 0) { + v = 0; + } + if (v > 1) { + v = 1; + } + + update_input_meter (dialog, + pa_stream_get_device_index (s), + pa_stream_get_monitor_stream (s), + v); +} + +static void +create_monitor_stream_for_source (GvcMixerDialog *dialog, + GvcMixerStream *stream) +{ + pa_stream *s; + char t[16]; + pa_buffer_attr attr; + pa_sample_spec ss; + pa_context *context; + int res; + pa_proplist *proplist; + gboolean has_monitor; + + if (stream == NULL) { + return; + } + has_monitor = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (stream), "has-monitor")); + if (has_monitor != FALSE) { + return; + } + + g_debug ("Create monitor for %u", + gvc_mixer_stream_get_index (stream)); + + context = gvc_mixer_control_get_pa_context (dialog->priv->mixer_control); + + if (pa_context_get_server_protocol_version (context) < 13) { + return; + } + + ss.channels = 1; + ss.format = PA_SAMPLE_FLOAT32; + ss.rate = 25; + + memset (&attr, 0, sizeof (attr)); + attr.fragsize = sizeof (float); + attr.maxlength = (uint32_t) -1; + + snprintf (t, sizeof (t), "%u", gvc_mixer_stream_get_index (stream)); + + proplist = pa_proplist_new (); + pa_proplist_sets (proplist, PA_PROP_APPLICATION_ID, "org.gnome.VolumeControl"); + s = pa_stream_new_with_proplist (context, _("Peak detect"), &ss, NULL, proplist); + pa_proplist_free (proplist); + if (s == NULL) { + g_warning ("Failed to create monitoring stream"); + return; + } + + pa_stream_set_read_callback (s, on_monitor_read_callback, dialog); + pa_stream_set_suspended_callback (s, on_monitor_suspended_callback, dialog); + + res = pa_stream_connect_record (s, + t, + &attr, + (pa_stream_flags_t) (PA_STREAM_DONT_MOVE + |PA_STREAM_PEAK_DETECT + |PA_STREAM_ADJUST_LATENCY)); + if (res < 0) { + g_warning ("Failed to connect monitoring stream"); + pa_stream_unref (s); + } else { + g_object_set_data (G_OBJECT (stream), "has-monitor", GINT_TO_POINTER (TRUE)); + g_object_set_data (G_OBJECT (dialog->priv->input_level_bar), "pa_stream", s); + g_object_set_data (G_OBJECT (dialog->priv->input_level_bar), "stream", stream); + } +} + +static void +stop_monitor_stream_for_source (GvcMixerDialog *dialog) +{ + pa_stream *s; + pa_context *context; + int res; + GvcMixerStream *stream; + + s = g_object_get_data (G_OBJECT (dialog->priv->input_level_bar), "pa_stream"); + if (s == NULL) + return; + stream = g_object_get_data (G_OBJECT (dialog->priv->input_level_bar), "stream"); + g_assert (stream != NULL); + + g_debug ("Stopping monitor for %u", pa_stream_get_index (s)); + + context = gvc_mixer_control_get_pa_context (dialog->priv->mixer_control); + + if (pa_context_get_server_protocol_version (context) < 13) { + return; + } + + res = pa_stream_disconnect (s); + if (res == 0) + g_object_set_data (G_OBJECT (stream), "has-monitor", GINT_TO_POINTER (FALSE)); + g_object_set_data (G_OBJECT (dialog->priv->input_level_bar), "pa_stream", NULL); + g_object_set_data (G_OBJECT (dialog->priv->input_level_bar), "stream", NULL); +} + +static void +update_input_settings (GvcMixerDialog *dialog, + GvcMixerUIDevice *device) +{ + GvcMixerStream *stream; + const GList *profiles; + GtkAdjustment *adj; + + g_debug ("Updating input settings"); + + stop_monitor_stream_for_source (dialog); + + if (dialog->priv->input_profile_combo != NULL) { + gtk_container_remove (GTK_CONTAINER (dialog->priv->input_settings_box), + dialog->priv->input_profile_combo); + dialog->priv->input_profile_combo = NULL; + } + + stream = gvc_mixer_control_get_stream_from_device (dialog->priv->mixer_control, + device); + if (stream == NULL) { + g_debug ("Default source stream not found"); + return; + } + + gvc_channel_bar_set_base_volume (GVC_CHANNEL_BAR (dialog->priv->input_bar), + gvc_mixer_stream_get_base_volume (stream)); + gvc_channel_bar_set_is_amplified (GVC_CHANNEL_BAR (dialog->priv->input_bar), + gvc_mixer_stream_get_can_decibel (stream)); + + /* Update the adjustment in case the previous bar wasn't decibel + * capable, and we clipped it */ + adj = GTK_ADJUSTMENT (gvc_channel_bar_get_adjustment (GVC_CHANNEL_BAR (dialog->priv->input_bar))); + gtk_adjustment_set_value (adj, + gvc_mixer_stream_get_volume (stream)); + + profiles = gvc_mixer_ui_device_get_profiles (device); + if (g_list_length ((GList *) profiles) >= 2) { + const gchar *active_profile; + + dialog->priv->input_profile_combo = gvc_combo_box_new (_("_Profile:")); + gvc_combo_box_set_profiles (GVC_COMBO_BOX (dialog->priv->input_profile_combo), + profiles); + + gtk_box_pack_start (GTK_BOX (dialog->priv->input_settings_box), + dialog->priv->input_profile_combo, + TRUE, TRUE, 0); + + if (dialog->priv->size_group != NULL) { + gvc_combo_box_set_size_group (GVC_COMBO_BOX (dialog->priv->input_profile_combo), + dialog->priv->size_group, FALSE); + } + + active_profile = gvc_mixer_ui_device_get_active_profile (device); + if (active_profile) + gvc_combo_box_set_active (GVC_COMBO_BOX (dialog->priv->input_profile_combo), active_profile); + + g_object_set_data (G_OBJECT (dialog->priv->input_profile_combo), + "uidevice", + device); + g_signal_connect (G_OBJECT (dialog->priv->input_profile_combo), "changed", + G_CALLBACK (profile_selection_changed), dialog); + + gtk_widget_show (dialog->priv->input_profile_combo); + } + + create_monitor_stream_for_source (dialog, stream); +} + +static void +gvc_mixer_dialog_set_mixer_control (GvcMixerDialog *dialog, + GvcMixerControl *control) +{ + g_return_if_fail (GVC_MIXER_DIALOG (dialog)); + g_return_if_fail (GVC_IS_MIXER_CONTROL (control)); + + g_object_ref (control); + + if (dialog->priv->mixer_control != NULL) { + g_signal_handlers_disconnect_by_func (dialog->priv->mixer_control, + G_CALLBACK (on_control_active_input_update), + dialog); + g_signal_handlers_disconnect_by_func (dialog->priv->mixer_control, + G_CALLBACK (on_control_active_output_update), + dialog); + g_object_unref (dialog->priv->mixer_control); + } + + dialog->priv->mixer_control = control; + + /* FIXME: Why are some mixer_control signals connected here, + * and others in the dialog constructor? (And similar for disconnect) */ + g_signal_connect (dialog->priv->mixer_control, + "active-input-update", + G_CALLBACK (on_control_active_input_update), + dialog); + g_signal_connect (dialog->priv->mixer_control, + "active-output-update", + G_CALLBACK (on_control_active_output_update), + dialog); + + g_object_notify (G_OBJECT (dialog), "mixer-control"); +} + +static GvcMixerControl * +gvc_mixer_dialog_get_mixer_control (GvcMixerDialog *dialog) +{ + g_return_val_if_fail (GVC_IS_MIXER_DIALOG (dialog), NULL); + + return dialog->priv->mixer_control; +} + +static void +gvc_mixer_dialog_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GvcMixerDialog *self = GVC_MIXER_DIALOG (object); + + switch (prop_id) { + case PROP_MIXER_CONTROL: + gvc_mixer_dialog_set_mixer_control (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gvc_mixer_dialog_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GvcMixerDialog *self = GVC_MIXER_DIALOG (object); + + switch (prop_id) { + case PROP_MIXER_CONTROL: + g_value_set_object (value, gvc_mixer_dialog_get_mixer_control (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +on_adjustment_value_changed (GtkAdjustment *adjustment, + GvcMixerDialog *dialog) +{ + GvcMixerStream *stream; + + stream = g_object_get_data (G_OBJECT (adjustment), "gvc-mixer-dialog-stream"); + if (stream != NULL) { + GObject *bar; + gdouble volume, rounded; + char *name; + + volume = gtk_adjustment_get_value (adjustment); + rounded = round (volume); + + bar = g_object_get_data (G_OBJECT (adjustment), "gvc-mixer-dialog-bar"); + g_object_get (bar, "name", &name, NULL); + g_debug ("Setting stream volume %lf (rounded: %lf) for bar '%s'", volume, rounded, name); + g_free (name); + + /* FIXME would need to do that in the balance bar really... */ + /* Make sure we do not unmute muted streams, there's a button for that */ + if (volume == 0.0) + gvc_mixer_stream_set_is_muted (stream, TRUE); + /* Only push the volume if it's actually changed */ + if (gvc_mixer_stream_set_volume (stream, (pa_volume_t) rounded) != FALSE) + gvc_mixer_stream_push_volume (stream); + } +} + +static void +on_bar_is_muted_notify (GObject *object, + GParamSpec *pspec, + GvcMixerDialog *dialog) +{ + gboolean is_muted; + GvcMixerStream *stream; + + is_muted = gvc_channel_bar_get_is_muted (GVC_CHANNEL_BAR (object)); + + stream = g_object_get_data (object, "gvc-mixer-dialog-stream"); + if (stream != NULL) { + gvc_mixer_stream_change_is_muted (stream, is_muted); + } else { + char *name; + g_object_get (object, "name", &name, NULL); + g_warning ("Unable to find stream for bar '%s'", name); + g_free (name); + } +} + +static GtkWidget * +lookup_bar_for_stream (GvcMixerDialog *dialog, + GvcMixerStream *stream) +{ + GtkWidget *bar; + + bar = g_hash_table_lookup (dialog->priv->bars, GUINT_TO_POINTER (gvc_mixer_stream_get_id (stream))); + + return bar; +} + +static void +on_stream_volume_notify (GObject *object, + GParamSpec *pspec, + GvcMixerDialog *dialog) +{ + GvcMixerStream *stream; + GtkWidget *bar; + GtkAdjustment *adj; + + stream = GVC_MIXER_STREAM (object); + + bar = lookup_bar_for_stream (dialog, stream); + + if (bar == NULL) { + g_warning ("Unable to find bar for stream %s in on_stream_volume_notify()", + gvc_mixer_stream_get_name (stream)); + return; + } + + adj = GTK_ADJUSTMENT (gvc_channel_bar_get_adjustment (GVC_CHANNEL_BAR (bar))); + + g_signal_handlers_block_by_func (adj, + on_adjustment_value_changed, + dialog); + + gtk_adjustment_set_value (adj, + gvc_mixer_stream_get_volume (stream)); + + g_signal_handlers_unblock_by_func (adj, + on_adjustment_value_changed, + dialog); +} + +static void +on_stream_is_muted_notify (GObject *object, + GParamSpec *pspec, + GvcMixerDialog *dialog) +{ + GvcMixerStream *stream; + GtkWidget *bar; + gboolean is_muted; + + stream = GVC_MIXER_STREAM (object); + bar = lookup_bar_for_stream (dialog, stream); + + if (bar == NULL) { + g_warning ("Unable to find bar for stream %s in on_stream_is_muted_notify()", + gvc_mixer_stream_get_name (stream)); + return; + } + + is_muted = gvc_mixer_stream_get_is_muted (stream); + gvc_channel_bar_set_is_muted (GVC_CHANNEL_BAR (bar), + is_muted); + + if (stream == gvc_mixer_control_get_default_sink (dialog->priv->mixer_control)) { + gtk_widget_set_sensitive (dialog->priv->applications_box, + !is_muted); + } + +} + +static void +save_bar_for_stream (GvcMixerDialog *dialog, + GvcMixerStream *stream, + GtkWidget *bar) +{ + g_hash_table_insert (dialog->priv->bars, + GUINT_TO_POINTER (gvc_mixer_stream_get_id (stream)), + bar); +} + +static GtkWidget * +create_bar (GvcMixerDialog *dialog, + gboolean add_to_size_group, + gboolean symmetric) +{ + GtkWidget *bar; + + bar = gvc_channel_bar_new (); + gtk_widget_set_sensitive (bar, FALSE); + if (add_to_size_group && dialog->priv->size_group != NULL) { + gvc_channel_bar_set_size_group (GVC_CHANNEL_BAR (bar), + dialog->priv->size_group, + symmetric); + } + gvc_channel_bar_set_orientation (GVC_CHANNEL_BAR (bar), + GTK_ORIENTATION_HORIZONTAL); + gvc_channel_bar_set_show_mute (GVC_CHANNEL_BAR (bar), + TRUE); + g_signal_connect (bar, + "notify::is-muted", + G_CALLBACK (on_bar_is_muted_notify), + dialog); + return bar; +} + +static GtkWidget * +create_app_bar (GvcMixerDialog *dialog, + const char *name, + const char *icon_name) +{ + GtkWidget *bar; + + bar = create_bar (dialog, FALSE, FALSE); + gvc_channel_bar_set_ellipsize (GVC_CHANNEL_BAR (bar), TRUE); + gvc_channel_bar_set_icon_name (GVC_CHANNEL_BAR (bar), icon_name); + if (name == NULL || strchr (name, '_') == NULL) { + gvc_channel_bar_set_name (GVC_CHANNEL_BAR (bar), name); + } else { + char **tokens, *escaped; + + tokens = g_strsplit (name, "_", -1); + escaped = g_strjoinv ("__", tokens); + g_strfreev (tokens); + gvc_channel_bar_set_name (GVC_CHANNEL_BAR (bar), escaped); + g_free (escaped); + } + + return bar; +} + +/* active_input_update + * Handle input update change from the backend (control). + * Trust the backend whole-heartedly to deliver the correct input. */ +static void +active_input_update (GvcMixerDialog *dialog, + GvcMixerUIDevice *active_input) +{ + /* First make sure the correct UI device is selected. */ + GtkTreeModel *model; + GtkTreeIter iter; + GvcMixerStream *stream; + + g_debug ("active_input_update device id = %i", + gvc_mixer_ui_device_get_id (active_input)); + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (dialog->priv->input_treeview)); + + if (gtk_tree_model_get_iter_first (model, &iter) == FALSE) { + g_warning ("No devices in the tree, so cannot set the active output"); + return; + } + + do { + gboolean is_selected = FALSE; + gint id; + + gtk_tree_model_get (model, &iter, + ID_COLUMN, &id, + -1); + + is_selected = id == gvc_mixer_ui_device_get_id (active_input); + + gtk_list_store_set (GTK_LIST_STORE (model), + &iter, + ACTIVE_COLUMN, is_selected, + -1); + + if (is_selected) { + GtkTreeSelection *selection; + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (dialog->priv->input_treeview)); + gtk_tree_selection_select_iter (selection, &iter); + } + } while (gtk_tree_model_iter_next (model, &iter)); + + stream = gvc_mixer_control_get_stream_from_device (dialog->priv->mixer_control, + active_input); + if (stream == NULL) { + g_warning ("Couldn't find a stream from the active input"); + gtk_widget_set_sensitive (dialog->priv->input_bar, FALSE); + return; + } + + bar_set_stream (dialog, dialog->priv->input_bar, stream); + update_input_settings (dialog, active_input); + +} + +/* active_output_update + * Handle output update change from the backend (control). + * Trust the backend whole heartedly to deliver the correct output. */ +static void +active_output_update (GvcMixerDialog *dialog, + GvcMixerUIDevice *active_output) +{ + /* First make sure the correct UI device is selected. */ + GvcMixerStream *stream; + GtkTreeModel *model; + GtkTreeIter iter; + + g_debug ("active output update device id = %i", + gvc_mixer_ui_device_get_id (active_output)); + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (dialog->priv->output_treeview)); + + if (gtk_tree_model_get_iter_first (model, &iter) == FALSE){ + g_warning ("No devices in the tree, so cannot set the active output"); + return; + } + + do { + gboolean is_selected; + gint id; + + gtk_tree_model_get (model, &iter, + ID_COLUMN, &id, + ACTIVE_COLUMN, &is_selected, + -1); + + if (is_selected && id == gvc_mixer_ui_device_get_id (active_output)) { + /* XXX: profile change on the same device? */ + g_debug ("Unneccessary active output update"); + } + + is_selected = id == gvc_mixer_ui_device_get_id (active_output); + + gtk_list_store_set (GTK_LIST_STORE (model), + &iter, + ACTIVE_COLUMN, is_selected, + -1); + + if (is_selected) { + GtkTreeSelection *selection; + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (dialog->priv->output_treeview)); + gtk_tree_selection_select_iter (selection, &iter); + } + } while (gtk_tree_model_iter_next (model, &iter)); + + stream = gvc_mixer_control_get_stream_from_device (dialog->priv->mixer_control, + active_output); + if (stream == NULL) { + g_warning ("Couldn't find a stream from the active output"); + return; + } + + bar_set_stream (dialog, dialog->priv->output_bar, stream); + update_output_settings (dialog, active_output); +} + +static void +bar_set_stream (GvcMixerDialog *dialog, + GtkWidget *bar, + GvcMixerStream *stream) +{ + GtkAdjustment *adj; + GvcMixerStream *old_stream; + + g_assert (bar != NULL); + + old_stream = g_object_get_data (G_OBJECT (bar), "gvc-mixer-dialog-stream"); + if (old_stream != NULL) { + char *name; + + g_object_get (bar, "name", &name, NULL); + g_debug ("Disconnecting old stream '%s' from bar '%s'", + gvc_mixer_stream_get_name (old_stream), name); + g_free (name); + + g_signal_handlers_disconnect_by_func (old_stream, on_stream_is_muted_notify, dialog); + g_signal_handlers_disconnect_by_func (old_stream, on_stream_volume_notify, dialog); + g_hash_table_remove (dialog->priv->bars, GUINT_TO_POINTER (gvc_mixer_stream_get_id (old_stream))); + } + + gtk_widget_set_sensitive (bar, (stream != NULL)); + + adj = GTK_ADJUSTMENT (gvc_channel_bar_get_adjustment (GVC_CHANNEL_BAR (bar))); + + g_signal_handlers_disconnect_by_func (adj, on_adjustment_value_changed, dialog); + + g_object_set_data (G_OBJECT (bar), "gvc-mixer-dialog-stream", stream); + g_object_set_data (G_OBJECT (bar), "gvc-mixer-dialog-stream-id", + GUINT_TO_POINTER (gvc_mixer_stream_get_id (stream))); + g_object_set_data (G_OBJECT (adj), "gvc-mixer-dialog-stream", stream); + g_object_set_data (G_OBJECT (adj), "gvc-mixer-dialog-bar", bar); + + if (stream != NULL) { + gboolean is_muted; + + is_muted = gvc_mixer_stream_get_is_muted (stream); + gvc_channel_bar_set_is_muted (GVC_CHANNEL_BAR (bar), is_muted); + + gtk_adjustment_set_value (adj, + gvc_mixer_stream_get_volume (stream)); + + g_signal_connect (stream, + "notify::is-muted", + G_CALLBACK (on_stream_is_muted_notify), + dialog); + g_signal_connect (stream, + "notify::volume", + G_CALLBACK (on_stream_volume_notify), + dialog); + g_signal_connect (adj, + "value-changed", + G_CALLBACK (on_adjustment_value_changed), + dialog); + } +} + +static void +add_stream (GvcMixerDialog *dialog, + GvcMixerStream *stream) +{ + GtkWidget *bar; + GvcMixerStream *old_stream; + + bar = NULL; + + if (GVC_IS_MIXER_SOURCE (stream) || GVC_IS_MIXER_SINK (stream)) + return; + else if (stream == gvc_mixer_control_get_event_sink_input (dialog->priv->mixer_control)) { + bar = dialog->priv->effects_bar; + g_debug ("Adding effects stream"); + } else { + /* Must be an application stream */ + const char *name; + name = gvc_mixer_stream_get_name (stream); + g_debug ("Add bar for application stream : %s", name); + + bar = create_app_bar (dialog, name, + gvc_mixer_stream_get_icon_name (stream)); + gtk_box_pack_start (GTK_BOX (dialog->priv->applications_box), bar, FALSE, FALSE, 12); + dialog->priv->num_apps++; + gtk_widget_hide (dialog->priv->no_apps_label); + } + + /* We should have a bar by now. */ + g_assert (bar != NULL); + + if (bar != NULL) { + old_stream = g_object_get_data (G_OBJECT (bar), "gvc-mixer-dialog-stream"); + if (old_stream != NULL) { + char *name; + + g_object_get (bar, "name", &name, NULL); + g_debug ("Disconnecting old stream '%s' from bar '%s'", + gvc_mixer_stream_get_name (old_stream), name); + g_free (name); + + g_signal_handlers_disconnect_by_func (old_stream, on_stream_is_muted_notify, dialog); + g_signal_handlers_disconnect_by_func (old_stream, on_stream_volume_notify, dialog); + g_hash_table_remove (dialog->priv->bars, GUINT_TO_POINTER (gvc_mixer_stream_get_id (old_stream))); + } + save_bar_for_stream (dialog, stream, bar); + bar_set_stream (dialog, bar, stream); + gtk_widget_show (bar); + } +} + +static void +on_control_stream_added (GvcMixerControl *control, + guint id, + GvcMixerDialog *dialog) +{ + GvcMixerStream *stream; + const char *app_id; + + stream = gvc_mixer_control_lookup_stream_id (control, id); + if (stream == NULL) + return; + + app_id = gvc_mixer_stream_get_application_id (stream); + + if (stream == gvc_mixer_control_get_event_sink_input (dialog->priv->mixer_control) || + (GVC_IS_MIXER_SOURCE (stream) == FALSE && + GVC_IS_MIXER_SINK (stream) == FALSE && + gvc_mixer_stream_is_virtual (stream) == FALSE && + g_strcmp0 (app_id, "org.gnome.VolumeControl") != 0 && + g_strcmp0 (app_id, "org.PulseAudio.pavucontrol") != 0)) { + GtkWidget *bar; + + bar = g_hash_table_lookup (dialog->priv->bars, GUINT_TO_POINTER (id)); + if (bar != NULL) { + g_debug ("GvcMixerDialog: Stream %u already added", id); + return; + } + add_stream (dialog, stream); + } +} + +static gboolean +find_item_by_id (GtkTreeModel *model, + guint id, + guint column, + GtkTreeIter *iter) +{ + gboolean found_item; + + found_item = FALSE; + + if (!gtk_tree_model_get_iter_first (model, iter)) { + return FALSE; + } + + do { + guint t_id; + + gtk_tree_model_get (model, iter, + column, &t_id, -1); + + if (id == t_id) { + found_item = TRUE; + } + } while (!found_item && gtk_tree_model_iter_next (model, iter)); + + return found_item; +} + +static void +add_input_ui_entry (GvcMixerDialog *dialog, + GvcMixerUIDevice *input) +{ + gchar *final_name; + gchar *port_name; + gchar *origin; + gchar *description; + gboolean available; + gint stream_id; + GtkTreeModel *model; + GtkTreeIter iter; + GIcon *icon; + GvcMixerCard *card; + + g_debug ("Add input ui entry with id :%u", + gvc_mixer_ui_device_get_id (input)); + + g_object_get (G_OBJECT (input), + "stream-id", &stream_id, + "card", &card, + "origin", &origin, + "description", &description, + "port-name", &port_name, + "port-available", &available, + NULL); + + if (origin && origin[0] != '\0') + final_name = g_strdup_printf ("%s - %s", description, origin); + else + final_name = g_strdup (description); + + g_free (port_name); + g_free (origin); + g_free (description); + + if (card == NULL) { + GvcMixerStream *stream; + g_debug ("just detected a network source"); + stream = gvc_mixer_control_get_stream_from_device (dialog->priv->mixer_control, input); + if (stream == NULL) { + g_warning ("tried to add the network source but the stream was null - fail ?!"); + g_free (final_name); + return; + } + icon = gvc_mixer_stream_get_gicon (stream); + } else + icon = gvc_mixer_card_get_gicon (card); + + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (dialog->priv->input_treeview)); + gtk_list_store_append (GTK_LIST_STORE (model), &iter); + + gtk_list_store_set (GTK_LIST_STORE (model), + &iter, + NAME_COLUMN, final_name, + DEVICE_COLUMN, "", + ACTIVE_COLUMN, FALSE, + ICON_COLUMN, icon, + ID_COLUMN, gvc_mixer_ui_device_get_id (input), + -1); + + if (icon != NULL) + g_object_unref (icon); + g_free (final_name); +} + +static void +add_output_ui_entry (GvcMixerDialog *dialog, + GvcMixerUIDevice *output) +{ + gchar *sink_port_name; + gchar *origin; + gchar *description; + gchar *final_name; + gboolean available; + gint sink_stream_id; + GtkTreeModel *model; + GtkTreeIter iter; + GIcon *icon; + GvcMixerCard *card; + + g_debug ("Add output ui entry with id :%u", + gvc_mixer_ui_device_get_id (output)); + + g_object_get (G_OBJECT (output), + "stream-id", &sink_stream_id, + "card", &card, + "origin", &origin, + "description", &description, + "port-name", &sink_port_name, + "port-available", &available, + NULL); + + if (origin && origin[0] != '\0') + final_name = g_strdup_printf ("%s - %s", description, origin); + else + final_name = g_strdup (description); + + g_free (sink_port_name); + g_free (origin); + g_free (description); + + if (card == NULL) { + GvcMixerStream *stream; + + g_debug ("just detected a network sink"); + stream = gvc_mixer_control_get_stream_from_device (dialog->priv->mixer_control, output); + + if (stream == NULL) { + g_warning ("tried to add the network sink but the stream was null - fail ?!"); + g_free (final_name); + return; + } + icon = gvc_mixer_stream_get_gicon (stream); + } else + icon = gvc_mixer_card_get_gicon (card); + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (dialog->priv->output_treeview)); + gtk_list_store_append (GTK_LIST_STORE (model), &iter); + + gtk_list_store_set (GTK_LIST_STORE (model), + &iter, + NAME_COLUMN, final_name, + DEVICE_COLUMN, "", + ACTIVE_COLUMN, FALSE, + ICON_COLUMN, icon, + ID_COLUMN, gvc_mixer_ui_device_get_id (output), + -1); + + if (icon != NULL) + g_object_unref (icon); + g_free (final_name); +} + + +static void +on_control_active_input_update (GvcMixerControl *control, + guint id, + GvcMixerDialog *dialog) +{ + GvcMixerUIDevice* in = NULL; + in = gvc_mixer_control_lookup_input_id (control, id); + + if (in == NULL) { + g_warning ("on_control_active_input_update - tried to fetch an input of id %u but got nothing", id); + return; + } + active_input_update (dialog, in); +} + +static void +on_control_active_output_update (GvcMixerControl *control, + guint id, + GvcMixerDialog *dialog) +{ + GvcMixerUIDevice* out = NULL; + out = gvc_mixer_control_lookup_output_id (control, id); + + if (out == NULL) { + g_warning ("on_control_active_output_update - tried to fetch an output of id %u but got nothing", id); + return; + } + active_output_update (dialog, out); +} + +static void +on_control_input_added (GvcMixerControl *control, + guint id, + GvcMixerDialog *dialog) +{ + GvcMixerUIDevice* in = NULL; + in = gvc_mixer_control_lookup_input_id (control, id); + + if (in == NULL) { + g_warning ("on_control_input_added - tried to fetch an input of id %u but got nothing", id); + return; + } + add_input_ui_entry (dialog, in); +} + +static void +on_control_input_removed (GvcMixerControl *control, + guint id, + GvcMixerDialog *dialog) +{ + gboolean found; + GtkTreeIter iter; + GtkTreeModel *model; + gint stream_id; + GvcMixerUIDevice *in; + + in = gvc_mixer_control_lookup_input_id (control, id); + + g_object_get (G_OBJECT (in), + "stream-id", &stream_id, + NULL); + + g_debug ("Remove input from dialog, id: %u, stream id: %i", + id, + stream_id); + + /* remove from any models */ + model = gtk_tree_view_get_model (GTK_TREE_VIEW (dialog->priv->input_treeview)); + found = find_item_by_id (GTK_TREE_MODEL (model), id, ID_COLUMN, &iter); + if (found) { + gtk_list_store_remove (GTK_LIST_STORE (model), &iter); + } +} + +static void +on_control_output_added (GvcMixerControl *control, + guint id, + GvcMixerDialog *dialog) +{ + GvcMixerUIDevice* out = NULL; + out = gvc_mixer_control_lookup_output_id (control, id); + + if (out == NULL) { + g_warning ("on_control_output_added - tried to fetch an output of id %u but got nothing", id); + return; + } + + add_output_ui_entry (dialog, out); +} + +static void +on_control_output_removed (GvcMixerControl *control, + guint id, + GvcMixerDialog *dialog) +{ + gboolean found; + GtkTreeIter iter; + GtkTreeModel *model; + gint sink_stream_id; + + GvcMixerUIDevice* out = NULL; + out = gvc_mixer_control_lookup_output_id (control, id); + + g_object_get (G_OBJECT (out), + "stream-id", &sink_stream_id, + NULL); + + g_debug ("Remove output from dialog \n id : %u \n sink stream id : %i \n", + id, + sink_stream_id); + + /* remove from any models */ + model = gtk_tree_view_get_model (GTK_TREE_VIEW (dialog->priv->output_treeview)); + found = find_item_by_id (GTK_TREE_MODEL (model), id, ID_COLUMN, &iter); + if (found) { + gtk_list_store_remove (GTK_LIST_STORE (model), &iter); + } +} + +static void +remove_stream (GvcMixerDialog *dialog, + guint id) +{ + GtkWidget *bar; + guint output_id, input_id; + + bar = g_hash_table_lookup (dialog->priv->bars, GUINT_TO_POINTER (id)); + if (bar != NULL) { + g_hash_table_remove (dialog->priv->bars, GUINT_TO_POINTER (id)); + gtk_container_remove (GTK_CONTAINER (gtk_widget_get_parent (bar)), + bar); + dialog->priv->num_apps--; + if (dialog->priv->num_apps == 0) { + gtk_widget_show (dialog->priv->no_apps_label); + } + return; + } + + output_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (dialog->priv->output_bar), "gvc-mixer-dialog-stream-id")); + input_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (dialog->priv->input_bar), "gvc-mixer-dialog-stream-id")); + + if (output_id == id) + bar = dialog->priv->output_bar; + else if (input_id == id) + bar = dialog->priv->input_bar; + else + return; + + g_object_set_data (G_OBJECT (bar), "gvc-mixer-dialog-stream-id", NULL); + g_object_set_data (G_OBJECT (bar), "gvc-mixer-dialog-stream", NULL); +} + +static void +on_control_stream_removed (GvcMixerControl *control, + guint id, + GvcMixerDialog *dialog) +{ + remove_stream (dialog, id); +} + +static void +_gtk_label_make_bold (GtkLabel *label) +{ + PangoFontDescription *font_desc; + + font_desc = pango_font_description_new (); + + pango_font_description_set_weight (font_desc, + PANGO_WEIGHT_BOLD); + + /* This will only affect the weight of the font, the rest is + * from the current state of the widget, which comes from the + * theme or user prefs, since the font desc only has the + * weight flag turned on. + */ + gtk_widget_modify_font (GTK_WIDGET (label), font_desc); + + pango_font_description_free (font_desc); +} + +static void +on_input_selection_changed (GtkTreeSelection *selection, + GvcMixerDialog *dialog) +{ + GtkTreeModel *model; + GtkTreeIter iter; + gboolean active; + guint id; + GvcMixerUIDevice *input; + + if (gtk_tree_selection_get_selected (selection, &model, &iter) == FALSE) { + g_debug ("Could not get default input from selection"); + return; + } + + gtk_tree_model_get (model, &iter, + ID_COLUMN, &id, + ACTIVE_COLUMN, &active, + -1); + + input = gvc_mixer_control_lookup_input_id (dialog->priv->mixer_control, id); + + if (input == NULL) { + g_warning ("on_input_selection_changed - Unable to find input with id: %u", id); + return; + } + + gvc_mixer_control_change_input (dialog->priv->mixer_control, input); +} + +static void +on_output_selection_changed (GtkTreeSelection *selection, + GvcMixerDialog *dialog) +{ + GtkTreeModel *model; + GtkTreeIter iter; + gboolean active; + guint id; + GvcMixerUIDevice *output; + + if (gtk_tree_selection_get_selected (selection, &model, &iter) == FALSE) { + g_debug ("Could not get default output from selection"); + return; + } + + gtk_tree_model_get (model, &iter, + ID_COLUMN, &id, + ACTIVE_COLUMN, &active, + -1); + + g_debug ("on_output_selection_changed() stream id: %u, active %i", id, active); + if (active) + return; + + output = gvc_mixer_control_lookup_output_id (dialog->priv->mixer_control, id); + + if (output == NULL) { + g_warning ("Unable to find output with id: %u", id); + return; + } + + gvc_mixer_control_change_output (dialog->priv->mixer_control, output); +} + +static GtkWidget * +create_ui_device_treeview (GvcMixerDialog *dialog, + GCallback on_selection_changed) +{ + GtkWidget *treeview; + GtkListStore *store; + GtkCellRenderer *renderer; + GtkTreeViewColumn *column; + GtkTreeSelection *selection; + + treeview = gtk_tree_view_new (); + gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (treeview), FALSE); + + store = gtk_list_store_new (NUM_COLUMNS, + G_TYPE_STRING, + G_TYPE_STRING, + G_TYPE_BOOLEAN, + G_TYPE_UINT, + G_TYPE_ICON); + gtk_tree_view_set_model (GTK_TREE_VIEW (treeview), + GTK_TREE_MODEL (store)); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview)); + gtk_tree_selection_set_mode (selection, GTK_SELECTION_BROWSE); + + column = gtk_tree_view_column_new (); + gtk_tree_view_column_set_title (column, _("Name")); + renderer = gtk_cell_renderer_pixbuf_new (); + gtk_tree_view_column_pack_start (column, renderer, FALSE); + g_object_set (G_OBJECT (renderer), "stock-size", GTK_ICON_SIZE_LARGE_TOOLBAR, NULL); + gtk_tree_view_column_set_attributes (column, renderer, + "gicon", ICON_COLUMN, + NULL); + + renderer = gtk_cell_renderer_text_new (); + gtk_tree_view_column_pack_start (column, renderer, TRUE); + gtk_tree_view_column_set_attributes (column, renderer, + "text", NAME_COLUMN, + NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column); + + g_signal_connect (G_OBJECT (selection), "changed", + on_selection_changed, dialog); +#if 0 + renderer = gtk_cell_renderer_text_new (); + column = gtk_tree_view_column_new_with_attributes (_("Device"), + renderer, + "text", DEVICE_COLUMN, + NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column); +#endif + return treeview; +} + +static void +on_test_speakers_clicked (GvcComboBox *widget, + gpointer user_data) +{ + GvcMixerDialog *dialog = GVC_MIXER_DIALOG (user_data); + GtkTreeModel *model; + GtkTreeIter iter; + gint stream_id; + gint active_output = GVC_MIXER_UI_DEVICE_INVALID; + GvcMixerUIDevice *output; + GvcMixerStream *stream; + GtkWidget *d, *speaker_test, *container; + char *title; + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (dialog->priv->output_treeview)); + + if (gtk_tree_model_get_iter_first (model, &iter) == FALSE) { + g_warning ("The tree is empty => we have no device to test speakers with return"); + return; + } + + do { + gboolean is_selected = FALSE; + gint id; + + gtk_tree_model_get (model, &iter, + ID_COLUMN, &id, + ACTIVE_COLUMN, &is_selected, + -1); + + if (is_selected) { + active_output = id; + break; + } + } while (gtk_tree_model_iter_next (model, &iter)); + + if (active_output == GVC_MIXER_UI_DEVICE_INVALID) { + g_warning ("Can't find the active output from the UI"); + return; + } + + output = gvc_mixer_control_lookup_output_id (dialog->priv->mixer_control, (guint)active_output); + stream_id = gvc_mixer_ui_device_get_stream_id (output); + + if (stream_id == GVC_MIXER_UI_DEVICE_INVALID) + return; + + g_debug ("Test speakers on '%s'", gvc_mixer_ui_device_get_description (output)); + + stream = gvc_mixer_control_lookup_stream_id (dialog->priv->mixer_control, stream_id); + if (stream == NULL) { + g_debug ("Stream/sink not found"); + return; + } + title = g_strdup_printf (_("Speaker Testing for %s"), gvc_mixer_ui_device_get_description (output)); + d = gtk_dialog_new_with_buttons (title, + GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (widget))), + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, + NULL); + gtk_window_set_has_resize_grip (GTK_WINDOW (d), FALSE); + + g_free (title); + speaker_test = gvc_speaker_test_new (dialog->priv->mixer_control, + stream); + gtk_widget_show (speaker_test); + container = gtk_dialog_get_content_area (GTK_DIALOG (d)); + gtk_container_add (GTK_CONTAINER (container), speaker_test); + + gtk_dialog_run (GTK_DIALOG (d)); + gtk_widget_destroy (d); +} + +static GObject * +gvc_mixer_dialog_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_params) +{ + GObject *object; + GvcMixerDialog *self; + GtkWidget *main_vbox; + GtkWidget *label; + GtkWidget *alignment; + GtkWidget *box; + GtkWidget *sbox; + GtkWidget *ebox; + GSList *streams; + GSList *l; + GvcMixerStream *stream; + + object = G_OBJECT_CLASS (gvc_mixer_dialog_parent_class)->constructor (type, n_construct_properties, construct_params); + + self = GVC_MIXER_DIALOG (object); + + main_vbox = GTK_WIDGET (self); + gtk_box_set_spacing (GTK_BOX (main_vbox), 2); + + gtk_container_set_border_width (GTK_CONTAINER (self), 6); + + self->priv->output_stream_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12); + alignment = gtk_alignment_new (0, 0, 1, 1); + gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 12, 0, 0, 0); + gtk_container_add (GTK_CONTAINER (alignment), self->priv->output_stream_box); + gtk_box_pack_start (GTK_BOX (main_vbox), + alignment, + FALSE, FALSE, 0); + self->priv->output_bar = create_bar (self, TRUE, TRUE); + gvc_channel_bar_set_name (GVC_CHANNEL_BAR (self->priv->output_bar), + _("_Output volume:")); + gtk_widget_set_sensitive (self->priv->output_bar, FALSE); + gtk_box_pack_start (GTK_BOX (self->priv->output_stream_box), + self->priv->output_bar, TRUE, TRUE, 12); + + self->priv->notebook = gtk_notebook_new (); + gtk_box_pack_start (GTK_BOX (main_vbox), + self->priv->notebook, + TRUE, TRUE, 0); + gtk_container_set_border_width (GTK_CONTAINER (self->priv->notebook), 5); + + /* Output page */ + self->priv->output_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); + gtk_container_set_border_width (GTK_CONTAINER (self->priv->output_box), 12); + label = gtk_label_new (_("Output")); + gtk_notebook_append_page (GTK_NOTEBOOK (self->priv->notebook), + self->priv->output_box, + label); + + box = gtk_frame_new (_("C_hoose a device for sound output:")); + label = gtk_frame_get_label_widget (GTK_FRAME (box)); + _gtk_label_make_bold (GTK_LABEL (label)); + gtk_label_set_use_underline (GTK_LABEL (label), TRUE); + gtk_frame_set_shadow_type (GTK_FRAME (box), GTK_SHADOW_NONE); + gtk_box_pack_start (GTK_BOX (self->priv->output_box), box, TRUE, TRUE, 0); + + alignment = gtk_alignment_new (0, 0, 1, 1); + gtk_container_add (GTK_CONTAINER (box), alignment); + gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 6, 0, 0, 0); + + self->priv->output_treeview = create_ui_device_treeview (self, + G_CALLBACK (on_output_selection_changed)); + gtk_label_set_mnemonic_widget (GTK_LABEL (label), self->priv->output_treeview); + + box = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (box), + GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (box), + GTK_SHADOW_IN); + gtk_container_add (GTK_CONTAINER (box), self->priv->output_treeview); + gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (box), 150); + gtk_container_add (GTK_CONTAINER (alignment), box); + + box = gtk_frame_new (_("Settings for the selected device:")); + label = gtk_frame_get_label_widget (GTK_FRAME (box)); + _gtk_label_make_bold (GTK_LABEL (label)); + gtk_frame_set_shadow_type (GTK_FRAME (box), GTK_SHADOW_NONE); + gtk_box_pack_start (GTK_BOX (self->priv->output_box), box, FALSE, FALSE, 12); + self->priv->output_settings_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_container_add (GTK_CONTAINER (box), self->priv->output_settings_box); + + /* Input page */ + self->priv->input_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); + gtk_container_set_border_width (GTK_CONTAINER (self->priv->input_box), 12); + label = gtk_label_new (_("Input")); + gtk_notebook_append_page (GTK_NOTEBOOK (self->priv->notebook), + self->priv->input_box, + label); + + self->priv->input_bar = create_bar (self, TRUE, TRUE); + gvc_channel_bar_set_name (GVC_CHANNEL_BAR (self->priv->input_bar), + _("_Input volume:")); + gvc_channel_bar_set_low_icon_name (GVC_CHANNEL_BAR (self->priv->input_bar), + "audio-input-microphone-low-symbolic"); + gvc_channel_bar_set_high_icon_name (GVC_CHANNEL_BAR (self->priv->input_bar), + "audio-input-microphone-high-symbolic"); + gtk_widget_set_sensitive (self->priv->input_bar, FALSE); + alignment = gtk_alignment_new (0, 0, 1, 1); + gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 6, 0, 0, 0); + gtk_container_add (GTK_CONTAINER (alignment), self->priv->input_bar); + gtk_box_pack_start (GTK_BOX (self->priv->input_box), + alignment, + FALSE, FALSE, 0); + + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start (GTK_BOX (self->priv->input_box), + box, + FALSE, FALSE, 6); + + sbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start (GTK_BOX (box), + sbox, + FALSE, FALSE, 0); + + label = gtk_label_new (_("Input level:")); + gtk_box_pack_start (GTK_BOX (sbox), + label, + FALSE, FALSE, 0); + if (self->priv->size_group != NULL) + gtk_size_group_add_widget (self->priv->size_group, sbox); + + self->priv->input_level_bar = gvc_level_bar_new (); + gvc_level_bar_set_orientation (GVC_LEVEL_BAR (self->priv->input_level_bar), + GTK_ORIENTATION_HORIZONTAL); + gvc_level_bar_set_scale (GVC_LEVEL_BAR (self->priv->input_level_bar), + GVC_LEVEL_SCALE_LINEAR); + gtk_box_pack_start (GTK_BOX (box), + self->priv->input_level_bar, + TRUE, TRUE, 6); + + ebox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start (GTK_BOX (box), + ebox, + FALSE, FALSE, 0); + if (self->priv->size_group != NULL) + gtk_size_group_add_widget (self->priv->size_group, ebox); + + self->priv->input_settings_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start (GTK_BOX (self->priv->input_box), + self->priv->input_settings_box, + FALSE, FALSE, 0); + + box = gtk_frame_new (_("C_hoose a device for sound input:")); + label = gtk_frame_get_label_widget (GTK_FRAME (box)); + _gtk_label_make_bold (GTK_LABEL (label)); + gtk_label_set_use_underline (GTK_LABEL (label), TRUE); + gtk_frame_set_shadow_type (GTK_FRAME (box), GTK_SHADOW_NONE); + gtk_box_pack_start (GTK_BOX (self->priv->input_box), box, TRUE, TRUE, 0); + + alignment = gtk_alignment_new (0, 0, 1, 1); + gtk_container_add (GTK_CONTAINER (box), alignment); + gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 6, 0, 0, 0); + + self->priv->input_treeview = create_ui_device_treeview (self, + G_CALLBACK (on_input_selection_changed)); + gtk_label_set_mnemonic_widget (GTK_LABEL (label), self->priv->input_treeview); + + box = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (box), + GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (box), + GTK_SHADOW_IN); + gtk_container_add (GTK_CONTAINER (box), self->priv->input_treeview); + gtk_container_add (GTK_CONTAINER (alignment), box); + + /* Effects page */ + self->priv->sound_effects_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); + gtk_container_set_border_width (GTK_CONTAINER (self->priv->sound_effects_box), 12); + label = gtk_label_new (_("Sound Effects")); + gtk_notebook_append_page (GTK_NOTEBOOK (self->priv->notebook), + self->priv->sound_effects_box, + label); + + self->priv->effects_bar = create_bar (self, TRUE, TRUE); + gvc_channel_bar_set_name (GVC_CHANNEL_BAR (self->priv->effects_bar), + _("_Alert volume:")); + gtk_widget_set_sensitive (self->priv->effects_bar, FALSE); + gtk_box_pack_start (GTK_BOX (self->priv->sound_effects_box), + self->priv->effects_bar, FALSE, FALSE, 0); + + self->priv->sound_theme_chooser = gvc_sound_theme_chooser_new (); + gtk_box_pack_start (GTK_BOX (self->priv->sound_effects_box), + self->priv->sound_theme_chooser, + TRUE, TRUE, 6); + + /* Applications */ + self->priv->applications_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); + gtk_container_set_border_width (GTK_CONTAINER (self->priv->applications_box), 12); + label = gtk_label_new (_("Applications")); + gtk_notebook_append_page (GTK_NOTEBOOK (self->priv->notebook), + self->priv->applications_box, + label); + self->priv->no_apps_label = gtk_label_new (_("No application is currently playing or recording audio.")); + gtk_box_pack_start (GTK_BOX (self->priv->applications_box), + self->priv->no_apps_label, + TRUE, TRUE, 0); + + g_signal_connect (self->priv->mixer_control, + "output-added", + G_CALLBACK (on_control_output_added), + self); + g_signal_connect (self->priv->mixer_control, + "output-removed", + G_CALLBACK (on_control_output_removed), + self); + g_signal_connect (self->priv->mixer_control, + "input-added", + G_CALLBACK (on_control_input_added), + self); + g_signal_connect (self->priv->mixer_control, + "input-removed", + G_CALLBACK (on_control_input_removed), + self); + + g_signal_connect (self->priv->mixer_control, + "stream-added", + G_CALLBACK (on_control_stream_added), + self); + g_signal_connect (self->priv->mixer_control, + "stream-removed", + G_CALLBACK (on_control_stream_removed), + self); + + gtk_widget_show_all (main_vbox); + + streams = gvc_mixer_control_get_streams (self->priv->mixer_control); + for (l = streams; l != NULL; l = l->next) { + stream = l->data; + add_stream (self, stream); + } + g_slist_free (streams); + + return object; +} + +static void +gvc_mixer_dialog_dispose (GObject *object) +{ + GvcMixerDialog *dialog = GVC_MIXER_DIALOG (object); + + if (dialog->priv->mixer_control != NULL) { + + g_signal_handlers_disconnect_by_func (dialog->priv->mixer_control, + on_control_output_added, + dialog); + g_signal_handlers_disconnect_by_func (dialog->priv->mixer_control, + on_control_output_removed, + dialog); + g_signal_handlers_disconnect_by_func (dialog->priv->mixer_control, + on_control_input_added, + dialog); + g_signal_handlers_disconnect_by_func (dialog->priv->mixer_control, + on_control_input_removed, + dialog); + g_signal_handlers_disconnect_by_func (dialog->priv->mixer_control, + on_control_active_input_update, + dialog); + g_signal_handlers_disconnect_by_func (dialog->priv->mixer_control, + on_control_active_output_update, + dialog); + g_signal_handlers_disconnect_by_func (dialog->priv->mixer_control, + on_control_stream_added, + dialog); + g_signal_handlers_disconnect_by_func (dialog->priv->mixer_control, + on_control_stream_removed, + dialog); + + g_object_unref (dialog->priv->mixer_control); + dialog->priv->mixer_control = NULL; + } + + if (dialog->priv->bars != NULL) { + g_hash_table_destroy (dialog->priv->bars); + dialog->priv->bars = NULL; + } + + G_OBJECT_CLASS (gvc_mixer_dialog_parent_class)->dispose (object); +} + +static void +gvc_mixer_dialog_class_init (GvcMixerDialogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructor = gvc_mixer_dialog_constructor; + object_class->dispose = gvc_mixer_dialog_dispose; + object_class->finalize = gvc_mixer_dialog_finalize; + object_class->set_property = gvc_mixer_dialog_set_property; + object_class->get_property = gvc_mixer_dialog_get_property; + + g_object_class_install_property (object_class, + PROP_MIXER_CONTROL, + g_param_spec_object ("mixer-control", + "mixer control", + "mixer control", + GVC_TYPE_MIXER_CONTROL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + + g_type_class_add_private (klass, sizeof (GvcMixerDialogPrivate)); +} + + +static void +gvc_mixer_dialog_init (GvcMixerDialog *dialog) +{ + dialog->priv = GVC_MIXER_DIALOG_GET_PRIVATE (dialog); + dialog->priv->bars = g_hash_table_new (NULL, NULL); + dialog->priv->size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); +} + +static void +gvc_mixer_dialog_finalize (GObject *object) +{ + GvcMixerDialog *mixer_dialog; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_DIALOG (object)); + + mixer_dialog = GVC_MIXER_DIALOG (object); + + g_return_if_fail (mixer_dialog->priv != NULL); + G_OBJECT_CLASS (gvc_mixer_dialog_parent_class)->finalize (object); +} + +GvcMixerDialog * +gvc_mixer_dialog_new (GvcMixerControl *control) +{ + GObject *dialog; + dialog = g_object_new (GVC_TYPE_MIXER_DIALOG, + "mixer-control", control, + NULL); + return GVC_MIXER_DIALOG (dialog); +} + +enum { + PAGE_OUTPUT, + PAGE_INPUT, + PAGE_EVENTS, + PAGE_APPLICATIONS +}; + +gboolean +gvc_mixer_dialog_set_page (GvcMixerDialog *self, + const char *page) +{ + guint num; + + g_return_val_if_fail (self != NULL, FALSE); + + num = PAGE_OUTPUT; + + if (g_str_equal (page, "effects")) + num = PAGE_EVENTS; + else if (g_str_equal (page, "input")) + num = PAGE_INPUT; + else if (g_str_equal (page, "output")) + num = PAGE_OUTPUT; + else if (g_str_equal (page, "applications")) + num = PAGE_APPLICATIONS; + + gtk_notebook_set_current_page (GTK_NOTEBOOK (self->priv->notebook), num); + + return TRUE; +} diff --git a/gvc-mixer-dialog.h b/gvc-mixer-dialog.h new file mode 100644 index 0000000..e95a7c7 --- /dev/null +++ b/gvc-mixer-dialog.h @@ -0,0 +1,56 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * 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. + * + */ + +#ifndef __GVC_MIXER_DIALOG_H +#define __GVC_MIXER_DIALOG_H + +#include <glib-object.h> +#include "gvc-mixer-control.h" + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_DIALOG (gvc_mixer_dialog_get_type ()) +#define GVC_MIXER_DIALOG(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_DIALOG, GvcMixerDialog)) +#define GVC_MIXER_DIALOG_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_DIALOG, GvcMixerDialogClass)) +#define GVC_IS_MIXER_DIALOG(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_DIALOG)) +#define GVC_IS_MIXER_DIALOG_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_DIALOG)) +#define GVC_MIXER_DIALOG_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_DIALOG, GvcMixerDialogClass)) + +typedef struct GvcMixerDialogPrivate GvcMixerDialogPrivate; + +typedef struct +{ + GtkVBox parent; + GvcMixerDialogPrivate *priv; +} GvcMixerDialog; + +typedef struct +{ + GtkVBoxClass parent_class; +} GvcMixerDialogClass; + +GType gvc_mixer_dialog_get_type (void); + +GvcMixerDialog * gvc_mixer_dialog_new (GvcMixerControl *control); +gboolean gvc_mixer_dialog_set_page (GvcMixerDialog *dialog, const gchar* page); + +G_END_DECLS + +#endif /* __GVC_MIXER_DIALOG_H */ diff --git a/gvc-mixer-event-role.c b/gvc-mixer-event-role.c new file mode 100644 index 0000000..3646df2 --- /dev/null +++ b/gvc-mixer-event-role.c @@ -0,0 +1,235 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * 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/ext-stream-restore.h> + +#include "gvc-mixer-event-role.h" +#include "gvc-mixer-stream-private.h" +#include "gvc-channel-map-private.h" + +#define GVC_MIXER_EVENT_ROLE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GVC_TYPE_MIXER_EVENT_ROLE, GvcMixerEventRolePrivate)) + +struct GvcMixerEventRolePrivate +{ + char *device; +}; + +enum +{ + PROP_0, + PROP_DEVICE +}; + +static void gvc_mixer_event_role_class_init (GvcMixerEventRoleClass *klass); +static void gvc_mixer_event_role_init (GvcMixerEventRole *mixer_event_role); +static void gvc_mixer_event_role_finalize (GObject *object); + +G_DEFINE_TYPE (GvcMixerEventRole, gvc_mixer_event_role, GVC_TYPE_MIXER_STREAM) + +static gboolean +update_settings (GvcMixerEventRole *role, + gboolean is_muted, + gpointer *op) +{ + pa_operation *o; + const GvcChannelMap *map; + pa_context *context; + pa_ext_stream_restore_info info; + + map = gvc_mixer_stream_get_channel_map (GVC_MIXER_STREAM(role)); + + info.volume = *gvc_channel_map_get_cvolume(map); + info.name = "sink-input-by-media-role:event"; + info.channel_map = *gvc_channel_map_get_pa_channel_map(map); + info.device = role->priv->device; + info.mute = is_muted; + + context = gvc_mixer_stream_get_pa_context (GVC_MIXER_STREAM (role)); + + o = pa_ext_stream_restore_write (context, + PA_UPDATE_REPLACE, + &info, + 1, + TRUE, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_ext_stream_restore_write() failed"); + return FALSE; + } + + if (op != NULL) + *op = o; + + return TRUE; +} + +static gboolean +gvc_mixer_event_role_push_volume (GvcMixerStream *stream, gpointer *op) +{ + return update_settings (GVC_MIXER_EVENT_ROLE (stream), + gvc_mixer_stream_get_is_muted (stream), op); +} + +static gboolean +gvc_mixer_event_role_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + /* Apply change straight away so that we don't get a race with + * gvc_mixer_event_role_push_volume(). + * See https://bugs.freedesktop.org/show_bug.cgi?id=51413 */ + gvc_mixer_stream_set_is_muted (stream, is_muted); + return update_settings (GVC_MIXER_EVENT_ROLE (stream), + is_muted, NULL); +} + +static gboolean +gvc_mixer_event_role_set_device (GvcMixerEventRole *role, + const char *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_EVENT_ROLE (role), FALSE); + + g_free (role->priv->device); + role->priv->device = g_strdup (device); + g_object_notify (G_OBJECT (role), "device"); + + return TRUE; +} + +static void +gvc_mixer_event_role_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GvcMixerEventRole *self = GVC_MIXER_EVENT_ROLE (object); + + switch (prop_id) { + case PROP_DEVICE: + gvc_mixer_event_role_set_device (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gvc_mixer_event_role_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GvcMixerEventRole *self = GVC_MIXER_EVENT_ROLE (object); + + switch (prop_id) { + case PROP_DEVICE: + g_value_set_string (value, self->priv->device); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gvc_mixer_event_role_class_init (GvcMixerEventRoleClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass); + + object_class->finalize = gvc_mixer_event_role_finalize; + object_class->set_property = gvc_mixer_event_role_set_property; + object_class->get_property = gvc_mixer_event_role_get_property; + + stream_class->push_volume = gvc_mixer_event_role_push_volume; + stream_class->change_is_muted = gvc_mixer_event_role_change_is_muted; + + g_object_class_install_property (object_class, + PROP_DEVICE, + g_param_spec_string ("device", + "Device", + "Device", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + + g_type_class_add_private (klass, sizeof (GvcMixerEventRolePrivate)); +} + +static void +gvc_mixer_event_role_init (GvcMixerEventRole *event_role) +{ + event_role->priv = GVC_MIXER_EVENT_ROLE_GET_PRIVATE (event_role); + +} + +static void +gvc_mixer_event_role_finalize (GObject *object) +{ + GvcMixerEventRole *mixer_event_role; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_EVENT_ROLE (object)); + + mixer_event_role = GVC_MIXER_EVENT_ROLE (object); + + g_return_if_fail (mixer_event_role->priv != NULL); + + g_free (mixer_event_role->priv->device); + + G_OBJECT_CLASS (gvc_mixer_event_role_parent_class)->finalize (object); +} + +/** + * gvc_mixer_event_role_new: (skip) + * + * @context: + * @index: + * @channel_map: + * + * Returns: + */ +GvcMixerStream * +gvc_mixer_event_role_new (pa_context *context, + const char *device, + GvcChannelMap *channel_map) +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_EVENT_ROLE, + "pa-context", context, + "index", 0, + "device", device, + "channel-map", channel_map, + NULL); + + return GVC_MIXER_STREAM (object); +} diff --git a/gvc-mixer-event-role.h b/gvc-mixer-event-role.h new file mode 100644 index 0000000..ab4c509 --- /dev/null +++ b/gvc-mixer-event-role.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * 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. + * + */ + +#ifndef __GVC_MIXER_EVENT_ROLE_H +#define __GVC_MIXER_EVENT_ROLE_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_EVENT_ROLE (gvc_mixer_event_role_get_type ()) +#define GVC_MIXER_EVENT_ROLE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_EVENT_ROLE, GvcMixerEventRole)) +#define GVC_MIXER_EVENT_ROLE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_EVENT_ROLE, GvcMixerEventRoleClass)) +#define GVC_IS_MIXER_EVENT_ROLE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_EVENT_ROLE)) +#define GVC_IS_MIXER_EVENT_ROLE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_EVENT_ROLE)) +#define GVC_MIXER_EVENT_ROLE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_EVENT_ROLE, GvcMixerEventRoleClass)) + +typedef struct GvcMixerEventRolePrivate GvcMixerEventRolePrivate; + +typedef struct +{ + GvcMixerStream parent; + GvcMixerEventRolePrivate *priv; +} GvcMixerEventRole; + +typedef struct +{ + GvcMixerStreamClass parent_class; +} GvcMixerEventRoleClass; + +GType gvc_mixer_event_role_get_type (void); + +GvcMixerStream * gvc_mixer_event_role_new (pa_context *context, + const char *device, + GvcChannelMap *channel_map); + +G_END_DECLS + +#endif /* __GVC_MIXER_EVENT_ROLE_H */ diff --git a/gvc-mixer-sink-input.c b/gvc-mixer-sink-input.c new file mode 100644 index 0000000..03ba3b2 --- /dev/null +++ b/gvc-mixer-sink-input.c @@ -0,0 +1,166 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * 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 "gvc-mixer-sink-input.h" +#include "gvc-mixer-stream-private.h" +#include "gvc-channel-map-private.h" + +#define GVC_MIXER_SINK_INPUT_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GVC_TYPE_MIXER_SINK_INPUT, GvcMixerSinkInputPrivate)) + +struct GvcMixerSinkInputPrivate +{ + gpointer dummy; +}; + +static void gvc_mixer_sink_input_class_init (GvcMixerSinkInputClass *klass); +static void gvc_mixer_sink_input_init (GvcMixerSinkInput *mixer_sink_input); +static void gvc_mixer_sink_input_finalize (GObject *object); + +G_DEFINE_TYPE (GvcMixerSinkInput, gvc_mixer_sink_input, GVC_TYPE_MIXER_STREAM) + +static gboolean +gvc_mixer_sink_input_push_volume (GvcMixerStream *stream, gpointer *op) +{ + pa_operation *o; + guint index; + const GvcChannelMap *map; + pa_context *context; + const pa_cvolume *cv; + + index = gvc_mixer_stream_get_index (stream); + + map = gvc_mixer_stream_get_channel_map (stream); + + cv = gvc_channel_map_get_cvolume(map); + + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_sink_input_volume (context, + index, + cv, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_sink_input_volume() failed"); + return FALSE; + } + + *op = o; + + return TRUE; +} + +static gboolean +gvc_mixer_sink_input_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + pa_operation *o; + guint index; + pa_context *context; + + index = gvc_mixer_stream_get_index (stream); + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_sink_input_mute (context, + index, + is_muted, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_sink_input_mute_by_index() failed"); + return FALSE; + } + + pa_operation_unref(o); + + return TRUE; +} + +static void +gvc_mixer_sink_input_class_init (GvcMixerSinkInputClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass); + + object_class->finalize = gvc_mixer_sink_input_finalize; + + stream_class->push_volume = gvc_mixer_sink_input_push_volume; + stream_class->change_is_muted = gvc_mixer_sink_input_change_is_muted; + + g_type_class_add_private (klass, sizeof (GvcMixerSinkInputPrivate)); +} + +static void +gvc_mixer_sink_input_init (GvcMixerSinkInput *sink_input) +{ + sink_input->priv = GVC_MIXER_SINK_INPUT_GET_PRIVATE (sink_input); +} + +static void +gvc_mixer_sink_input_finalize (GObject *object) +{ + GvcMixerSinkInput *mixer_sink_input; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_SINK_INPUT (object)); + + mixer_sink_input = GVC_MIXER_SINK_INPUT (object); + + g_return_if_fail (mixer_sink_input->priv != NULL); + G_OBJECT_CLASS (gvc_mixer_sink_input_parent_class)->finalize (object); +} + +/** + * gvc_mixer_sink_input_new: (skip) + * + * @context: + * @index: + * @channel_map: + * + * Returns: + */ +GvcMixerStream * +gvc_mixer_sink_input_new (pa_context *context, + guint index, + GvcChannelMap *channel_map) +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_SINK_INPUT, + "pa-context", context, + "index", index, + "channel-map", channel_map, + NULL); + + return GVC_MIXER_STREAM (object); +} diff --git a/gvc-mixer-sink-input.h b/gvc-mixer-sink-input.h new file mode 100644 index 0000000..8a4b714 --- /dev/null +++ b/gvc-mixer-sink-input.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * 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. + * + */ + +#ifndef __GVC_MIXER_SINK_INPUT_H +#define __GVC_MIXER_SINK_INPUT_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_SINK_INPUT (gvc_mixer_sink_input_get_type ()) +#define GVC_MIXER_SINK_INPUT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SINK_INPUT, GvcMixerSinkInput)) +#define GVC_MIXER_SINK_INPUT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SINK_INPUT, GvcMixerSinkInputClass)) +#define GVC_IS_MIXER_SINK_INPUT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SINK_INPUT)) +#define GVC_IS_MIXER_SINK_INPUT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SINK_INPUT)) +#define GVC_MIXER_SINK_INPUT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SINK_INPUT, GvcMixerSinkInputClass)) + +typedef struct GvcMixerSinkInputPrivate GvcMixerSinkInputPrivate; + +typedef struct +{ + GvcMixerStream parent; + GvcMixerSinkInputPrivate *priv; +} GvcMixerSinkInput; + +typedef struct +{ + GvcMixerStreamClass parent_class; +} GvcMixerSinkInputClass; + +GType gvc_mixer_sink_input_get_type (void); + +GvcMixerStream * gvc_mixer_sink_input_new (pa_context *context, + guint index, + GvcChannelMap *map); + +G_END_DECLS + +#endif /* __GVC_MIXER_SINK_INPUT_H */ diff --git a/gvc-mixer-sink.c b/gvc-mixer-sink.c new file mode 100644 index 0000000..78c3c68 --- /dev/null +++ b/gvc-mixer-sink.c @@ -0,0 +1,196 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * 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 "gvc-mixer-sink.h" +#include "gvc-mixer-stream-private.h" +#include "gvc-channel-map-private.h" + +#define GVC_MIXER_SINK_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GVC_TYPE_MIXER_SINK, GvcMixerSinkPrivate)) + +struct GvcMixerSinkPrivate +{ + gpointer dummy; +}; + +static void gvc_mixer_sink_class_init (GvcMixerSinkClass *klass); +static void gvc_mixer_sink_init (GvcMixerSink *mixer_sink); +static void gvc_mixer_sink_finalize (GObject *object); + +G_DEFINE_TYPE (GvcMixerSink, gvc_mixer_sink, GVC_TYPE_MIXER_STREAM) + +static gboolean +gvc_mixer_sink_push_volume (GvcMixerStream *stream, gpointer *op) +{ + pa_operation *o; + guint index; + const GvcChannelMap *map; + pa_context *context; + const pa_cvolume *cv; + + index = gvc_mixer_stream_get_index (stream); + + map = gvc_mixer_stream_get_channel_map (stream); + + /* set the volume */ + cv = gvc_channel_map_get_cvolume(map); + + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_sink_volume_by_index (context, + index, + cv, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_sink_volume_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + *op = o; + + return TRUE; +} + +static gboolean +gvc_mixer_sink_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + pa_operation *o; + guint index; + pa_context *context; + + index = gvc_mixer_stream_get_index (stream); + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_sink_mute_by_index (context, + index, + is_muted, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_sink_mute_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + pa_operation_unref(o); + + return TRUE; +} + +static gboolean +gvc_mixer_sink_change_port (GvcMixerStream *stream, + const char *port) +{ + pa_operation *o; + guint index; + pa_context *context; + + index = gvc_mixer_stream_get_index (stream); + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_sink_port_by_index (context, + index, + port, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_sink_port_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + pa_operation_unref(o); + + return TRUE; +} + +static void +gvc_mixer_sink_class_init (GvcMixerSinkClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass); + + object_class->finalize = gvc_mixer_sink_finalize; + + stream_class->push_volume = gvc_mixer_sink_push_volume; + stream_class->change_port = gvc_mixer_sink_change_port; + stream_class->change_is_muted = gvc_mixer_sink_change_is_muted; + + g_type_class_add_private (klass, sizeof (GvcMixerSinkPrivate)); +} + +static void +gvc_mixer_sink_init (GvcMixerSink *sink) +{ + sink->priv = GVC_MIXER_SINK_GET_PRIVATE (sink); +} + +static void +gvc_mixer_sink_finalize (GObject *object) +{ + GvcMixerSink *mixer_sink; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_SINK (object)); + + mixer_sink = GVC_MIXER_SINK (object); + + g_return_if_fail (mixer_sink->priv != NULL); + G_OBJECT_CLASS (gvc_mixer_sink_parent_class)->finalize (object); +} + +/** + * gvc_mixer_sink_new: (skip) + * + * @context: + * @index: + * @channel_map: + * + * Returns: + */ +GvcMixerStream * +gvc_mixer_sink_new (pa_context *context, + guint index, + GvcChannelMap *channel_map) + +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_SINK, + "pa-context", context, + "index", index, + "channel-map", channel_map, + NULL); + + return GVC_MIXER_STREAM (object); +} diff --git a/gvc-mixer-sink.h b/gvc-mixer-sink.h new file mode 100644 index 0000000..2a4a4ba --- /dev/null +++ b/gvc-mixer-sink.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * 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. + * + */ + +#ifndef __GVC_MIXER_SINK_H +#define __GVC_MIXER_SINK_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_SINK (gvc_mixer_sink_get_type ()) +#define GVC_MIXER_SINK(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SINK, GvcMixerSink)) +#define GVC_MIXER_SINK_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SINK, GvcMixerSinkClass)) +#define GVC_IS_MIXER_SINK(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SINK)) +#define GVC_IS_MIXER_SINK_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SINK)) +#define GVC_MIXER_SINK_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SINK, GvcMixerSinkClass)) + +typedef struct GvcMixerSinkPrivate GvcMixerSinkPrivate; + +typedef struct +{ + GvcMixerStream parent; + GvcMixerSinkPrivate *priv; +} GvcMixerSink; + +typedef struct +{ + GvcMixerStreamClass parent_class; +} GvcMixerSinkClass; + +GType gvc_mixer_sink_get_type (void); + +GvcMixerStream * gvc_mixer_sink_new (pa_context *context, + guint index, + GvcChannelMap *map); + +G_END_DECLS + +#endif /* __GVC_MIXER_SINK_H */ diff --git a/gvc-mixer-source-output.c b/gvc-mixer-source-output.c new file mode 100644 index 0000000..536487b --- /dev/null +++ b/gvc-mixer-source-output.c @@ -0,0 +1,121 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * 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 "gvc-mixer-source-output.h" + +#define GVC_MIXER_SOURCE_OUTPUT_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GVC_TYPE_MIXER_SOURCE_OUTPUT, GvcMixerSourceOutputPrivate)) + +struct GvcMixerSourceOutputPrivate +{ + gpointer dummy; +}; + +static void gvc_mixer_source_output_class_init (GvcMixerSourceOutputClass *klass); +static void gvc_mixer_source_output_init (GvcMixerSourceOutput *mixer_source_output); +static void gvc_mixer_source_output_finalize (GObject *object); + +G_DEFINE_TYPE (GvcMixerSourceOutput, gvc_mixer_source_output, GVC_TYPE_MIXER_STREAM) + +static gboolean +gvc_mixer_source_output_push_volume (GvcMixerStream *stream, gpointer *op) +{ + /* FIXME: */ + *op = NULL; + return TRUE; +} + +static gboolean +gvc_mixer_source_output_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + /* FIXME: */ + return TRUE; +} + +static void +gvc_mixer_source_output_class_init (GvcMixerSourceOutputClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass); + + object_class->finalize = gvc_mixer_source_output_finalize; + + stream_class->push_volume = gvc_mixer_source_output_push_volume; + stream_class->change_is_muted = gvc_mixer_source_output_change_is_muted; + + g_type_class_add_private (klass, sizeof (GvcMixerSourceOutputPrivate)); +} + +static void +gvc_mixer_source_output_init (GvcMixerSourceOutput *source_output) +{ + source_output->priv = GVC_MIXER_SOURCE_OUTPUT_GET_PRIVATE (source_output); + +} + +static void +gvc_mixer_source_output_finalize (GObject *object) +{ + GvcMixerSourceOutput *mixer_source_output; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_SOURCE_OUTPUT (object)); + + mixer_source_output = GVC_MIXER_SOURCE_OUTPUT (object); + + g_return_if_fail (mixer_source_output->priv != NULL); + G_OBJECT_CLASS (gvc_mixer_source_output_parent_class)->finalize (object); +} + +/** + * gvc_mixer_source_output_new: (skip) + * + * @context: + * @index: + * @channel_map: + * + * Returns: + */ +GvcMixerStream * +gvc_mixer_source_output_new (pa_context *context, + guint index, + GvcChannelMap *channel_map) +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_SOURCE_OUTPUT, + "pa-context", context, + "index", index, + "channel-map", channel_map, + NULL); + + return GVC_MIXER_STREAM (object); +} diff --git a/gvc-mixer-source-output.h b/gvc-mixer-source-output.h new file mode 100644 index 0000000..2283e3b --- /dev/null +++ b/gvc-mixer-source-output.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * 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. + * + */ + +#ifndef __GVC_MIXER_SOURCE_OUTPUT_H +#define __GVC_MIXER_SOURCE_OUTPUT_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_SOURCE_OUTPUT (gvc_mixer_source_output_get_type ()) +#define GVC_MIXER_SOURCE_OUTPUT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SOURCE_OUTPUT, GvcMixerSourceOutput)) +#define GVC_MIXER_SOURCE_OUTPUT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SOURCE_OUTPUT, GvcMixerSourceOutputClass)) +#define GVC_IS_MIXER_SOURCE_OUTPUT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SOURCE_OUTPUT)) +#define GVC_IS_MIXER_SOURCE_OUTPUT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SOURCE_OUTPUT)) +#define GVC_MIXER_SOURCE_OUTPUT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SOURCE_OUTPUT, GvcMixerSourceOutputClass)) + +typedef struct GvcMixerSourceOutputPrivate GvcMixerSourceOutputPrivate; + +typedef struct +{ + GvcMixerStream parent; + GvcMixerSourceOutputPrivate *priv; +} GvcMixerSourceOutput; + +typedef struct +{ + GvcMixerStreamClass parent_class; +} GvcMixerSourceOutputClass; + +GType gvc_mixer_source_output_get_type (void); + +GvcMixerStream * gvc_mixer_source_output_new (pa_context *context, + guint index, + GvcChannelMap *map); + +G_END_DECLS + +#endif /* __GVC_MIXER_SOURCE_OUTPUT_H */ diff --git a/gvc-mixer-source.c b/gvc-mixer-source.c new file mode 100644 index 0000000..dd2d0b4 --- /dev/null +++ b/gvc-mixer-source.c @@ -0,0 +1,196 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * 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 "gvc-mixer-source.h" +#include "gvc-mixer-stream-private.h" +#include "gvc-channel-map-private.h" + +#define GVC_MIXER_SOURCE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GVC_TYPE_MIXER_SOURCE, GvcMixerSourcePrivate)) + +struct GvcMixerSourcePrivate +{ + gpointer dummy; +}; + +static void gvc_mixer_source_class_init (GvcMixerSourceClass *klass); +static void gvc_mixer_source_init (GvcMixerSource *mixer_source); +static void gvc_mixer_source_finalize (GObject *object); + +G_DEFINE_TYPE (GvcMixerSource, gvc_mixer_source, GVC_TYPE_MIXER_STREAM) + +static gboolean +gvc_mixer_source_push_volume (GvcMixerStream *stream, gpointer *op) +{ + pa_operation *o; + guint index; + const GvcChannelMap *map; + pa_context *context; + const pa_cvolume *cv; + + index = gvc_mixer_stream_get_index (stream); + + map = gvc_mixer_stream_get_channel_map (stream); + + /* set the volume */ + cv = gvc_channel_map_get_cvolume (map); + + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_source_volume_by_index (context, + index, + cv, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_source_volume_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + *op = o; + + return TRUE; +} + +static gboolean +gvc_mixer_source_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + pa_operation *o; + guint index; + pa_context *context; + + index = gvc_mixer_stream_get_index (stream); + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_source_mute_by_index (context, + index, + is_muted, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_source_mute_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + pa_operation_unref(o); + + return TRUE; +} + +static gboolean +gvc_mixer_source_change_port (GvcMixerStream *stream, + const char *port) +{ + pa_operation *o; + guint index; + pa_context *context; + + index = gvc_mixer_stream_get_index (stream); + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_source_port_by_index (context, + index, + port, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_source_port_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + pa_operation_unref(o); + + return TRUE; +} + +static void +gvc_mixer_source_class_init (GvcMixerSourceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass); + + object_class->finalize = gvc_mixer_source_finalize; + + stream_class->push_volume = gvc_mixer_source_push_volume; + stream_class->change_is_muted = gvc_mixer_source_change_is_muted; + stream_class->change_port = gvc_mixer_source_change_port; + + g_type_class_add_private (klass, sizeof (GvcMixerSourcePrivate)); +} + +static void +gvc_mixer_source_init (GvcMixerSource *source) +{ + source->priv = GVC_MIXER_SOURCE_GET_PRIVATE (source); +} + +static void +gvc_mixer_source_finalize (GObject *object) +{ + GvcMixerSource *mixer_source; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_SOURCE (object)); + + mixer_source = GVC_MIXER_SOURCE (object); + + g_return_if_fail (mixer_source->priv != NULL); + G_OBJECT_CLASS (gvc_mixer_source_parent_class)->finalize (object); +} + +/** + * gvc_mixer_source_new: (skip) + * + * @context: + * @index: + * @channel_map: + * + * Returns: + */ +GvcMixerStream * +gvc_mixer_source_new (pa_context *context, + guint index, + GvcChannelMap *channel_map) + +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_SOURCE, + "pa-context", context, + "index", index, + "channel-map", channel_map, + NULL); + + return GVC_MIXER_STREAM (object); +} diff --git a/gvc-mixer-source.h b/gvc-mixer-source.h new file mode 100644 index 0000000..503f1b5 --- /dev/null +++ b/gvc-mixer-source.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * 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. + * + */ + +#ifndef __GVC_MIXER_SOURCE_H +#define __GVC_MIXER_SOURCE_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_SOURCE (gvc_mixer_source_get_type ()) +#define GVC_MIXER_SOURCE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SOURCE, GvcMixerSource)) +#define GVC_MIXER_SOURCE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SOURCE, GvcMixerSourceClass)) +#define GVC_IS_MIXER_SOURCE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SOURCE)) +#define GVC_IS_MIXER_SOURCE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SOURCE)) +#define GVC_MIXER_SOURCE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SOURCE, GvcMixerSourceClass)) + +typedef struct GvcMixerSourcePrivate GvcMixerSourcePrivate; + +typedef struct +{ + GvcMixerStream parent; + GvcMixerSourcePrivate *priv; +} GvcMixerSource; + +typedef struct +{ + GvcMixerStreamClass parent_class; +} GvcMixerSourceClass; + +GType gvc_mixer_source_get_type (void); + +GvcMixerStream * gvc_mixer_source_new (pa_context *context, + guint index, + GvcChannelMap *map); + +G_END_DECLS + +#endif /* __GVC_MIXER_SOURCE_H */ diff --git a/gvc-mixer-stream-private.h b/gvc-mixer-stream-private.h new file mode 100644 index 0000000..b97ecf5 --- /dev/null +++ b/gvc-mixer-stream-private.h @@ -0,0 +1,34 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * 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. + * + */ + +#ifndef __GVC_MIXER_STREAM_PRIVATE_H +#define __GVC_MIXER_STREAM_PRIVATE_H + +#include <glib-object.h> + +#include "gvc-channel-map.h" + +G_BEGIN_DECLS + +pa_context * gvc_mixer_stream_get_pa_context (GvcMixerStream *stream); + +G_END_DECLS + +#endif /* __GVC_MIXER_STREAM_PRIVATE_H */ diff --git a/gvc-mixer-stream.c b/gvc-mixer-stream.c new file mode 100644 index 0000000..0a5663e --- /dev/null +++ b/gvc-mixer-stream.c @@ -0,0 +1,1006 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * 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 "gvc-mixer-stream.h" +#include "gvc-mixer-stream-private.h" +#include "gvc-channel-map-private.h" + +#define GVC_MIXER_STREAM_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GVC_TYPE_MIXER_STREAM, GvcMixerStreamPrivate)) + +static guint32 stream_serial = 1; + +struct GvcMixerStreamPrivate +{ + pa_context *pa_context; + guint id; + guint index; + gint card_index; + GvcChannelMap *channel_map; + char *name; + char *description; + char *application_id; + char *icon_name; + char *sysfs_path; + gboolean is_muted; + gboolean can_decibel; + gboolean is_event_stream; + gboolean is_virtual; + pa_volume_t base_volume; + pa_operation *change_volume_op; + char *port; + char *human_port; + GList *ports; +}; + +enum +{ + PROP_0, + PROP_ID, + PROP_PA_CONTEXT, + PROP_CHANNEL_MAP, + PROP_INDEX, + PROP_NAME, + PROP_DESCRIPTION, + PROP_APPLICATION_ID, + PROP_ICON_NAME, + PROP_SYSFS_PATH, + PROP_VOLUME, + PROP_DECIBEL, + PROP_IS_MUTED, + PROP_CAN_DECIBEL, + PROP_IS_EVENT_STREAM, + PROP_IS_VIRTUAL, + PROP_CARD_INDEX, + PROP_PORT, +}; + +static void gvc_mixer_stream_class_init (GvcMixerStreamClass *klass); +static void gvc_mixer_stream_init (GvcMixerStream *mixer_stream); +static void gvc_mixer_stream_finalize (GObject *object); + +G_DEFINE_ABSTRACT_TYPE (GvcMixerStream, gvc_mixer_stream, G_TYPE_OBJECT) + +static guint32 +get_next_stream_serial (void) +{ + guint32 serial; + + serial = stream_serial++; + + if ((gint32)stream_serial < 0) { + stream_serial = 1; + } + + return serial; +} + +pa_context * +gvc_mixer_stream_get_pa_context (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + return stream->priv->pa_context; +} + +guint +gvc_mixer_stream_get_index (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + return stream->priv->index; +} + +guint +gvc_mixer_stream_get_id (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + return stream->priv->id; +} + +const GvcChannelMap * +gvc_mixer_stream_get_channel_map (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->channel_map; +} + +/** + * gvc_mixer_stream_get_volume: + * + * @stream: + * + * Returns: (type guint32) (transfer none): + */ +pa_volume_t +gvc_mixer_stream_get_volume (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + + return (pa_volume_t) gvc_channel_map_get_volume(stream->priv->channel_map)[VOLUME]; +} + +gdouble +gvc_mixer_stream_get_decibel (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + + return pa_sw_volume_to_dB( + (pa_volume_t) gvc_channel_map_get_volume(stream->priv->channel_map)[VOLUME]); +} + +/** + * gvc_mixer_stream_set_volume: + * + * @stream: + * @volume: (type guint32): + * + * Returns: + */ +gboolean +gvc_mixer_stream_set_volume (GvcMixerStream *stream, + pa_volume_t volume) +{ + pa_cvolume cv; + + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + cv = *gvc_channel_map_get_cvolume(stream->priv->channel_map); + pa_cvolume_scale(&cv, volume); + + if (!pa_cvolume_equal(gvc_channel_map_get_cvolume(stream->priv->channel_map), &cv)) { + gvc_channel_map_volume_changed(stream->priv->channel_map, &cv, FALSE); + g_object_notify (G_OBJECT (stream), "volume"); + return TRUE; + } + + return FALSE; +} + +gboolean +gvc_mixer_stream_set_decibel (GvcMixerStream *stream, + gdouble db) +{ + pa_cvolume cv; + + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + cv = *gvc_channel_map_get_cvolume(stream->priv->channel_map); + pa_cvolume_scale(&cv, pa_sw_volume_from_dB(db)); + + if (!pa_cvolume_equal(gvc_channel_map_get_cvolume(stream->priv->channel_map), &cv)) { + gvc_channel_map_volume_changed(stream->priv->channel_map, &cv, FALSE); + g_object_notify (G_OBJECT (stream), "volume"); + } + + return TRUE; +} + +gboolean +gvc_mixer_stream_get_is_muted (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + return stream->priv->is_muted; +} + +gboolean +gvc_mixer_stream_get_can_decibel (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + return stream->priv->can_decibel; +} + +gboolean +gvc_mixer_stream_set_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + if (is_muted != stream->priv->is_muted) { + stream->priv->is_muted = is_muted; + g_object_notify (G_OBJECT (stream), "is-muted"); + } + + return TRUE; +} + +gboolean +gvc_mixer_stream_set_can_decibel (GvcMixerStream *stream, + gboolean can_decibel) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + if (can_decibel != stream->priv->can_decibel) { + stream->priv->can_decibel = can_decibel; + g_object_notify (G_OBJECT (stream), "can-decibel"); + } + + return TRUE; +} + +const char * +gvc_mixer_stream_get_name (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->name; +} + +const char * +gvc_mixer_stream_get_description (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->description; +} + +gboolean +gvc_mixer_stream_set_name (GvcMixerStream *stream, + const char *name) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_free (stream->priv->name); + stream->priv->name = g_strdup (name); + g_object_notify (G_OBJECT (stream), "name"); + + return TRUE; +} + +gboolean +gvc_mixer_stream_set_description (GvcMixerStream *stream, + const char *description) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_free (stream->priv->description); + stream->priv->description = g_strdup (description); + g_object_notify (G_OBJECT (stream), "description"); + + return TRUE; +} + +gboolean +gvc_mixer_stream_is_event_stream (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + return stream->priv->is_event_stream; +} + +gboolean +gvc_mixer_stream_set_is_event_stream (GvcMixerStream *stream, + gboolean is_event_stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + stream->priv->is_event_stream = is_event_stream; + g_object_notify (G_OBJECT (stream), "is-event-stream"); + + return TRUE; +} + +gboolean +gvc_mixer_stream_is_virtual (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + return stream->priv->is_virtual; +} + +gboolean +gvc_mixer_stream_set_is_virtual (GvcMixerStream *stream, + gboolean is_virtual) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + stream->priv->is_virtual = is_virtual; + g_object_notify (G_OBJECT (stream), "is-virtual"); + + return TRUE; +} + +const char * +gvc_mixer_stream_get_application_id (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->application_id; +} + +gboolean +gvc_mixer_stream_set_application_id (GvcMixerStream *stream, + const char *application_id) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_free (stream->priv->application_id); + stream->priv->application_id = g_strdup (application_id); + g_object_notify (G_OBJECT (stream), "application-id"); + + return TRUE; +} + +static void +on_channel_map_volume_changed (GvcChannelMap *channel_map, + gboolean set, + GvcMixerStream *stream) +{ + if (set == TRUE) + gvc_mixer_stream_push_volume (stream); + + g_object_notify (G_OBJECT (stream), "volume"); +} + +static gboolean +gvc_mixer_stream_set_channel_map (GvcMixerStream *stream, + GvcChannelMap *channel_map) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + if (channel_map != NULL) { + g_object_ref (channel_map); + } + + if (stream->priv->channel_map != NULL) { + g_signal_handlers_disconnect_by_func (stream->priv->channel_map, + on_channel_map_volume_changed, + stream); + g_object_unref (stream->priv->channel_map); + } + + stream->priv->channel_map = channel_map; + + if (stream->priv->channel_map != NULL) { + g_signal_connect (stream->priv->channel_map, + "volume-changed", + G_CALLBACK (on_channel_map_volume_changed), + stream); + + g_object_notify (G_OBJECT (stream), "channel-map"); + } + + return TRUE; +} + +const char * +gvc_mixer_stream_get_icon_name (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->icon_name; +} + +const char * +gvc_mixer_stream_get_sysfs_path (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->sysfs_path; +} + +/** + * gvc_mixer_stream_get_gicon: + * @stream: a #GvcMixerStream + * + * Returns: (transfer full): a new #GIcon + */ +GIcon * +gvc_mixer_stream_get_gicon (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + if (stream->priv->icon_name == NULL) + return NULL; + return g_themed_icon_new_with_default_fallbacks (stream->priv->icon_name); +} + +gboolean +gvc_mixer_stream_set_icon_name (GvcMixerStream *stream, + const char *icon_name) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_free (stream->priv->icon_name); + stream->priv->icon_name = g_strdup (icon_name); + g_object_notify (G_OBJECT (stream), "icon-name"); + + return TRUE; +} + +gboolean +gvc_mixer_stream_set_sysfs_path (GvcMixerStream *stream, + const char *sysfs_path) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_free (stream->priv->sysfs_path); + stream->priv->sysfs_path = g_strdup (sysfs_path); + g_object_notify (G_OBJECT (stream), "sysfs-path"); + + return TRUE; +} + +/** + * gvc_mixer_stream_get_base_volume: + * + * @stream: + * + * Returns: (type guint32) (transfer none): + */ +pa_volume_t +gvc_mixer_stream_get_base_volume (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + + return stream->priv->base_volume; +} + +/** + * gvc_mixer_stream_set_base_volume: + * + * @stream: + * @base_volume: (type guint32): + * + * Returns: + */ +gboolean +gvc_mixer_stream_set_base_volume (GvcMixerStream *stream, + pa_volume_t base_volume) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + stream->priv->base_volume = base_volume; + + return TRUE; +} + +const GvcMixerStreamPort * +gvc_mixer_stream_get_port (GvcMixerStream *stream) +{ + GList *l; + + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + g_return_val_if_fail (stream->priv->ports != NULL, NULL); + + for (l = stream->priv->ports; l != NULL; l = l->next) { + GvcMixerStreamPort *p = l->data; + if (g_strcmp0 (stream->priv->port, p->port) == 0) { + return p; + } + } + + g_assert_not_reached (); + + return NULL; +} + +gboolean +gvc_mixer_stream_set_port (GvcMixerStream *stream, + const char *port) +{ + GList *l; + + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + g_return_val_if_fail (stream->priv->ports != NULL, FALSE); + + g_free (stream->priv->port); + stream->priv->port = g_strdup (port); + + g_free (stream->priv->human_port); + stream->priv->human_port = NULL; + + for (l = stream->priv->ports; l != NULL; l = l->next) { + GvcMixerStreamPort *p = l->data; + if (g_str_equal (stream->priv->port, p->port)) { + stream->priv->human_port = g_strdup (p->human_port); + break; + } + } + + g_object_notify (G_OBJECT (stream), "port"); + + return TRUE; +} + +gboolean +gvc_mixer_stream_change_port (GvcMixerStream *stream, + const char *port) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + return GVC_MIXER_STREAM_GET_CLASS (stream)->change_port (stream, port); +} + +/** + * gvc_mixer_stream_get_ports: + * + * Return value: (transfer none) (element-type GvcMixerStreamPort): + */ +const GList * +gvc_mixer_stream_get_ports (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->ports; +} + +static int +sort_ports (GvcMixerStreamPort *a, + GvcMixerStreamPort *b) +{ + if (a->priority == b->priority) + return 0; + if (a->priority > b->priority) + return 1; + return -1; +} + +/** + * gvc_mixer_stream_set_ports: + * @ports: (transfer full) (element-type GvcMixerStreamPort): + */ +gboolean +gvc_mixer_stream_set_ports (GvcMixerStream *stream, + GList *ports) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + g_return_val_if_fail (stream->priv->ports == NULL, FALSE); + + stream->priv->ports = g_list_sort (ports, (GCompareFunc) sort_ports); + + return TRUE; +} + +gint +gvc_mixer_stream_get_card_index (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), PA_INVALID_INDEX); + return stream->priv->card_index; +} + +gboolean +gvc_mixer_stream_set_card_index (GvcMixerStream *stream, + gint card_index) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + stream->priv->card_index = card_index; + g_object_notify (G_OBJECT (stream), "card-index"); + + return TRUE; +} + +static void +gvc_mixer_stream_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GvcMixerStream *self = GVC_MIXER_STREAM (object); + + switch (prop_id) { + case PROP_PA_CONTEXT: + self->priv->pa_context = g_value_get_pointer (value); + break; + case PROP_INDEX: + self->priv->index = g_value_get_ulong (value); + break; + case PROP_ID: + self->priv->id = g_value_get_ulong (value); + break; + case PROP_CHANNEL_MAP: + gvc_mixer_stream_set_channel_map (self, g_value_get_object (value)); + break; + case PROP_NAME: + gvc_mixer_stream_set_name (self, g_value_get_string (value)); + break; + case PROP_DESCRIPTION: + gvc_mixer_stream_set_description (self, g_value_get_string (value)); + break; + case PROP_APPLICATION_ID: + gvc_mixer_stream_set_application_id (self, g_value_get_string (value)); + break; + case PROP_ICON_NAME: + gvc_mixer_stream_set_icon_name (self, g_value_get_string (value)); + break; + case PROP_SYSFS_PATH: + gvc_mixer_stream_set_sysfs_path (self, g_value_get_string (value)); + break; + case PROP_VOLUME: + gvc_mixer_stream_set_volume (self, g_value_get_ulong (value)); + break; + case PROP_DECIBEL: + gvc_mixer_stream_set_decibel (self, g_value_get_double (value)); + break; + case PROP_IS_MUTED: + gvc_mixer_stream_set_is_muted (self, g_value_get_boolean (value)); + break; + case PROP_IS_EVENT_STREAM: + gvc_mixer_stream_set_is_event_stream (self, g_value_get_boolean (value)); + break; + case PROP_IS_VIRTUAL: + gvc_mixer_stream_set_is_virtual (self, g_value_get_boolean (value)); + break; + case PROP_CAN_DECIBEL: + gvc_mixer_stream_set_can_decibel (self, g_value_get_boolean (value)); + break; + case PROP_PORT: + gvc_mixer_stream_set_port (self, g_value_get_string (value)); + break; + case PROP_CARD_INDEX: + self->priv->card_index = g_value_get_long (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gvc_mixer_stream_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GvcMixerStream *self = GVC_MIXER_STREAM (object); + + switch (prop_id) { + case PROP_PA_CONTEXT: + g_value_set_pointer (value, self->priv->pa_context); + break; + case PROP_INDEX: + g_value_set_ulong (value, self->priv->index); + break; + case PROP_ID: + g_value_set_ulong (value, self->priv->id); + break; + case PROP_CHANNEL_MAP: + g_value_set_object (value, self->priv->channel_map); + break; + case PROP_NAME: + g_value_set_string (value, self->priv->name); + break; + case PROP_DESCRIPTION: + g_value_set_string (value, self->priv->description); + break; + case PROP_APPLICATION_ID: + g_value_set_string (value, self->priv->application_id); + break; + case PROP_ICON_NAME: + g_value_set_string (value, self->priv->icon_name); + break; + case PROP_SYSFS_PATH: + g_value_set_string (value, self->priv->sysfs_path); + break; + case PROP_VOLUME: + g_value_set_ulong (value, + pa_cvolume_max(gvc_channel_map_get_cvolume(self->priv->channel_map))); + break; + case PROP_DECIBEL: + g_value_set_double (value, + pa_sw_volume_to_dB(pa_cvolume_max(gvc_channel_map_get_cvolume(self->priv->channel_map)))); + break; + case PROP_IS_MUTED: + g_value_set_boolean (value, self->priv->is_muted); + break; + case PROP_IS_EVENT_STREAM: + g_value_set_boolean (value, self->priv->is_event_stream); + break; + case PROP_IS_VIRTUAL: + g_value_set_boolean (value, self->priv->is_virtual); + break; + case PROP_CAN_DECIBEL: + g_value_set_boolean (value, self->priv->can_decibel); + break; + case PROP_PORT: + g_value_set_string (value, self->priv->port); + break; + case PROP_CARD_INDEX: + g_value_set_long (value, self->priv->card_index); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static GObject * +gvc_mixer_stream_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_params) +{ + GObject *object; + GvcMixerStream *self; + + object = G_OBJECT_CLASS (gvc_mixer_stream_parent_class)->constructor (type, n_construct_properties, construct_params); + + self = GVC_MIXER_STREAM (object); + + self->priv->id = get_next_stream_serial (); + + return object; +} + +static gboolean +gvc_mixer_stream_real_change_port (GvcMixerStream *stream, + const char *port) +{ + return FALSE; +} + +static gboolean +gvc_mixer_stream_real_push_volume (GvcMixerStream *stream, gpointer *op) +{ + return FALSE; +} + +static gboolean +gvc_mixer_stream_real_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + return FALSE; +} + +gboolean +gvc_mixer_stream_push_volume (GvcMixerStream *stream) +{ + pa_operation *op; + gboolean ret; + + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + if (stream->priv->is_event_stream != FALSE) + return TRUE; + + g_debug ("Pushing new volume to stream '%s' (%s)", + stream->priv->description, stream->priv->name); + + ret = GVC_MIXER_STREAM_GET_CLASS (stream)->push_volume (stream, (gpointer *) &op); + if (ret) { + if (stream->priv->change_volume_op != NULL) + pa_operation_unref (stream->priv->change_volume_op); + stream->priv->change_volume_op = op; + } + return ret; +} + +gboolean +gvc_mixer_stream_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + gboolean ret; + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + ret = GVC_MIXER_STREAM_GET_CLASS (stream)->change_is_muted (stream, is_muted); + return ret; +} + +gboolean +gvc_mixer_stream_is_running (GvcMixerStream *stream) +{ + if (stream->priv->change_volume_op == NULL) + return FALSE; + + if ((pa_operation_get_state(stream->priv->change_volume_op) == PA_OPERATION_RUNNING)) + return TRUE; + + pa_operation_unref(stream->priv->change_volume_op); + stream->priv->change_volume_op = NULL; + + return FALSE; +} + +static void +gvc_mixer_stream_class_init (GvcMixerStreamClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->constructor = gvc_mixer_stream_constructor; + gobject_class->finalize = gvc_mixer_stream_finalize; + gobject_class->set_property = gvc_mixer_stream_set_property; + gobject_class->get_property = gvc_mixer_stream_get_property; + + klass->push_volume = gvc_mixer_stream_real_push_volume; + klass->change_port = gvc_mixer_stream_real_change_port; + klass->change_is_muted = gvc_mixer_stream_real_change_is_muted; + + g_object_class_install_property (gobject_class, + PROP_INDEX, + g_param_spec_ulong ("index", + "Index", + "The index for this stream", + 0, G_MAXULONG, 0, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property (gobject_class, + PROP_ID, + g_param_spec_ulong ("id", + "id", + "The id for this stream", + 0, G_MAXULONG, 0, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property (gobject_class, + PROP_CHANNEL_MAP, + g_param_spec_object ("channel-map", + "channel map", + "The channel map for this stream", + GVC_TYPE_CHANNEL_MAP, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_PA_CONTEXT, + g_param_spec_pointer ("pa-context", + "PulseAudio context", + "The PulseAudio context for this stream", + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property (gobject_class, + PROP_VOLUME, + g_param_spec_ulong ("volume", + "Volume", + "The volume for this stream", + 0, G_MAXULONG, 0, + G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, + PROP_DECIBEL, + g_param_spec_double ("decibel", + "Decibel", + "The decibel level for this stream", + -G_MAXDOUBLE, G_MAXDOUBLE, 0, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + + g_object_class_install_property (gobject_class, + PROP_NAME, + g_param_spec_string ("name", + "Name", + "Name to display for this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_DESCRIPTION, + g_param_spec_string ("description", + "Description", + "Description to display for this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_APPLICATION_ID, + g_param_spec_string ("application-id", + "Application identifier", + "Application identifier for this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_ICON_NAME, + g_param_spec_string ("icon-name", + "Icon Name", + "Name of icon to display for this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_SYSFS_PATH, + g_param_spec_string ("sysfs-path", + "Sysfs path", + "Sysfs path for the device associated with this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_IS_MUTED, + g_param_spec_boolean ("is-muted", + "is muted", + "Whether stream is muted", + FALSE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_CAN_DECIBEL, + g_param_spec_boolean ("can-decibel", + "can decibel", + "Whether stream volume can be converted to decibel units", + FALSE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_IS_EVENT_STREAM, + g_param_spec_boolean ("is-event-stream", + "is event stream", + "Whether stream's role is to play an event", + FALSE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_IS_VIRTUAL, + g_param_spec_boolean ("is-virtual", + "is virtual stream", + "Whether the stream is virtual", + FALSE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_PORT, + g_param_spec_string ("port", + "Port", + "The name of the current port for this stream", + NULL, + G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, + PROP_CARD_INDEX, + g_param_spec_long ("card-index", + "Card index", + "The index of the card for this stream", + PA_INVALID_INDEX, G_MAXLONG, PA_INVALID_INDEX, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_type_class_add_private (klass, sizeof (GvcMixerStreamPrivate)); +} + +static void +gvc_mixer_stream_init (GvcMixerStream *stream) +{ + stream->priv = GVC_MIXER_STREAM_GET_PRIVATE (stream); +} + +static void +free_port (GvcMixerStreamPort *p) +{ + g_free (p->port); + g_free (p->human_port); + g_free (p); +} + +static void +gvc_mixer_stream_finalize (GObject *object) +{ + GvcMixerStream *mixer_stream; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_STREAM (object)); + + mixer_stream = GVC_MIXER_STREAM (object); + + g_return_if_fail (mixer_stream->priv != NULL); + + g_object_unref (mixer_stream->priv->channel_map); + mixer_stream->priv->channel_map = NULL; + + g_free (mixer_stream->priv->name); + mixer_stream->priv->name = NULL; + + g_free (mixer_stream->priv->description); + mixer_stream->priv->description = NULL; + + g_free (mixer_stream->priv->application_id); + mixer_stream->priv->application_id = NULL; + + g_free (mixer_stream->priv->icon_name); + mixer_stream->priv->icon_name = NULL; + + g_free (mixer_stream->priv->sysfs_path); + mixer_stream->priv->sysfs_path = NULL; + + g_free (mixer_stream->priv->port); + mixer_stream->priv->port = NULL; + + g_free (mixer_stream->priv->human_port); + mixer_stream->priv->human_port = NULL; + + g_list_foreach (mixer_stream->priv->ports, (GFunc) free_port, NULL); + g_list_free (mixer_stream->priv->ports); + mixer_stream->priv->ports = NULL; + + if (mixer_stream->priv->change_volume_op) { + pa_operation_unref(mixer_stream->priv->change_volume_op); + mixer_stream->priv->change_volume_op = NULL; + } + + G_OBJECT_CLASS (gvc_mixer_stream_parent_class)->finalize (object); +} diff --git a/gvc-mixer-stream.h b/gvc-mixer-stream.h new file mode 100644 index 0000000..846f2e0 --- /dev/null +++ b/gvc-mixer-stream.h @@ -0,0 +1,131 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * 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. + * + */ + +#ifndef __GVC_MIXER_STREAM_H +#define __GVC_MIXER_STREAM_H + +#include <glib-object.h> +#include "gvc-pulseaudio-fake.h" +#include "gvc-channel-map.h" +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_STREAM (gvc_mixer_stream_get_type ()) +#define GVC_MIXER_STREAM(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_STREAM, GvcMixerStream)) +#define GVC_MIXER_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_STREAM, GvcMixerStreamClass)) +#define GVC_IS_MIXER_STREAM(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_STREAM)) +#define GVC_IS_MIXER_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_STREAM)) +#define GVC_MIXER_STREAM_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_STREAM, GvcMixerStreamClass)) + +typedef struct GvcMixerStreamPrivate GvcMixerStreamPrivate; + +typedef struct +{ + GObject parent; + GvcMixerStreamPrivate *priv; +} GvcMixerStream; + +typedef struct +{ + GObjectClass parent_class; + + /* vtable */ + gboolean (*push_volume) (GvcMixerStream *stream, + gpointer *operation); + gboolean (*change_is_muted) (GvcMixerStream *stream, + gboolean is_muted); + gboolean (*change_port) (GvcMixerStream *stream, + const char *port); +} GvcMixerStreamClass; + +typedef struct +{ + char *port; + char *human_port; + guint priority; + gboolean available; +} GvcMixerStreamPort; + +GType gvc_mixer_stream_get_type (void); + +guint gvc_mixer_stream_get_index (GvcMixerStream *stream); +guint gvc_mixer_stream_get_id (GvcMixerStream *stream); +const GvcChannelMap *gvc_mixer_stream_get_channel_map(GvcMixerStream *stream); +const GvcMixerStreamPort *gvc_mixer_stream_get_port (GvcMixerStream *stream); +const GList * gvc_mixer_stream_get_ports (GvcMixerStream *stream); +gboolean gvc_mixer_stream_change_port (GvcMixerStream *stream, + const char *port); + +pa_volume_t gvc_mixer_stream_get_volume (GvcMixerStream *stream); +gdouble gvc_mixer_stream_get_decibel (GvcMixerStream *stream); +gboolean gvc_mixer_stream_push_volume (GvcMixerStream *stream); +pa_volume_t gvc_mixer_stream_get_base_volume (GvcMixerStream *stream); + +gboolean gvc_mixer_stream_get_is_muted (GvcMixerStream *stream); +gboolean gvc_mixer_stream_get_can_decibel (GvcMixerStream *stream); +gboolean gvc_mixer_stream_change_is_muted (GvcMixerStream *stream, + gboolean is_muted); +gboolean gvc_mixer_stream_is_running (GvcMixerStream *stream); +const char * gvc_mixer_stream_get_name (GvcMixerStream *stream); +const char * gvc_mixer_stream_get_icon_name (GvcMixerStream *stream); +const char * gvc_mixer_stream_get_sysfs_path (GvcMixerStream *stream); +GIcon * gvc_mixer_stream_get_gicon (GvcMixerStream *stream); +const char * gvc_mixer_stream_get_description (GvcMixerStream *stream); +const char * gvc_mixer_stream_get_application_id (GvcMixerStream *stream); +gboolean gvc_mixer_stream_is_event_stream (GvcMixerStream *stream); +gboolean gvc_mixer_stream_is_virtual (GvcMixerStream *stream); +gint gvc_mixer_stream_get_card_index (GvcMixerStream *stream); + +/* private */ +gboolean gvc_mixer_stream_set_volume (GvcMixerStream *stream, + pa_volume_t volume); +gboolean gvc_mixer_stream_set_decibel (GvcMixerStream *stream, + gdouble db); +gboolean gvc_mixer_stream_set_is_muted (GvcMixerStream *stream, + gboolean is_muted); +gboolean gvc_mixer_stream_set_can_decibel (GvcMixerStream *stream, + gboolean can_decibel); +gboolean gvc_mixer_stream_set_name (GvcMixerStream *stream, + const char *name); +gboolean gvc_mixer_stream_set_description (GvcMixerStream *stream, + const char *description); +gboolean gvc_mixer_stream_set_icon_name (GvcMixerStream *stream, + const char *name); +gboolean gvc_mixer_stream_set_sysfs_path (GvcMixerStream *stream, + const char *sysfs_path); +gboolean gvc_mixer_stream_set_is_event_stream (GvcMixerStream *stream, + gboolean is_event_stream); +gboolean gvc_mixer_stream_set_is_virtual (GvcMixerStream *stream, + gboolean is_event_stream); +gboolean gvc_mixer_stream_set_application_id (GvcMixerStream *stream, + const char *application_id); +gboolean gvc_mixer_stream_set_base_volume (GvcMixerStream *stream, + pa_volume_t base_volume); +gboolean gvc_mixer_stream_set_port (GvcMixerStream *stream, + const char *port); +gboolean gvc_mixer_stream_set_ports (GvcMixerStream *stream, + GList *ports); +gboolean gvc_mixer_stream_set_card_index (GvcMixerStream *stream, + gint card_index); + +G_END_DECLS + +#endif /* __GVC_MIXER_STREAM_H */ diff --git a/gvc-mixer-ui-device.c b/gvc-mixer-ui-device.c new file mode 100644 index 0000000..6f35990 --- /dev/null +++ b/gvc-mixer-ui-device.c @@ -0,0 +1,656 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */ +/* + * gvc-mixer-ui-device.c + * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com> + * Copyright (C) 2012 David Henningsson, Canonical Ltd. <david.henningsson@canonical.com> + * + * gvc-mixer-ui-device.c 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. + * + * gvc-mixer-ui-device.c 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, see <http://www.gnu.org/licenses/>. + */ + +#include <string.h> + +#include "gvc-mixer-ui-device.h" +#include "gvc-mixer-card.h" + +#define GVC_MIXER_UI_DEVICE_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GVC_TYPE_MIXER_UI_DEVICE, GvcMixerUIDevicePrivate)) + +struct GvcMixerUIDevicePrivate +{ + gchar *first_line_desc; + gchar *second_line_desc; + + GvcMixerCard *card; + gchar *port_name; + gint stream_id; + guint id; + gboolean port_available; + + /* These two lists contain pointers to GvcMixerCardProfile objects. Those objects are owned by GvcMixerCard. * + * TODO: Do we want to add a weak reference to the GvcMixerCard for this reason? */ + GList *supported_profiles; /* all profiles supported by this port.*/ + GList *profiles; /* profiles to be added to combobox, subset of supported_profiles. */ + GvcMixerUIDeviceDirection type; + gboolean disable_profile_swapping; + gchar *user_preferred_profile; +}; + +enum +{ + PROP_0, + PROP_DESC_LINE_1, + PROP_DESC_LINE_2, + PROP_CARD, + PROP_PORT_NAME, + PROP_STREAM_ID, + PROP_UI_DEVICE_TYPE, + PROP_PORT_AVAILABLE, +}; + +static void gvc_mixer_ui_device_class_init (GvcMixerUIDeviceClass *klass); +static void gvc_mixer_ui_device_init (GvcMixerUIDevice *device); +static void gvc_mixer_ui_device_finalize (GObject *object); + +G_DEFINE_TYPE (GvcMixerUIDevice, gvc_mixer_ui_device, G_TYPE_OBJECT); + +static guint32 +get_next_output_serial (void) +{ + static guint32 output_serial = 1; + guint32 serial; + + serial = output_serial++; + + if ((gint32)output_serial < 0) + output_serial = 1; + + return serial; +} + +static void +gvc_mixer_ui_device_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GvcMixerUIDevice *self = GVC_MIXER_UI_DEVICE (object); + + switch (property_id) { + case PROP_DESC_LINE_1: + g_value_set_string (value, self->priv->first_line_desc); + break; + case PROP_DESC_LINE_2: + g_value_set_string (value, self->priv->second_line_desc); + break; + case PROP_CARD: + g_value_set_pointer (value, self->priv->card); + break; + case PROP_PORT_NAME: + g_value_set_string (value, self->priv->port_name); + break; + case PROP_STREAM_ID: + g_value_set_int (value, self->priv->stream_id); + break; + case PROP_UI_DEVICE_TYPE: + g_value_set_uint (value, (guint)self->priv->type); + break; + case PROP_PORT_AVAILABLE: + g_value_set_boolean (value, self->priv->port_available); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +gvc_mixer_ui_device_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GvcMixerUIDevice *self = GVC_MIXER_UI_DEVICE (object); + + switch (property_id) { + case PROP_DESC_LINE_1: + g_free (self->priv->first_line_desc); + self->priv->first_line_desc = g_value_dup_string (value); + g_debug ("gvc-mixer-output-set-property - 1st line: %s\n", + self->priv->first_line_desc); + break; + case PROP_DESC_LINE_2: + g_free (self->priv->second_line_desc); + self->priv->second_line_desc = g_value_dup_string (value); + g_debug ("gvc-mixer-output-set-property - 2nd line: %s\n", + self->priv->second_line_desc); + break; + case PROP_CARD: + self->priv->card = g_value_get_pointer (value); + g_debug ("gvc-mixer-output-set-property - card: %p\n", + self->priv->card); + break; + case PROP_PORT_NAME: + g_free (self->priv->port_name); + self->priv->port_name = g_value_dup_string (value); + g_debug ("gvc-mixer-output-set-property - card port name: %s\n", + self->priv->port_name); + break; + case PROP_STREAM_ID: + self->priv->stream_id = g_value_get_int (value); + g_debug ("gvc-mixer-output-set-property - sink/source id: %i\n", + self->priv->stream_id); + break; + case PROP_UI_DEVICE_TYPE: + self->priv->type = (GvcMixerUIDeviceDirection) g_value_get_uint (value); + break; + case PROP_PORT_AVAILABLE: + self->priv->port_available = g_value_get_boolean (value); + g_debug ("gvc-mixer-output-set-property - port available %i, value passed in %i \n", + self->priv->port_available, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static GObject * +gvc_mixer_ui_device_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_params) +{ + GObject *object; + GvcMixerUIDevice *self; + + object = G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->constructor (type, n_construct_properties, construct_params); + + self = GVC_MIXER_UI_DEVICE (object); + self->priv->id = get_next_output_serial (); + self->priv->stream_id = GVC_MIXER_UI_DEVICE_INVALID; + return object; +} + +static void +gvc_mixer_ui_device_init (GvcMixerUIDevice *device) +{ + device->priv = GVC_MIXER_UI_DEVICE_GET_PRIVATE (device); +} + +static void +gvc_mixer_ui_device_dispose (GObject *object) +{ + GvcMixerUIDevice *device; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_MIXER_UI_DEVICE (object)); + + device = GVC_MIXER_UI_DEVICE (object); + + g_clear_pointer (&device->priv->port_name, g_free); + g_clear_pointer (&device->priv->first_line_desc, g_free); + g_clear_pointer (&device->priv->second_line_desc, g_free); + g_clear_pointer (&device->priv->profiles, g_list_free); + g_clear_pointer (&device->priv->supported_profiles, g_list_free); + g_clear_pointer (&device->priv->user_preferred_profile, g_free); + + G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->dispose (object); +} + +static void +gvc_mixer_ui_device_finalize (GObject *object) +{ + G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->finalize (object); +} + +static void +gvc_mixer_ui_device_class_init (GvcMixerUIDeviceClass *klass) +{ + GObjectClass* object_class = G_OBJECT_CLASS (klass); + GParamSpec *pspec; + + object_class->constructor = gvc_mixer_ui_device_constructor; + object_class->dispose = gvc_mixer_ui_device_dispose; + object_class->finalize = gvc_mixer_ui_device_finalize; + object_class->set_property = gvc_mixer_ui_device_set_property; + object_class->get_property = gvc_mixer_ui_device_get_property; + + pspec = g_param_spec_string ("description", + "Description construct prop", + "Set first line description", + "no-name-set", + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_DESC_LINE_1, pspec); + + pspec = g_param_spec_string ("origin", + "origin construct prop", + "Set second line description name", + "no-name-set", + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_DESC_LINE_2, pspec); + + pspec = g_param_spec_pointer ("card", + "Card from pulse", + "Set/Get card", + G_PARAM_READWRITE); + + g_object_class_install_property (object_class, PROP_CARD, pspec); + + pspec = g_param_spec_string ("port-name", + "port-name construct prop", + "Set port-name", + NULL, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_PORT_NAME, pspec); + + pspec = g_param_spec_int ("stream-id", + "stream id assigned by gvc-stream", + "Set/Get stream id", + -1, + G_MAXINT, + GVC_MIXER_UI_DEVICE_INVALID, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_STREAM_ID, pspec); + + pspec = g_param_spec_uint ("type", + "ui-device type", + "determine whether its an input and output", + 0, 1, 0, G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_UI_DEVICE_TYPE, pspec); + + pspec = g_param_spec_boolean ("port-available", + "available", + "determine whether this port is available", + FALSE, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_PORT_AVAILABLE, pspec); + + g_type_class_add_private (klass, sizeof (GvcMixerUIDevicePrivate)); +} + +/* Removes the part of the string that starts with skip_prefix + * ie. corresponding to the other direction. + * Normally either "input:" or "output:" + * + * Example: if given the input string "output:hdmi-stereo+input:analog-stereo" and + * skip_prefix "input:", the resulting string is "output:hdmi-stereo". + * + * The returned string must be freed with g_free(). + */ +static gchar * +get_profile_canonical_name (const gchar *profile_name, const gchar *skip_prefix) +{ + gchar *result = NULL; + gchar **s; + int i; + + /* optimisation for the simple case. */ + if (strstr (profile_name, skip_prefix) == NULL) + return g_strdup (profile_name); + + s = g_strsplit (profile_name, "+", 0); + for (i = 0; i < g_strv_length (s); i++) { + if (g_str_has_prefix (s[i], skip_prefix)) + continue; + if (result == NULL) + result = g_strdup (s[i]); + else { + gchar *c = g_strdup_printf("%s+%s", result, s[i]); + g_free(result); + result = c; + } + } + + g_strfreev(s); + + if (!result) + return g_strdup("off"); + + return result; +} + +const gchar * +gvc_mixer_ui_device_get_matching_profile (GvcMixerUIDevice *device, const gchar *profile) +{ + gchar *skip_prefix = device->priv->type == UIDeviceInput ? "output:" : "input:"; + gchar *target_cname = get_profile_canonical_name (profile, skip_prefix); + GList *l; + gchar *result = NULL; + + for (l = device->priv->profiles; l != NULL; l = l->next) { + gchar *canonical_name; + GvcMixerCardProfile* p = l->data; + canonical_name = get_profile_canonical_name (p->profile, skip_prefix); + if (strcmp (canonical_name, target_cname) == 0) + result = p->profile; + g_free (canonical_name); + } + + g_free (target_cname); + g_debug ("Matching profile for '%s' is '%s'", profile, result ? result : "(null)"); + return result; +} + + +static void +add_canonical_names_of_profiles (GvcMixerUIDevice *device, + const GList *in_profiles, + GHashTable *added_profiles, + const gchar *skip_prefix, + gboolean only_canonical) +{ + const GList *l; + + for (l = in_profiles; l != NULL; l = l->next) { + gchar *canonical_name; + GvcMixerCardProfile* p = l->data; + + canonical_name = get_profile_canonical_name (p->profile, skip_prefix); + g_debug ("The canonical name for '%s' is '%s'", p->profile, canonical_name); + + /* Have we already added the canonical version of this profile? */ + if (g_hash_table_contains (added_profiles, canonical_name)) { + g_free (canonical_name); + continue; + } + + if (only_canonical && strcmp (p->profile, canonical_name) != 0) { + g_free (canonical_name); + continue; + } + + g_free (canonical_name); + + g_debug ("Adding profile to combobox: '%s' - '%s'", p->profile, p->human_profile); + g_hash_table_insert (added_profiles, g_strdup (p->profile), p); + device->priv->profiles = g_list_append (device->priv->profiles, p); + } +} + +/** + * gvc_mixer_ui_device_set_profiles: + * + * @in_profiles: a list of GvcMixerCardProfile + * + * Assigns value to + * - device->priv->profiles (profiles to be added to combobox) + * - device->priv->supported_profiles (all profiles of this port) + * - device->priv->disable_profile_swapping (whether to show the combobox) + * + * This method attempts to reduce the list of profiles visible to the user by figuring out + * from the context of that device (whether it's an input or an output) what profiles + * actually provide an alternative. + * + * It does this by the following. + * - It ignores off profiles. + * - It takes the canonical name of the profile. That name is what you get when you + * ignore the other direction. + * - In the first iteration, it only adds the names of canonical profiles - i e + * when the other side is turned off. + * - Normally the first iteration covers all cases, but sometimes (e g bluetooth) + * it doesn't, so add other profiles whose canonical name isn't already added + * in a second iteration. + */ +void +gvc_mixer_ui_device_set_profiles (GvcMixerUIDevice *device, + const GList *in_profiles) +{ + GHashTable *added_profiles; + gchar *skip_prefix = device->priv->type == UIDeviceInput ? "output:" : "input:"; + + g_debug ("Set profiles for '%s'", gvc_mixer_ui_device_get_description(device)); + + if (in_profiles == NULL) + return; + + device->priv->supported_profiles = g_list_copy ((GList*) in_profiles); + + added_profiles = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + /* Run two iterations: First, add profiles which are canonical themselves, + * Second, add profiles for which the canonical name is not added already. */ + + add_canonical_names_of_profiles(device, in_profiles, added_profiles, skip_prefix, TRUE); + add_canonical_names_of_profiles(device, in_profiles, added_profiles, skip_prefix, FALSE); + + /* TODO: Consider adding the "Off" profile here */ + + device->priv->disable_profile_swapping = g_hash_table_size (added_profiles) <= 1; + g_hash_table_destroy (added_profiles); +} + +/** + * gvc_mixer_ui_device_get_best_profile: + * + * @selected: The selected profile or its canonical name or %NULL for any profile + * @current: The currently selected profile + * + * Returns: (transfer none): a profile name, valid as long as the UI device profiles are. + */ +const gchar * +gvc_mixer_ui_device_get_best_profile (GvcMixerUIDevice *device, + const gchar *selected, + const gchar *current) +{ + GList *candidates, *l; + const gchar *result; + gchar *skip_prefix; + gchar *canonical_name_selected; + + if (device->priv->type == UIDeviceInput) + skip_prefix = "output:"; + else + skip_prefix = "input:"; + + /* First make a list of profiles acceptable to switch to */ + canonical_name_selected = NULL; + if (selected) + canonical_name_selected = get_profile_canonical_name (selected, skip_prefix); + + candidates = NULL; + for (l = device->priv->supported_profiles; l != NULL; l = l->next) { + gchar *canonical_name; + GvcMixerCardProfile* p = l->data; + canonical_name = get_profile_canonical_name (p->profile, skip_prefix); + if (!canonical_name_selected || strcmp (canonical_name, canonical_name_selected) == 0) { + candidates = g_list_append (candidates, p); + g_debug ("Candidate for profile switching: '%s'", p->profile); + } + } + + if (!candidates) { + g_warning ("No suitable profile candidates for '%s'", selected ? selected : "(null)"); + g_free (canonical_name_selected); + return current; + } + + /* 1) Maybe we can skip profile switching altogether? */ + result = NULL; + for (l = candidates; (result == NULL) && (l != NULL); l = l->next) { + GvcMixerCardProfile* p = l->data; + if (strcmp (current, p->profile) == 0) + result = p->profile; + } + + /* 2) Try to keep the other side unchanged if possible */ + if (result == NULL) { + guint prio = 0; + gchar *skip_prefix_reverse = device->priv->type == UIDeviceInput ? "input:" : "output:"; + gchar *current_reverse = get_profile_canonical_name (current, skip_prefix_reverse); + for (l = candidates; l != NULL; l = l->next) { + gchar *p_reverse; + GvcMixerCardProfile* p = l->data; + p_reverse = get_profile_canonical_name (p->profile, skip_prefix_reverse); + g_debug ("Comparing '%s' (from '%s') with '%s', prio %d", p_reverse, p->profile, current_reverse, p->priority); + if (strcmp (p_reverse, current_reverse) == 0 && (!result || p->priority > prio)) { + result = p->profile; + prio = p->priority; + } + g_free (p_reverse); + } + g_free (current_reverse); + } + + /* 3) All right, let's just pick the profile with highest priority. + * TODO: We could consider asking a GUI question if this stops streams + * in the other direction */ + if (result == NULL) { + guint prio = 0; + for (l = candidates; l != NULL; l = l->next) { + GvcMixerCardProfile* p = l->data; + if ((p->priority > prio) || !result) { + result = p->profile; + prio = p->priority; + } + } + } + + g_list_free (candidates); + g_free (canonical_name_selected); + return result; +} + +const gchar * +gvc_mixer_ui_device_get_active_profile (GvcMixerUIDevice* device) +{ + GvcMixerCardProfile *profile; + + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + if (device->priv->card == NULL) { + g_warning ("Device did not have an appropriate card"); + return NULL; + } + + profile = gvc_mixer_card_get_profile (device->priv->card); + return gvc_mixer_ui_device_get_matching_profile (device, profile->profile); +} + +gboolean +gvc_mixer_ui_device_should_profiles_be_hidden (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE); + + return device->priv->disable_profile_swapping; +} + +GList* +gvc_mixer_ui_device_get_profiles (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->profiles; +} + +GList* +gvc_mixer_ui_device_get_supported_profiles (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->supported_profiles; +} + +guint +gvc_mixer_ui_device_get_id (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), 0); + + return device->priv->id; +} + +gint +gvc_mixer_ui_device_get_stream_id (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), 0); + + return device->priv->stream_id; +} + +void +gvc_mixer_ui_device_invalidate_stream (GvcMixerUIDevice *self) +{ + g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (self)); + + self->priv->stream_id = GVC_MIXER_UI_DEVICE_INVALID; +} + +const gchar * +gvc_mixer_ui_device_get_description (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->first_line_desc; +} + +const gchar * +gvc_mixer_ui_device_get_origin (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->second_line_desc; +} + +const gchar* +gvc_mixer_ui_device_get_user_preferred_profile (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->user_preferred_profile; +} + +const gchar * +gvc_mixer_ui_device_get_top_priority_profile (GvcMixerUIDevice *device) +{ + GList *last; + GvcMixerCardProfile *profile; + + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + last = g_list_last (device->priv->supported_profiles); + profile = last->data; + + return profile->profile; +} + +void +gvc_mixer_ui_device_set_user_preferred_profile (GvcMixerUIDevice *device, + const gchar *profile) +{ + g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (device)); + + g_free (device->priv->user_preferred_profile); + device->priv->user_preferred_profile = g_strdup (profile); +} + +const gchar * +gvc_mixer_ui_device_get_port (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->port_name; +} + +gboolean +gvc_mixer_ui_device_has_ports (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE); + + return (device->priv->port_name != NULL); +} + +gboolean +gvc_mixer_ui_device_is_output (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE); + + return (device->priv->type == UIDeviceOutput); +} diff --git a/gvc-mixer-ui-device.h b/gvc-mixer-ui-device.h new file mode 100644 index 0000000..fbf3b9c --- /dev/null +++ b/gvc-mixer-ui-device.h @@ -0,0 +1,82 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */ +/* + * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com> + * + * This 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. + * + * gvc-mixer-ui-device.h 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, see <http://www.gnu.org/licenses/>. + */ + +#ifndef _GVC_MIXER_UI_DEVICE_H_ +#define _GVC_MIXER_UI_DEVICE_H_ + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_UI_DEVICE (gvc_mixer_ui_device_get_type ()) +#define GVC_MIXER_UI_DEVICE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GVC_TYPE_MIXER_UI_DEVICE, GvcMixerUIDevice)) +#define GVC_MIXER_UI_DEVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GVC_TYPE_MIXER_UI_DEVICE, GvcMixerUIDeviceClass)) +#define GVC_IS_MIXER_UI_DEVICE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GVC_TYPE_MIXER_UI_DEVICE)) +#define GVC_IS_MIXER_UI_DEVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GVC_TYPE_MIXER_UI_DEVICE)) +#define GVC_MIXER_UI_DEVICE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GVC_TYPE_MIXER_UI_DEVICE, GvcMixerUIDeviceClass)) + +#define GVC_MIXER_UI_DEVICE_INVALID -1 + +typedef struct GvcMixerUIDevicePrivate GvcMixerUIDevicePrivate; + +typedef struct +{ + GObjectClass parent_class; +} GvcMixerUIDeviceClass; + +typedef struct +{ + GObject parent_instance; + GvcMixerUIDevicePrivate *priv; +} GvcMixerUIDevice; + +typedef enum +{ + UIDeviceInput, + UIDeviceOutput, +} GvcMixerUIDeviceDirection; + +GType gvc_mixer_ui_device_get_type (void) G_GNUC_CONST; + +guint gvc_mixer_ui_device_get_id (GvcMixerUIDevice *dev); +gint gvc_mixer_ui_device_get_stream_id (GvcMixerUIDevice *dev); +const gchar * gvc_mixer_ui_device_get_description (GvcMixerUIDevice *dev); +const gchar * gvc_mixer_ui_device_get_origin (GvcMixerUIDevice *dev); +const gchar * gvc_mixer_ui_device_get_port (GvcMixerUIDevice *dev); +const gchar * gvc_mixer_ui_device_get_best_profile (GvcMixerUIDevice *dev, + const gchar *selected, + const gchar *current); +const gchar * gvc_mixer_ui_device_get_active_profile (GvcMixerUIDevice* device); +const gchar * gvc_mixer_ui_device_get_matching_profile (GvcMixerUIDevice *dev, + const gchar *profile); +const gchar * gvc_mixer_ui_device_get_user_preferred_profile (GvcMixerUIDevice *dev); +const gchar * gvc_mixer_ui_device_get_top_priority_profile (GvcMixerUIDevice *dev); +GList * gvc_mixer_ui_device_get_profiles (GvcMixerUIDevice *dev); +GList * gvc_mixer_ui_device_get_supported_profiles (GvcMixerUIDevice *device); +gboolean gvc_mixer_ui_device_should_profiles_be_hidden (GvcMixerUIDevice *device); +void gvc_mixer_ui_device_set_profiles (GvcMixerUIDevice *device, + const GList *profiles); +void gvc_mixer_ui_device_set_user_preferred_profile (GvcMixerUIDevice *device, + const gchar *profile); +void gvc_mixer_ui_device_invalidate_stream (GvcMixerUIDevice *dev); +gboolean gvc_mixer_ui_device_has_ports (GvcMixerUIDevice *dev); +gboolean gvc_mixer_ui_device_is_output (GvcMixerUIDevice *dev); + +G_END_DECLS + +#endif /* _GVC_MIXER_UI_DEVICE_H_ */ diff --git a/gvc-pulseaudio-fake.h b/gvc-pulseaudio-fake.h new file mode 100644 index 0000000..ab8f6b4 --- /dev/null +++ b/gvc-pulseaudio-fake.h @@ -0,0 +1,34 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * 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. + * + */ + +#ifndef __GVC_PULSEAUDIO_FAKE_H +#define __GVC_PULSEAUDIO_FAKE_H + +#ifdef WITH_INTROSPECTION + +#ifndef PA_API_VERSION +#define pa_channel_position_t int +#define pa_volume_t guint32 +#define pa_context gpointer +#endif /* PA_API_VERSION */ + +#endif /* WITH_INTROSPECTION */ + +#endif /* __GVC_PULSEAUDIO_FAKE_H */ |