summaryrefslogtreecommitdiffstats
path: root/panels/sound
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:45:20 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:45:20 +0000
commitae1c76ff830d146d41e88d6fba724c0a54bce868 (patch)
tree3c354bec95af07be35fc71a4b738268496f1a1c4 /panels/sound
parentInitial commit. (diff)
downloadgnome-control-center-ae1c76ff830d146d41e88d6fba724c0a54bce868.tar.xz
gnome-control-center-ae1c76ff830d146d41e88d6fba724c0a54bce868.zip
Adding upstream version 1:43.6.upstream/1%43.6upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'panels/sound')
-rw-r--r--panels/sound/cc-alert-chooser.c306
-rw-r--r--panels/sound/cc-alert-chooser.h28
-rw-r--r--panels/sound/cc-alert-chooser.ui41
-rw-r--r--panels/sound/cc-balance-slider.c119
-rw-r--r--panels/sound/cc-balance-slider.h33
-rw-r--r--panels/sound/cc-balance-slider.ui30
-rw-r--r--panels/sound/cc-device-combo-box.c213
-rw-r--r--panels/sound/cc-device-combo-box.h36
-rw-r--r--panels/sound/cc-device-combo-box.ui33
-rw-r--r--panels/sound/cc-fade-slider.c119
-rw-r--r--panels/sound/cc-fade-slider.h33
-rw-r--r--panels/sound/cc-fade-slider.ui30
-rw-r--r--panels/sound/cc-level-bar.c290
-rw-r--r--panels/sound/cc-level-bar.h36
-rw-r--r--panels/sound/cc-output-test-dialog.c160
-rw-r--r--panels/sound/cc-output-test-dialog.h34
-rw-r--r--panels/sound/cc-output-test-dialog.ui163
-rw-r--r--panels/sound/cc-profile-combo-box.c135
-rw-r--r--panels/sound/cc-profile-combo-box.h37
-rw-r--r--panels/sound/cc-profile-combo-box.ui22
-rw-r--r--panels/sound/cc-sound-enums.h30
-rw-r--r--panels/sound/cc-sound-panel.c318
-rw-r--r--panels/sound/cc-sound-panel.h28
-rw-r--r--panels/sound/cc-sound-panel.ui351
-rw-r--r--panels/sound/cc-speaker-test-button.c241
-rw-r--r--panels/sound/cc-speaker-test-button.h36
-rw-r--r--panels/sound/cc-speaker-test-button.ui30
-rw-r--r--panels/sound/cc-stream-list-box.c248
-rw-r--r--panels/sound/cc-stream-list-box.h38
-rw-r--r--panels/sound/cc-stream-list-box.ui15
-rw-r--r--panels/sound/cc-stream-row.c142
-rw-r--r--panels/sound/cc-stream-row.h43
-rw-r--r--panels/sound/cc-stream-row.ui39
-rw-r--r--panels/sound/cc-subwoofer-slider.c135
-rw-r--r--panels/sound/cc-subwoofer-slider.h37
-rw-r--r--panels/sound/cc-subwoofer-slider.ui21
-rw-r--r--panels/sound/cc-volume-slider.c263
-rw-r--r--panels/sound/cc-volume-slider.h45
-rw-r--r--panels/sound/cc-volume-slider.ui34
-rw-r--r--panels/sound/gnome-sound-panel.desktop.in.in19
-rw-r--r--panels/sound/gvc-mixer-stream-private.h30
-rw-r--r--panels/sound/icons/audio-speaker-center-back-testing.svg1
-rw-r--r--panels/sound/icons/audio-speaker-center-back.svg1
-rw-r--r--panels/sound/icons/audio-speaker-center-testing.svg1
-rw-r--r--panels/sound/icons/audio-speaker-center.svg1
-rw-r--r--panels/sound/icons/audio-speaker-left-back-testing.svg1
-rw-r--r--panels/sound/icons/audio-speaker-left-back.svg1
-rw-r--r--panels/sound/icons/audio-speaker-left-side-testing.svg1
-rw-r--r--panels/sound/icons/audio-speaker-left-side.svg1
-rw-r--r--panels/sound/icons/audio-speaker-left-testing.svg1
-rw-r--r--panels/sound/icons/audio-speaker-left.svg1
-rw-r--r--panels/sound/icons/audio-speaker-mono-testing.svg1
-rw-r--r--panels/sound/icons/audio-speaker-mono.svg1
-rw-r--r--panels/sound/icons/audio-speaker-right-back-testing.svg1
-rw-r--r--panels/sound/icons/audio-speaker-right-back.svg1
-rw-r--r--panels/sound/icons/audio-speaker-right-side-testing.svg1
-rw-r--r--panels/sound/icons/audio-speaker-right-side.svg1
-rw-r--r--panels/sound/icons/audio-speaker-right-testing.svg1
-rw-r--r--panels/sound/icons/audio-speaker-right.svg1
-rw-r--r--panels/sound/icons/audio-speaker-testing.svg1
-rw-r--r--panels/sound/icons/audio-subwoofer-testing.svg1
-rw-r--r--panels/sound/icons/audio-subwoofer.svg1
-rw-r--r--panels/sound/icons/org.gnome.Settings-sound-symbolic.svg1
-rw-r--r--panels/sound/meson.build112
-rw-r--r--panels/sound/sound.gresource.xml40
-rw-r--r--panels/sound/sounds/click.oggbin0 -> 10510 bytes
-rw-r--r--panels/sound/sounds/hum.oggbin0 -> 12632 bytes
-rw-r--r--panels/sound/sounds/string.oggbin0 -> 18245 bytes
-rw-r--r--panels/sound/sounds/swing.oggbin0 -> 11445 bytes
69 files changed, 4215 insertions, 0 deletions
diff --git a/panels/sound/cc-alert-chooser.c b/panels/sound/cc-alert-chooser.c
new file mode 100644
index 0000000..686d8c6
--- /dev/null
+++ b/panels/sound/cc-alert-chooser.c
@@ -0,0 +1,306 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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 <glib/gi18n.h>
+#include <gsound.h>
+
+#include "config.h"
+#include "cc-alert-chooser.h"
+#include "cc-sound-resources.h"
+
+#define KEY_SOUNDS_SCHEMA "org.gnome.desktop.sound"
+
+struct _CcAlertChooser
+{
+ GtkBox parent_instance;
+
+ GtkToggleButton *click_button;
+ GtkToggleButton *hum_button;
+ GtkToggleButton *string_button;
+ GtkToggleButton *swing_button;
+
+ GSoundContext *context;
+ GSettings *sound_settings;
+};
+
+static void clicked_cb (CcAlertChooser *self,
+ GtkToggleButton *button);
+
+G_DEFINE_TYPE (CcAlertChooser, cc_alert_chooser, GTK_TYPE_BOX)
+
+#define CUSTOM_THEME_NAME "__custom"
+
+static gchar *
+get_theme_dir (void)
+{
+ return g_build_filename (g_get_user_data_dir (), "sounds", CUSTOM_THEME_NAME, NULL);
+}
+
+static gchar *
+get_sound_path (const gchar *name)
+{
+ g_autofree gchar *filename = NULL;
+
+ filename = g_strdup_printf ("%s.ogg", name);
+ return g_build_filename (SOUND_DATA_DIR, "gnome", "default", "alerts", filename, NULL);
+}
+
+static gchar *
+get_alert_name (void)
+{
+ g_autofree gchar *dir = NULL;
+ g_autofree gchar *path = NULL;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GFileInfo) info = NULL;
+ const gchar *target;
+ g_autofree gchar *basename = NULL;
+ g_autoptr(GError) error = NULL;
+
+ dir = get_theme_dir ();
+ path = g_build_filename (dir, "bell-terminal.ogg", NULL);
+ file = g_file_new_for_path (path);
+
+ info = g_file_query_info (file,
+ G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET,
+ G_FILE_QUERY_INFO_NONE,
+ NULL,
+ &error);
+ if (info == NULL)
+ {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ g_warning ("Failed to get sound theme symlink %s: %s", path, error->message);
+ return NULL;
+ }
+ target = g_file_info_get_attribute_byte_string (info, G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET);
+ if (target == NULL)
+ return NULL;
+
+ basename = g_path_get_basename (target);
+ if (g_str_has_suffix (basename, ".ogg"))
+ basename[strlen (basename) - 4] = '\0';
+
+ return g_steal_pointer (&basename);
+}
+
+static void
+set_sound_symlink (const gchar *alert_name,
+ const gchar *name)
+{
+ g_autofree gchar *dir = NULL;
+ g_autofree gchar *source_filename = NULL;
+ g_autofree gchar *source_path = NULL;
+ g_autofree gchar *target_path = NULL;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GError) error = NULL;
+
+ dir = get_theme_dir ();
+ source_filename = g_strdup_printf ("%s.ogg", alert_name);
+ source_path = g_build_filename (dir, source_filename, NULL);
+ target_path = get_sound_path (name);
+
+ file = g_file_new_for_path (source_path);
+ if (!g_file_delete (file, NULL, &error))
+ {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ g_warning ("Failed to remove existing sound symbolic link %s: %s", source_path, error->message);
+ }
+ if (!g_file_make_symbolic_link (file, target_path, NULL, &error))
+ g_warning ("Failed to make sound theme symbolic link %s->%s: %s", source_path, target_path, error->message);
+}
+
+static void
+set_custom_theme (CcAlertChooser *self,
+ const gchar *name)
+{
+ g_autofree gchar *dir_path = NULL;
+ g_autofree gchar *theme_path = NULL;
+ g_autoptr(GDateTime) now = NULL;
+ g_autoptr(GFile) dir = NULL;
+ g_autoptr(GKeyFile) theme_file = NULL;
+ g_autoptr(GVariant) default_theme = NULL;
+ g_autoptr(GError) load_error = NULL;
+ g_autoptr(GError) save_error = NULL;
+ g_autoptr(GError) mtime_error = NULL;
+
+ dir_path = get_theme_dir ();
+ g_mkdir_with_parents (dir_path, USER_DIR_MODE);
+
+ theme_path = g_build_filename (dir_path, "index.theme", NULL);
+
+ default_theme = g_settings_get_default_value (self->sound_settings, "theme-name");
+
+ theme_file = g_key_file_new ();
+ if (!g_key_file_load_from_file (theme_file, theme_path, G_KEY_FILE_KEEP_COMMENTS, &load_error))
+ {
+ if (!g_error_matches (load_error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+ g_printerr ("Failed to load theme file %s: %s", theme_path, load_error->message);
+ }
+ g_key_file_set_string (theme_file, "Sound Theme", "Name", _("Custom"));
+ if (default_theme != NULL)
+ g_key_file_set_string (theme_file, "Sound Theme", "Inherits", g_variant_get_string (default_theme, NULL));
+ g_key_file_set_string (theme_file, "Sound Theme", "Directories", ".");
+
+ if (!g_key_file_save_to_file (theme_file, theme_path, &save_error))
+ {
+ g_warning ("Failed to save theme file %s: %s", theme_path, save_error->message);
+ }
+
+ set_sound_symlink ("bell-terminal", name);
+ set_sound_symlink ("bell-window-system", name);
+
+ /* Ensure the g-s-d sound plugin which does non-recursive monitoring
+ * notices the change even if the theme directory already existed.
+ */
+ now = g_date_time_new_now_utc ();
+ dir = g_file_new_for_path (dir_path);
+ if (!g_file_set_attribute_uint64 (dir,
+ G_FILE_ATTRIBUTE_TIME_MODIFIED,
+ g_date_time_to_unix (now),
+ G_FILE_QUERY_INFO_NONE,
+ NULL,
+ &mtime_error))
+ {
+ g_warning ("Failed to update theme directory modification time for %s: %s",
+ dir_path, mtime_error->message);
+ }
+
+ g_settings_set_boolean (self->sound_settings, "event-sounds", TRUE);
+ g_settings_set_string (self->sound_settings, "theme-name", CUSTOM_THEME_NAME);
+}
+
+static void
+select_sound (CcAlertChooser *self,
+ const gchar *name)
+{
+ g_autofree gchar *path = NULL;
+ g_autoptr(GError) error = NULL;
+
+ path = get_sound_path (name);
+ if (!gsound_context_play_simple (self->context, NULL, &error,
+ GSOUND_ATTR_MEDIA_FILENAME, path,
+ NULL))
+ {
+ g_warning ("Failed to play alert sound %s: %s", path, error->message);
+ }
+
+ set_custom_theme (self, name);
+}
+
+static void
+set_button (CcAlertChooser *self,
+ GtkToggleButton *button,
+ gboolean active)
+{
+ g_signal_handlers_block_by_func (button, clicked_cb, self);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), active);
+ g_signal_handlers_unblock_by_func (button, clicked_cb, self);
+}
+
+static void
+clicked_cb (CcAlertChooser *self,
+ GtkToggleButton *button)
+{
+ if (button == self->click_button)
+ select_sound (self, "click");
+ else if (button == self->hum_button)
+ select_sound (self, "hum");
+ else if (button == self->string_button)
+ select_sound (self, "string");
+ else if (button == self->swing_button)
+ select_sound (self, "swing");
+
+ set_button (self, button, TRUE);
+ if (button != self->click_button)
+ set_button (self, self->click_button, FALSE);
+ if (button != self->hum_button)
+ set_button (self, self->hum_button, FALSE);
+ if (button != self->string_button)
+ set_button (self, self->string_button, FALSE);
+ if (button != self->swing_button)
+ set_button (self, self->swing_button, FALSE);
+}
+
+static void
+cc_alert_chooser_dispose (GObject *object)
+{
+ CcAlertChooser *self = CC_ALERT_CHOOSER (object);
+
+ g_clear_object (&self->context);
+ g_clear_object (&self->sound_settings);
+
+ G_OBJECT_CLASS (cc_alert_chooser_parent_class)->dispose (object);
+}
+
+void
+cc_alert_chooser_class_init (CcAlertChooserClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_alert_chooser_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/sound/cc-alert-chooser.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcAlertChooser, click_button);
+ gtk_widget_class_bind_template_child (widget_class, CcAlertChooser, hum_button);
+ gtk_widget_class_bind_template_child (widget_class, CcAlertChooser, string_button);
+ gtk_widget_class_bind_template_child (widget_class, CcAlertChooser, swing_button);
+
+ gtk_widget_class_bind_template_callback (widget_class, clicked_cb);
+}
+
+void
+cc_alert_chooser_init (CcAlertChooser *self)
+{
+ g_autofree gchar *alert_name = NULL;
+ g_autoptr(GError) error = NULL;
+
+ g_resources_register (cc_sound_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->context = gsound_context_new (NULL, &error);
+ if (self->context == NULL)
+ g_error ("Failed to make sound context: %s", error->message);
+
+ self->sound_settings = g_settings_new (KEY_SOUNDS_SCHEMA);
+
+ alert_name = get_alert_name ();
+
+ /* If user has selected an old sound alert, migrate them to click. */
+ if (g_strcmp0 (alert_name, "click") != 0 &&
+ g_strcmp0 (alert_name, "hum") != 0 &&
+ g_strcmp0 (alert_name, "string") != 0 &&
+ g_strcmp0 (alert_name, "swing") != 0)
+ {
+ set_custom_theme (self, "click");
+ g_free (alert_name);
+ alert_name = g_strdup ("click");
+ }
+
+ if (g_strcmp0 (alert_name, "click") == 0)
+ set_button (self, self->click_button, TRUE);
+ else if (g_strcmp0 (alert_name, "hum") == 0)
+ set_button (self, self->hum_button, TRUE);
+ else if (g_strcmp0 (alert_name, "string") == 0)
+ set_button (self, self->string_button, TRUE);
+ else if (g_strcmp0 (alert_name, "swing") == 0)
+ set_button (self, self->swing_button, TRUE);
+ else if (alert_name != NULL)
+ g_warning ("Current alert sound has unknown name %s", alert_name);
+}
diff --git a/panels/sound/cc-alert-chooser.h b/panels/sound/cc-alert-chooser.h
new file mode 100644
index 0000000..c6f4b87
--- /dev/null
+++ b/panels/sound/cc-alert-chooser.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_ALERT_CHOOSER (cc_alert_chooser_get_type ())
+G_DECLARE_FINAL_TYPE (CcAlertChooser, cc_alert_chooser, CC, ALERT_CHOOSER, GtkBox)
+
+G_END_DECLS
diff --git a/panels/sound/cc-alert-chooser.ui b/panels/sound/cc-alert-chooser.ui
new file mode 100644
index 0000000..603aff9
--- /dev/null
+++ b/panels/sound/cc-alert-chooser.ui
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcAlertChooser" parent="GtkBox">
+ <property name="homogeneous">True</property>
+ <style>
+ <class name="linked"/>
+ </style>
+ <child>
+ <object class="GtkToggleButton" id="click_button">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Click</property>
+ <signal name="clicked" handler="clicked_cb" object="CcAlertChooser" swapped="yes"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="string_button">
+ <property name="visible">True</property>
+ <property name="group">click_button</property>
+ <property name="label" translatable="yes">String</property>
+ <signal name="clicked" handler="clicked_cb" object="CcAlertChooser" swapped="yes"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="swing_button">
+ <property name="visible">True</property>
+ <property name="group">click_button</property>
+ <property name="label" translatable="yes">Swing</property>
+ <signal name="clicked" handler="clicked_cb" object="CcAlertChooser" swapped="yes"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="hum_button">
+ <property name="visible">True</property>
+ <property name="group">click_button</property>
+ <property name="label" translatable="yes">Hum</property>
+ <signal name="clicked" handler="clicked_cb" object="CcAlertChooser" swapped="yes"/>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/panels/sound/cc-balance-slider.c b/panels/sound/cc-balance-slider.c
new file mode 100644
index 0000000..7cb8a04
--- /dev/null
+++ b/panels/sound/cc-balance-slider.c
@@ -0,0 +1,119 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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 "cc-sound-resources.h"
+#include "cc-balance-slider.h"
+#include "gvc-channel-map-private.h"
+
+struct _CcBalanceSlider
+{
+ GtkBox parent_instance;
+
+ GtkAdjustment *adjustment;
+
+ GvcChannelMap *channel_map;
+ guint volume_changed_handler_id;
+};
+
+G_DEFINE_TYPE (CcBalanceSlider, cc_balance_slider, GTK_TYPE_BOX)
+
+static void
+changed_cb (CcBalanceSlider *self)
+{
+ gdouble value;
+ const pa_channel_map *pa_map;
+ pa_cvolume pa_volume;
+
+ if (self->channel_map == NULL)
+ return;
+
+ value = gtk_adjustment_get_value (self->adjustment);
+ pa_map = gvc_channel_map_get_pa_channel_map (self->channel_map);
+ pa_volume = *gvc_channel_map_get_cvolume (self->channel_map);
+ pa_cvolume_set_balance (&pa_volume, pa_map, value);
+ gvc_channel_map_volume_changed (self->channel_map, &pa_volume, TRUE);
+}
+
+static void
+volume_changed_cb (CcBalanceSlider *self)
+{
+ const gdouble *volumes;
+
+ volumes = gvc_channel_map_get_volume (self->channel_map);
+ g_signal_handlers_block_by_func (self->adjustment, volume_changed_cb, self);
+ gtk_adjustment_set_value (self->adjustment, volumes[BALANCE]);
+ g_signal_handlers_unblock_by_func (self->adjustment, volume_changed_cb, self);
+}
+
+static void
+cc_balance_slider_dispose (GObject *object)
+{
+ CcBalanceSlider *self = CC_BALANCE_SLIDER (object);
+
+ g_clear_object (&self->channel_map);
+
+ G_OBJECT_CLASS (cc_balance_slider_parent_class)->dispose (object);
+}
+
+void
+cc_balance_slider_class_init (CcBalanceSliderClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_balance_slider_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/sound/cc-balance-slider.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcBalanceSlider, adjustment);
+
+ gtk_widget_class_bind_template_callback (widget_class, changed_cb);
+}
+
+void
+cc_balance_slider_init (CcBalanceSlider *self)
+{
+ g_resources_register (cc_sound_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+void
+cc_balance_slider_set_channel_map (CcBalanceSlider *self,
+ GvcChannelMap *channel_map)
+{
+ g_return_if_fail (CC_IS_BALANCE_SLIDER (self));
+
+ if (self->channel_map != NULL)
+ {
+ g_signal_handler_disconnect (self->channel_map, self->volume_changed_handler_id);
+ self->volume_changed_handler_id = 0;
+ }
+ g_clear_object (&self->channel_map);
+
+ if (channel_map != NULL)
+ {
+ self->channel_map = g_object_ref (channel_map);
+
+ self->volume_changed_handler_id = g_signal_connect_object (channel_map,
+ "volume-changed",
+ G_CALLBACK (volume_changed_cb),
+ self, G_CONNECT_SWAPPED);
+ volume_changed_cb (self);
+ }
+}
diff --git a/panels/sound/cc-balance-slider.h b/panels/sound/cc-balance-slider.h
new file mode 100644
index 0000000..6a968a7
--- /dev/null
+++ b/panels/sound/cc-balance-slider.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <pulse/pulseaudio.h>
+#include <gvc-channel-map.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_BALANCE_SLIDER (cc_balance_slider_get_type ())
+G_DECLARE_FINAL_TYPE (CcBalanceSlider, cc_balance_slider, CC, BALANCE_SLIDER, GtkBox)
+
+void cc_balance_slider_set_channel_map (CcBalanceSlider *slider,
+ GvcChannelMap *channel_map);
+
+G_END_DECLS
diff --git a/panels/sound/cc-balance-slider.ui b/panels/sound/cc-balance-slider.ui
new file mode 100644
index 0000000..871a915
--- /dev/null
+++ b/panels/sound/cc-balance-slider.ui
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcBalanceSlider" parent="GtkBox">
+ <child>
+ <object class="GtkScale">
+ <property name="visible">True</property>
+ <property name="hexpand">True</property>
+ <property name="draw_value">False</property>
+ <property name="has_origin">False</property>
+ <property name="adjustment">adjustment</property>
+ <accessibility>
+ <property name="label" translatable="yes">Balance</property>
+ </accessibility>
+ <marks>
+ <mark value="-1" translatable="yes">Left</mark>
+ <mark value="0"/>
+ <mark value="1" translatable="yes">Right</mark>
+ </marks>
+ </object>
+ </child>
+ </template>
+ <object class="GtkAdjustment" id="adjustment">
+ <property name="lower">-1.0</property>
+ <property name="upper">1.0</property>
+ <property name="step_increment">0.5</property>
+ <property name="page_increment">0.5</property>
+ <signal name="value_changed" handler="changed_cb" object="CcBalanceSlider" swapped="yes"/>
+ </object>
+</interface>
diff --git a/panels/sound/cc-device-combo-box.c b/panels/sound/cc-device-combo-box.c
new file mode 100644
index 0000000..85537ef
--- /dev/null
+++ b/panels/sound/cc-device-combo-box.c
@@ -0,0 +1,213 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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 "cc-device-combo-box.h"
+#include "cc-sound-resources.h"
+
+struct _CcDeviceComboBox
+{
+ GtkComboBox parent_instance;
+
+ GtkListStore *device_model;
+
+ GvcMixerControl *mixer_control;
+ guint added_handler_id;
+ guint removed_handler_id;
+ guint active_update_handler_id;
+ gboolean is_output;
+};
+
+G_DEFINE_TYPE (CcDeviceComboBox, cc_device_combo_box, GTK_TYPE_COMBO_BOX)
+
+static void
+device_added_cb (CcDeviceComboBox *self,
+ guint id)
+{
+ GvcMixerUIDevice *device = NULL;
+ g_autofree gchar *label = NULL;
+ g_autofree gchar *icon_name = NULL;
+ const gchar *origin;
+ GtkTreeIter iter;
+
+ if (self->is_output)
+ device = gvc_mixer_control_lookup_output_id (self->mixer_control, id);
+ else
+ device = gvc_mixer_control_lookup_input_id (self->mixer_control, id);
+ if (device == NULL)
+ return;
+
+ origin = gvc_mixer_ui_device_get_origin (device);
+ if (origin && origin[0] != '\0')
+ {
+ label = g_strdup_printf ("%s - %s",
+ gvc_mixer_ui_device_get_description (device),
+ origin);
+ }
+ else
+ {
+ label = g_strdup (gvc_mixer_ui_device_get_description (device));
+ }
+
+ if (gvc_mixer_ui_device_get_icon_name (device) != NULL)
+ icon_name = g_strdup_printf ("%s-symbolic", gvc_mixer_ui_device_get_icon_name (device));
+
+ gtk_list_store_append (self->device_model, &iter);
+ gtk_list_store_set (self->device_model, &iter,
+ 0, label,
+ 1, icon_name,
+ 2, id,
+ -1);
+}
+
+static gboolean
+get_iter (CcDeviceComboBox *self,
+ guint id,
+ GtkTreeIter *iter)
+{
+ if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->device_model), iter))
+ return FALSE;
+
+ do
+ {
+ guint i;
+
+ gtk_tree_model_get (GTK_TREE_MODEL (self->device_model), iter, 2, &i, -1);
+ if (i == id)
+ return TRUE;
+ } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->device_model), iter));
+
+ return FALSE;
+}
+
+static void
+device_removed_cb (CcDeviceComboBox *self,
+ guint id)
+{
+ GtkTreeIter iter;
+
+ if (get_iter (self, id, &iter))
+ gtk_list_store_remove (self->device_model, &iter);
+}
+
+static void
+active_device_update_cb (CcDeviceComboBox *self,
+ guint id)
+{
+ GtkTreeIter iter;
+
+ if (get_iter (self, id, &iter))
+ gtk_combo_box_set_active_iter (GTK_COMBO_BOX (self), &iter);
+}
+
+static void
+cc_device_combo_box_dispose (GObject *object)
+{
+ CcDeviceComboBox *self = CC_DEVICE_COMBO_BOX (object);
+
+ g_clear_object (&self->mixer_control);
+
+ G_OBJECT_CLASS (cc_device_combo_box_parent_class)->dispose (object);
+}
+
+void
+cc_device_combo_box_class_init (CcDeviceComboBoxClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_device_combo_box_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/sound/cc-device-combo-box.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcDeviceComboBox, device_model);
+}
+
+void
+cc_device_combo_box_init (CcDeviceComboBox *self)
+{
+ g_resources_register (cc_sound_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+void
+cc_device_combo_box_set_mixer_control (CcDeviceComboBox *self,
+ GvcMixerControl *mixer_control,
+ gboolean is_output)
+{
+ const gchar *added_signal, *removed_signal, *active_update_signal;
+ g_return_if_fail (CC_IS_DEVICE_COMBO_BOX (self));
+
+ if (self->mixer_control != NULL)
+ {
+ g_signal_handler_disconnect (self->mixer_control, self->added_handler_id);
+ self->added_handler_id = 0;
+ g_signal_handler_disconnect (self->mixer_control, self->removed_handler_id);
+ self->removed_handler_id = 0;
+ g_signal_handler_disconnect (self->mixer_control, self->active_update_handler_id);
+ self->active_update_handler_id = 0;
+ }
+ g_clear_object (&self->mixer_control);
+
+ self->mixer_control = g_object_ref (mixer_control);
+ self->is_output = is_output;
+ if (is_output)
+ {
+ added_signal = "output-added";
+ removed_signal = "output-removed";
+ active_update_signal = "active-output-update";
+ }
+ else
+ {
+ added_signal = "input-added";
+ removed_signal = "input-removed";
+ active_update_signal = "active-input-update";
+ }
+
+ self->added_handler_id = g_signal_connect_object (self->mixer_control,
+ added_signal,
+ G_CALLBACK (device_added_cb),
+ self, G_CONNECT_SWAPPED);
+ self->removed_handler_id = g_signal_connect_object (self->mixer_control,
+ removed_signal,
+ G_CALLBACK (device_removed_cb),
+ self, G_CONNECT_SWAPPED);
+ self->active_update_handler_id = g_signal_connect_object (self->mixer_control,
+ active_update_signal,
+ G_CALLBACK (active_device_update_cb),
+ self, G_CONNECT_SWAPPED);
+}
+
+GvcMixerUIDevice *
+cc_device_combo_box_get_device (CcDeviceComboBox *self)
+{
+ GtkTreeIter iter;
+ guint id;
+
+ g_return_val_if_fail (CC_IS_DEVICE_COMBO_BOX (self), NULL);
+
+ if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX (self), &iter))
+ return NULL;
+
+ gtk_tree_model_get (GTK_TREE_MODEL (self->device_model), &iter, 2, &id, -1);
+
+ if (self->is_output)
+ return gvc_mixer_control_lookup_output_id (self->mixer_control, id);
+ else
+ return gvc_mixer_control_lookup_input_id (self->mixer_control, id);
+}
diff --git a/panels/sound/cc-device-combo-box.h b/panels/sound/cc-device-combo-box.h
new file mode 100644
index 0000000..119de59
--- /dev/null
+++ b/panels/sound/cc-device-combo-box.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <pulse/pulseaudio.h>
+#include <gvc-mixer-control.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_DEVICE_COMBO_BOX (cc_device_combo_box_get_type ())
+G_DECLARE_FINAL_TYPE (CcDeviceComboBox, cc_device_combo_box, CC, DEVICE_COMBO_BOX, GtkComboBox)
+
+void cc_device_combo_box_set_mixer_control (CcDeviceComboBox *combo_box,
+ GvcMixerControl *mixer_control,
+ gboolean is_output);
+
+GvcMixerUIDevice *cc_device_combo_box_get_device (CcDeviceComboBox *combo_box);
+
+G_END_DECLS
diff --git a/panels/sound/cc-device-combo-box.ui b/panels/sound/cc-device-combo-box.ui
new file mode 100644
index 0000000..e54f292
--- /dev/null
+++ b/panels/sound/cc-device-combo-box.ui
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcDeviceComboBox" parent="GtkComboBox">
+ <property name="model">device_model</property>
+ <child>
+ <object class="GtkCellRendererPixbuf">
+ <property name="xpad">6</property>
+ </object>
+ <attributes>
+ <attribute name="icon-name">1</attribute>
+ </attributes>
+ </child>
+ <child>
+ <object class="GtkCellRendererText">
+ <property name="ellipsize">end</property>
+ </object>
+ <attributes>
+ <attribute name="text">0</attribute>
+ </attributes>
+ </child>
+ </template>
+ <object class="GtkListStore" id="device_model">
+ <columns>
+ <!-- column-name title -->
+ <column type="gchararray"/>
+ <!-- column-name icon -->
+ <column type="gchararray"/>
+ <!-- column-name id -->
+ <column type="guint"/>
+ </columns>
+ </object>
+</interface>
diff --git a/panels/sound/cc-fade-slider.c b/panels/sound/cc-fade-slider.c
new file mode 100644
index 0000000..888f214
--- /dev/null
+++ b/panels/sound/cc-fade-slider.c
@@ -0,0 +1,119 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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 "cc-sound-resources.h"
+#include "cc-fade-slider.h"
+#include "gvc-channel-map-private.h"
+
+struct _CcFadeSlider
+{
+ GtkBox parent_instance;
+
+ GtkAdjustment *adjustment;
+
+ GvcChannelMap *channel_map;
+ guint volume_changed_handler_id;
+};
+
+G_DEFINE_TYPE (CcFadeSlider, cc_fade_slider, GTK_TYPE_BOX)
+
+static void
+changed_cb (CcFadeSlider *self)
+{
+ gdouble value;
+ const pa_channel_map *pa_map;
+ pa_cvolume pa_volume;
+
+ if (self->channel_map == NULL)
+ return;
+
+ value = gtk_adjustment_get_value (self->adjustment);
+ pa_map = gvc_channel_map_get_pa_channel_map (self->channel_map);
+ pa_volume = *gvc_channel_map_get_cvolume (self->channel_map);
+ pa_cvolume_set_fade (&pa_volume, pa_map, value);
+ gvc_channel_map_volume_changed (self->channel_map, &pa_volume, TRUE);
+}
+
+static void
+volume_changed_cb (CcFadeSlider *self)
+{
+ const gdouble *volumes;
+
+ volumes = gvc_channel_map_get_volume (self->channel_map);
+ g_signal_handlers_block_by_func (self->adjustment, volume_changed_cb, self);
+ gtk_adjustment_set_value (self->adjustment, volumes[FADE]);
+ g_signal_handlers_unblock_by_func (self->adjustment, volume_changed_cb, self);
+}
+
+static void
+cc_fade_slider_dispose (GObject *object)
+{
+ CcFadeSlider *self = CC_FADE_SLIDER (object);
+
+ g_clear_object (&self->channel_map);
+
+ G_OBJECT_CLASS (cc_fade_slider_parent_class)->dispose (object);
+}
+
+void
+cc_fade_slider_class_init (CcFadeSliderClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_fade_slider_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/sound/cc-fade-slider.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcFadeSlider, adjustment);
+
+ gtk_widget_class_bind_template_callback (widget_class, changed_cb);
+}
+
+void
+cc_fade_slider_init (CcFadeSlider *self)
+{
+ g_resources_register (cc_sound_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+void
+cc_fade_slider_set_channel_map (CcFadeSlider *self,
+ GvcChannelMap *channel_map)
+{
+ g_return_if_fail (CC_IS_FADE_SLIDER (self));
+
+ if (self->channel_map != NULL)
+ {
+ g_signal_handler_disconnect (self->channel_map, self->volume_changed_handler_id);
+ self->volume_changed_handler_id = 0;
+ }
+ g_clear_object (&self->channel_map);
+
+ if (channel_map != NULL)
+ {
+ self->channel_map = g_object_ref (channel_map);
+
+ self->volume_changed_handler_id = g_signal_connect_object (channel_map,
+ "volume-changed",
+ G_CALLBACK (volume_changed_cb),
+ self, G_CONNECT_SWAPPED);
+ volume_changed_cb (self);
+ }
+}
diff --git a/panels/sound/cc-fade-slider.h b/panels/sound/cc-fade-slider.h
new file mode 100644
index 0000000..92b8041
--- /dev/null
+++ b/panels/sound/cc-fade-slider.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <pulse/pulseaudio.h>
+#include <gvc-channel-map.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_FADE_SLIDER (cc_fade_slider_get_type ())
+G_DECLARE_FINAL_TYPE (CcFadeSlider, cc_fade_slider, CC, FADE_SLIDER, GtkBox)
+
+void cc_fade_slider_set_channel_map (CcFadeSlider *slider,
+ GvcChannelMap *channel_map);
+
+G_END_DECLS
diff --git a/panels/sound/cc-fade-slider.ui b/panels/sound/cc-fade-slider.ui
new file mode 100644
index 0000000..97984fb
--- /dev/null
+++ b/panels/sound/cc-fade-slider.ui
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcFadeSlider" parent="GtkBox">
+ <child>
+ <object class="GtkScale">
+ <property name="visible">True</property>
+ <property name="hexpand">True</property>
+ <property name="draw_value">False</property>
+ <property name="has_origin">False</property>
+ <property name="adjustment">adjustment</property>
+ <accessibility>
+ <property name="label" translatable="yes">Fade</property>
+ </accessibility>
+ <marks>
+ <mark value="-1" translatable="yes">Rear</mark>
+ <mark value="0"/>
+ <mark value="1" translatable="yes">Front</mark>
+ </marks>
+ </object>
+ </child>
+ </template>
+ <object class="GtkAdjustment" id="adjustment">
+ <property name="lower">-1.0</property>
+ <property name="upper">1.0</property>
+ <property name="step_increment">0.5</property>
+ <property name="page_increment">0.5</property>
+ <signal name="value_changed" handler="changed_cb" object="CcFadeSlider" swapped="yes"/>
+ </object>
+</interface>
diff --git a/panels/sound/cc-level-bar.c b/panels/sound/cc-level-bar.c
new file mode 100644
index 0000000..8636132
--- /dev/null
+++ b/panels/sound/cc-level-bar.c
@@ -0,0 +1,290 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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 "cc-level-bar.h"
+#include "cc-sound-enums.h"
+#include "gvc-mixer-stream-private.h"
+
+struct _CcLevelBar
+{
+ GtkWidget parent_instance;
+
+ CcStreamType type;
+ pa_stream *level_stream;
+ gdouble last_input_peak;
+
+ gdouble value;
+};
+
+G_DEFINE_TYPE (CcLevelBar, cc_level_bar, GTK_TYPE_WIDGET)
+
+#define LED_WIDTH 12
+#define LED_HEIGHT 3
+#define LED_SPACING 4
+
+#define DECAY_STEP .15
+
+static void
+set_peak (CcLevelBar *self,
+ gdouble value)
+{
+ if (value < 0)
+ value = 0;
+ if (value > 1)
+ value = 1;
+
+ if (self->last_input_peak >= DECAY_STEP &&
+ value < self->last_input_peak - DECAY_STEP)
+ value = self->last_input_peak - DECAY_STEP;
+ self->last_input_peak = value;
+
+ self->value = value;
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+read_cb (pa_stream *stream,
+ size_t length,
+ void *userdata)
+{
+ CcLevelBar *self = userdata;
+ const void *data;
+ gdouble value;
+
+ if (pa_stream_peek (stream, &data, &length) < 0)
+ {
+ g_warning ("Failed to read data from stream");
+ return;
+ }
+
+ if (!data)
+ {
+ pa_stream_drop (stream);
+ return;
+ }
+
+ assert (length > 0);
+ assert (length % sizeof (float) == 0);
+
+ value = ((const float *) data)[length / sizeof (float) -1];
+
+ pa_stream_drop (stream);
+
+ set_peak (self, value);
+}
+
+static void
+suspended_cb (pa_stream *stream,
+ void *userdata)
+{
+ CcLevelBar *self = userdata;
+
+ if (pa_stream_is_suspended (stream))
+ {
+ g_debug ("Stream suspended");
+ self->value = 0.0;
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+ }
+}
+
+static void
+cc_level_bar_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ if (orientation == GTK_ORIENTATION_VERTICAL)
+ {
+ *minimum = *natural = LED_HEIGHT;
+ }
+ else
+ {
+ GTK_WIDGET_CLASS (cc_level_bar_parent_class)->measure (widget,
+ orientation,
+ for_size,
+ minimum,
+ natural,
+ minimum_baseline,
+ natural_baseline);
+ }
+}
+
+static void
+cc_level_bar_snapshot (GtkWidget *widget,
+ GtkSnapshot *snapshot)
+{
+ CcLevelBar *self = CC_LEVEL_BAR (widget);
+ GdkRGBA inactive_color, active_color;
+ int i, n_leds;
+ double level;
+ double spacing, x_offset = 0.0;
+
+ n_leds = gtk_widget_get_width (widget) / (LED_WIDTH + LED_SPACING);
+ spacing = (double) (gtk_widget_get_width (widget) - (n_leds * LED_WIDTH)) / (n_leds - 1);
+ level = self->value * n_leds;
+
+ gdk_rgba_parse (&inactive_color, "#C0C0C0");
+ switch (self->type)
+ {
+ default:
+ case CC_STREAM_TYPE_OUTPUT:
+ gdk_rgba_parse (&active_color, "#4a90d9");
+ break;
+ case CC_STREAM_TYPE_INPUT:
+ gdk_rgba_parse (&active_color, "#ff0000");
+ break;
+ }
+
+ for (i = 0; i < n_leds; i++)
+ {
+ GdkRGBA blended_color;
+ double led_level;
+
+ led_level = level - i;
+ if (led_level < 0.0)
+ led_level = 0.0;
+ else if (led_level > 1.0)
+ led_level = 1.0;
+
+ blended_color = (GdkRGBA) {
+ .red = (1.0 - led_level) * inactive_color.red + led_level * active_color.red,
+ .green = (1.0 - led_level) * inactive_color.green + led_level * active_color.green,
+ .blue = (1.0 - led_level) * inactive_color.blue + led_level * active_color.blue,
+ .alpha = 1.0,
+ };
+
+ gtk_snapshot_append_color (snapshot,
+ &blended_color,
+ &GRAPHENE_RECT_INIT (x_offset, 0,
+ LED_WIDTH,
+ gtk_widget_get_height (widget)));
+ x_offset += LED_WIDTH + spacing;
+ }
+}
+
+static void
+close_stream (pa_stream *stream)
+{
+ if (stream == NULL)
+ return;
+
+ /* Stop receiving data */
+ pa_stream_set_read_callback (stream, NULL, NULL);
+ pa_stream_set_suspended_callback (stream, NULL, NULL);
+
+ /* Disconnect from the stream */
+ pa_stream_disconnect (stream);
+}
+
+static void
+cc_level_bar_dispose (GObject *object)
+{
+ CcLevelBar *self = CC_LEVEL_BAR (object);
+
+ close_stream (self->level_stream);
+ g_clear_pointer (&self->level_stream, pa_stream_unref);
+
+ G_OBJECT_CLASS (cc_level_bar_parent_class)->dispose (object);
+}
+
+void
+cc_level_bar_class_init (CcLevelBarClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_level_bar_dispose;
+
+ widget_class->measure = cc_level_bar_measure;
+ widget_class->snapshot = cc_level_bar_snapshot;
+}
+
+void
+cc_level_bar_init (CcLevelBar *self)
+{
+}
+
+void
+cc_level_bar_set_stream (CcLevelBar *self,
+ GvcMixerStream *stream,
+ CcStreamType type)
+{
+ pa_context *context;
+ pa_sample_spec sample_spec;
+ pa_proplist *proplist;
+ pa_buffer_attr attr;
+ g_autofree gchar *device = NULL;
+
+ g_return_if_fail (CC_IS_LEVEL_BAR (self));
+
+ close_stream (self->level_stream);
+ g_clear_pointer (&self->level_stream, pa_stream_unref);
+
+ self->type = type;
+
+ if (stream == NULL)
+ {
+ self->value = 0.0;
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+ return;
+ }
+
+ context = gvc_mixer_stream_get_pa_context (stream);
+
+ if (pa_context_get_server_protocol_version (context) < 13)
+ {
+ g_warning ("Unsupported version of PulseAudio");
+ return;
+ }
+
+ sample_spec.channels = 1;
+ sample_spec.format = PA_SAMPLE_FLOAT32;
+ sample_spec.rate = 25;
+
+ proplist = pa_proplist_new ();
+ pa_proplist_sets (proplist, PA_PROP_APPLICATION_ID, "org.gnome.VolumeControl");
+ self->level_stream = pa_stream_new_with_proplist (context, "Peak detect", &sample_spec, NULL, proplist);
+ pa_proplist_free (proplist);
+ if (self->level_stream == NULL)
+ {
+ g_warning ("Failed to create monitoring stream");
+ return;
+ }
+
+ pa_stream_set_read_callback (self->level_stream, read_cb, self);
+ pa_stream_set_suspended_callback (self->level_stream, suspended_cb, self);
+
+ memset (&attr, 0, sizeof (attr));
+ attr.fragsize = sizeof (float);
+ attr.maxlength = (uint32_t) -1;
+ device = g_strdup_printf ("%u", gvc_mixer_stream_get_index (stream));
+ if (pa_stream_connect_record (self->level_stream,
+ device,
+ &attr,
+ (pa_stream_flags_t) (PA_STREAM_DONT_MOVE |
+ PA_STREAM_PEAK_DETECT |
+ PA_STREAM_ADJUST_LATENCY)) < 0)
+ {
+ g_warning ("Failed to connect monitoring stream");
+ }
+
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+}
diff --git a/panels/sound/cc-level-bar.h b/panels/sound/cc-level-bar.h
new file mode 100644
index 0000000..34ef1ea
--- /dev/null
+++ b/panels/sound/cc-level-bar.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <pulse/pulseaudio.h>
+#include <gvc-mixer-stream.h>
+
+#include "cc-sound-enums.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_LEVEL_BAR (cc_level_bar_get_type ())
+G_DECLARE_FINAL_TYPE (CcLevelBar, cc_level_bar, CC, LEVEL_BAR, GtkWidget)
+
+void cc_level_bar_set_stream (CcLevelBar *bar,
+ GvcMixerStream *stream,
+ CcStreamType type);
+
+G_END_DECLS
diff --git a/panels/sound/cc-output-test-dialog.c b/panels/sound/cc-output-test-dialog.c
new file mode 100644
index 0000000..b8a518e
--- /dev/null
+++ b/panels/sound/cc-output-test-dialog.c
@@ -0,0 +1,160 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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 <gsound.h>
+#include <glib/gi18n.h>
+
+#include "cc-output-test-dialog.h"
+#include "cc-sound-resources.h"
+#include "cc-speaker-test-button.h"
+
+struct _CcOutputTestDialog
+{
+ GtkDialog parent_instance;
+
+ CcSpeakerTestButton *front_center_speaker_button;
+ CcSpeakerTestButton *front_left_speaker_button;
+ CcSpeakerTestButton *front_left_of_center_speaker_button;
+ CcSpeakerTestButton *front_right_of_center_speaker_button;
+ CcSpeakerTestButton *front_right_speaker_button;
+ CcSpeakerTestButton *lfe_speaker_button;
+ CcSpeakerTestButton *rear_center_speaker_button;
+ CcSpeakerTestButton *rear_left_speaker_button;
+ CcSpeakerTestButton *rear_right_speaker_button;
+ CcSpeakerTestButton *side_left_speaker_button;
+ CcSpeakerTestButton *side_right_speaker_button;
+
+ GvcMixerUIDevice *device;
+ GSoundContext *context;
+};
+
+G_DEFINE_TYPE (CcOutputTestDialog, cc_output_test_dialog, GTK_TYPE_DIALOG)
+
+static void
+cc_output_test_dialog_dispose (GObject *object)
+{
+ CcOutputTestDialog *self = CC_OUTPUT_TEST_DIALOG (object);
+
+ g_clear_object (&self->device);
+ g_clear_object (&self->context);
+
+ G_OBJECT_CLASS (cc_output_test_dialog_parent_class)->dispose (object);
+}
+
+void
+cc_output_test_dialog_class_init (CcOutputTestDialogClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_output_test_dialog_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/sound/cc-output-test-dialog.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcOutputTestDialog, front_center_speaker_button);
+ gtk_widget_class_bind_template_child (widget_class, CcOutputTestDialog, front_left_speaker_button);
+ gtk_widget_class_bind_template_child (widget_class, CcOutputTestDialog, front_left_of_center_speaker_button);
+ gtk_widget_class_bind_template_child (widget_class, CcOutputTestDialog, front_right_of_center_speaker_button);
+ gtk_widget_class_bind_template_child (widget_class, CcOutputTestDialog, front_right_speaker_button);
+ gtk_widget_class_bind_template_child (widget_class, CcOutputTestDialog, lfe_speaker_button);
+ gtk_widget_class_bind_template_child (widget_class, CcOutputTestDialog, rear_center_speaker_button);
+ gtk_widget_class_bind_template_child (widget_class, CcOutputTestDialog, rear_left_speaker_button);
+ gtk_widget_class_bind_template_child (widget_class, CcOutputTestDialog, rear_right_speaker_button);
+ gtk_widget_class_bind_template_child (widget_class, CcOutputTestDialog, side_left_speaker_button);
+ gtk_widget_class_bind_template_child (widget_class, CcOutputTestDialog, side_right_speaker_button);
+
+ g_type_ensure (CC_TYPE_SPEAKER_TEST_BUTTON);
+}
+
+void
+cc_output_test_dialog_init (CcOutputTestDialog *self)
+{
+ GtkSettings *settings;
+ g_autofree gchar *theme_name = NULL;
+
+ g_resources_register (cc_sound_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->context = gsound_context_new (NULL, NULL);
+ gsound_context_set_driver (self->context, "pulse", NULL);
+ gsound_context_set_attributes (self->context, NULL,
+ GSOUND_ATTR_APPLICATION_ID, "org.gnome.VolumeControl",
+ NULL);
+ settings = gtk_settings_get_for_display (gdk_display_get_default ());
+ g_object_get (G_OBJECT (settings),
+ "gtk-sound-theme-name", &theme_name,
+ NULL);
+ if (theme_name != NULL)
+ gsound_context_set_attributes (self->context, NULL,
+ GSOUND_ATTR_CANBERRA_XDG_THEME_NAME, theme_name,
+ NULL);
+
+ cc_speaker_test_button_set_channel_position (self->front_left_speaker_button, self->context, PA_CHANNEL_POSITION_FRONT_LEFT);
+ cc_speaker_test_button_set_channel_position (self->front_left_of_center_speaker_button, self->context, PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER);
+ cc_speaker_test_button_set_channel_position (self->front_center_speaker_button, self->context, PA_CHANNEL_POSITION_FRONT_CENTER);
+ cc_speaker_test_button_set_channel_position (self->front_right_of_center_speaker_button, self->context, PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER);
+ cc_speaker_test_button_set_channel_position (self->front_right_speaker_button, self->context, PA_CHANNEL_POSITION_FRONT_RIGHT);
+ cc_speaker_test_button_set_channel_position (self->side_left_speaker_button, self->context, PA_CHANNEL_POSITION_SIDE_LEFT);
+ cc_speaker_test_button_set_channel_position (self->side_right_speaker_button, self->context, PA_CHANNEL_POSITION_SIDE_RIGHT);
+ cc_speaker_test_button_set_channel_position (self->lfe_speaker_button, self->context, PA_CHANNEL_POSITION_LFE);
+ cc_speaker_test_button_set_channel_position (self->rear_left_speaker_button, self->context, PA_CHANNEL_POSITION_REAR_LEFT);
+ cc_speaker_test_button_set_channel_position (self->rear_center_speaker_button, self->context, PA_CHANNEL_POSITION_REAR_CENTER);
+ cc_speaker_test_button_set_channel_position (self->rear_right_speaker_button, self->context, PA_CHANNEL_POSITION_REAR_RIGHT);
+}
+
+CcOutputTestDialog *
+cc_output_test_dialog_new (GvcMixerUIDevice *device,
+ GvcMixerStream *stream)
+{
+ CcOutputTestDialog *self;
+ const GvcChannelMap *map = NULL;
+ g_autofree gchar *title = NULL;
+
+ self = g_object_new (CC_TYPE_OUTPUT_TEST_DIALOG,
+ "use-header-bar", 1,
+ NULL);
+ self->device = g_object_ref (device);
+
+ title = g_strdup_printf (_("Testing %s"), gvc_mixer_ui_device_get_description (device));
+ gtk_window_set_title (GTK_WINDOW (self), title);
+
+ map = gvc_mixer_stream_get_channel_map (stream);
+ gtk_widget_set_visible (GTK_WIDGET (self->front_left_speaker_button), gvc_channel_map_has_position (map, PA_CHANNEL_POSITION_FRONT_LEFT));
+ gtk_widget_set_visible (GTK_WIDGET (self->front_left_of_center_speaker_button), gvc_channel_map_has_position (map, PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER));
+ gtk_widget_set_visible (GTK_WIDGET (self->front_center_speaker_button), gvc_channel_map_has_position (map, PA_CHANNEL_POSITION_FRONT_CENTER));
+ gtk_widget_set_visible (GTK_WIDGET (self->front_right_of_center_speaker_button), gvc_channel_map_has_position (map, PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER));
+ gtk_widget_set_visible (GTK_WIDGET (self->front_right_speaker_button), gvc_channel_map_has_position (map, PA_CHANNEL_POSITION_FRONT_RIGHT));
+ gtk_widget_set_visible (GTK_WIDGET (self->side_left_speaker_button), gvc_channel_map_has_position (map, PA_CHANNEL_POSITION_SIDE_LEFT));
+ gtk_widget_set_visible (GTK_WIDGET (self->side_right_speaker_button), gvc_channel_map_has_position (map, PA_CHANNEL_POSITION_SIDE_RIGHT));
+ gtk_widget_set_visible (GTK_WIDGET (self->lfe_speaker_button), gvc_channel_map_has_position (map, PA_CHANNEL_POSITION_LFE));
+ gtk_widget_set_visible (GTK_WIDGET (self->rear_left_speaker_button), gvc_channel_map_has_position (map, PA_CHANNEL_POSITION_REAR_LEFT));
+ gtk_widget_set_visible (GTK_WIDGET (self->rear_center_speaker_button), gvc_channel_map_has_position (map, PA_CHANNEL_POSITION_REAR_CENTER));
+ gtk_widget_set_visible (GTK_WIDGET (self->rear_right_speaker_button), gvc_channel_map_has_position (map, PA_CHANNEL_POSITION_REAR_RIGHT));
+
+ /* Replace the center channel with a mono channel */
+ if (gvc_channel_map_has_position (map, PA_CHANNEL_POSITION_MONO))
+ {
+ if (gvc_channel_map_has_position (map, PA_CHANNEL_POSITION_FRONT_CENTER))
+ g_warning ("Testing output with both front center and mono channels - front center is hidden");
+ cc_speaker_test_button_set_channel_position (self->front_center_speaker_button, self->context, PA_CHANNEL_POSITION_MONO);
+ gtk_widget_set_visible (GTK_WIDGET (self->front_center_speaker_button), TRUE);
+ }
+
+ return self;
+}
diff --git a/panels/sound/cc-output-test-dialog.h b/panels/sound/cc-output-test-dialog.h
new file mode 100644
index 0000000..ece6f82
--- /dev/null
+++ b/panels/sound/cc-output-test-dialog.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <gvc-mixer-ui-device.h>
+#include <pulse/pulseaudio.h>
+#include <gvc-mixer-stream.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_OUTPUT_TEST_DIALOG (cc_output_test_dialog_get_type ())
+G_DECLARE_FINAL_TYPE (CcOutputTestDialog, cc_output_test_dialog, CC, OUTPUT_TEST_DIALOG, GtkDialog)
+
+CcOutputTestDialog *cc_output_test_dialog_new (GvcMixerUIDevice *device,
+ GvcMixerStream *stream);
+
+G_END_DECLS
diff --git a/panels/sound/cc-output-test-dialog.ui b/panels/sound/cc-output-test-dialog.ui
new file mode 100644
index 0000000..7ec5b7b
--- /dev/null
+++ b/panels/sound/cc-output-test-dialog.ui
@@ -0,0 +1,163 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcOutputTestDialog" parent="GtkDialog">
+ <property name="modal">True</property>
+ <property name="resizable">False</property>
+ <child>
+ <object class="GtkBox">
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="row_spacing">30</property>
+ <property name="column_spacing">30</property>
+ <property name="margin-top">30</property>
+ <property name="margin-bottom">30</property>
+ <property name="margin-start">30</property>
+ <property name="margin-end">30</property>
+ <child>
+ <object class="CcSpeakerTestButton" id="front_left_speaker_button">
+ <property name="visible">True</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">0</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="CcSpeakerTestButton" id="front_left_of_center_speaker_button">
+ <property name="visible">True</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">0</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="CcSpeakerTestButton" id="front_center_speaker_button">
+ <property name="visible">True</property>
+ <layout>
+ <property name="column">2</property>
+ <property name="row">0</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="CcSpeakerTestButton" id="front_right_of_center_speaker_button">
+ <property name="visible">True</property>
+ <layout>
+ <property name="column">3</property>
+ <property name="row">0</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="CcSpeakerTestButton" id="front_right_speaker_button">
+ <property name="visible">True</property>
+ <layout>
+ <property name="column">4</property>
+ <property name="row">0</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="CcSpeakerTestButton" id="side_left_speaker_button">
+ <property name="visible">True</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">1</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon_name">avatar-default-symbolic</property>
+ <property name="pixel_size">64</property>
+ <layout>
+ <property name="column">2</property>
+ <property name="row">1</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="CcSpeakerTestButton" id="side_right_speaker_button">
+ <property name="visible">True</property>
+ <layout>
+ <property name="column">4</property>
+ <property name="row">1</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="CcSpeakerTestButton" id="rear_left_speaker_button">
+ <property name="visible">True</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="CcSpeakerTestButton" id="rear_center_speaker_button">
+ <property name="visible">True</property>
+ <layout>
+ <property name="column">2</property>
+ <property name="row">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="CcSpeakerTestButton" id="lfe_speaker_button">
+ <property name="visible">True</property>
+ <layout>
+ <property name="column">3</property>
+ <property name="row">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="CcSpeakerTestButton" id="rear_right_speaker_button">
+ <property name="visible">True</property>
+ <layout>
+ <property name="column">4</property>
+ <property name="row">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Click a speaker to test</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">3</property>
+ <property name="column-span">5</property>
+ </layout>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtkSizeGroup">
+ <property name="mode">both</property>
+ <widgets>
+ <widget name="front_left_speaker_button"/>
+ <widget name="front_left_of_center_speaker_button"/>
+ <widget name="front_center_speaker_button"/>
+ <widget name="front_right_of_center_speaker_button"/>
+ <widget name="front_right_speaker_button"/>
+ <widget name="side_left_speaker_button"/>
+ <widget name="side_right_speaker_button"/>
+ <widget name="rear_left_speaker_button"/>
+ <widget name="rear_center_speaker_button"/>
+ <widget name="lfe_speaker_button"/>
+ <widget name="rear_right_speaker_button"/>
+ </widgets>
+ </object>
+</interface>
diff --git a/panels/sound/cc-profile-combo-box.c b/panels/sound/cc-profile-combo-box.c
new file mode 100644
index 0000000..a328b98
--- /dev/null
+++ b/panels/sound/cc-profile-combo-box.c
@@ -0,0 +1,135 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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 "cc-profile-combo-box.h"
+#include "cc-sound-resources.h"
+
+struct _CcProfileComboBox
+{
+ GtkComboBox parent_instance;
+
+ GtkListStore *profile_model;
+
+ GvcMixerControl *mixer_control;
+ GvcMixerUIDevice *device;
+};
+
+G_DEFINE_TYPE (CcProfileComboBox, cc_profile_combo_box, GTK_TYPE_COMBO_BOX)
+
+static void
+profile_changed_cb (CcProfileComboBox *self)
+{
+ GtkTreeIter iter;
+ g_autofree gchar *profile = NULL;
+
+ if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX (self), &iter))
+ return;
+
+ gtk_tree_model_get (GTK_TREE_MODEL (self->profile_model), &iter,
+ 1, &profile,
+ -1);
+
+ if (!gvc_mixer_control_change_profile_on_selected_device (self->mixer_control,
+ self->device,
+ profile))
+ {
+ g_warning ("Failed to change profile on %s", gvc_mixer_ui_device_get_description (self->device));
+ }
+}
+
+static void
+cc_profile_combo_box_dispose (GObject *object)
+{
+ CcProfileComboBox *self = CC_PROFILE_COMBO_BOX (object);
+
+ g_clear_object (&self->device);
+
+ G_OBJECT_CLASS (cc_profile_combo_box_parent_class)->dispose (object);
+}
+
+void
+cc_profile_combo_box_class_init (CcProfileComboBoxClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_profile_combo_box_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/sound/cc-profile-combo-box.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcProfileComboBox, profile_model);
+
+ gtk_widget_class_bind_template_callback (widget_class, profile_changed_cb);
+}
+
+void
+cc_profile_combo_box_init (CcProfileComboBox *self)
+{
+ g_resources_register (cc_sound_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+void
+cc_profile_combo_box_set_device (CcProfileComboBox *self,
+ GvcMixerControl *mixer_control,
+ GvcMixerUIDevice *device)
+{
+ GList *profiles, *link;
+
+ g_return_if_fail (CC_IS_PROFILE_COMBO_BOX (self));
+
+ if (device == self->device)
+ return;
+
+ g_clear_object (&self->mixer_control);
+ self->mixer_control = g_object_ref (mixer_control);
+ g_clear_object (&self->device);
+ gtk_list_store_clear (self->profile_model);
+
+ if (device == NULL)
+ return;
+
+ self->device = g_object_ref (device);
+ profiles = gvc_mixer_ui_device_get_profiles (device);
+ for (link = profiles; link; link = link->next)
+ {
+ GvcMixerCardProfile *profile = link->data;
+ GtkTreeIter iter;
+
+ gtk_list_store_append (self->profile_model, &iter);
+ gtk_list_store_set (self->profile_model, &iter,
+ 0, profile->human_profile,
+ 1, profile->profile,
+ -1);
+
+ if (g_strcmp0 (gvc_mixer_ui_device_get_active_profile (device), profile->profile) == 0)
+ {
+ g_signal_handlers_block_by_func(self, profile_changed_cb, self);
+ gtk_combo_box_set_active_iter (GTK_COMBO_BOX (self), &iter);
+ g_signal_handlers_unblock_by_func(self, profile_changed_cb, self);
+ }
+ }
+}
+
+gint
+cc_profile_combo_box_get_profile_count (CcProfileComboBox *self)
+{
+ g_return_val_if_fail (CC_IS_PROFILE_COMBO_BOX (self), 0);
+ return gtk_tree_model_iter_n_children (GTK_TREE_MODEL (self->profile_model), NULL);
+}
diff --git a/panels/sound/cc-profile-combo-box.h b/panels/sound/cc-profile-combo-box.h
new file mode 100644
index 0000000..d76becb
--- /dev/null
+++ b/panels/sound/cc-profile-combo-box.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <pulse/pulseaudio.h>
+#include <gvc-mixer-control.h>
+#include <gvc-mixer-ui-device.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_PROFILE_COMBO_BOX (cc_profile_combo_box_get_type ())
+G_DECLARE_FINAL_TYPE (CcProfileComboBox, cc_profile_combo_box, CC, PROFILE_COMBO_BOX, GtkComboBox)
+
+void cc_profile_combo_box_set_device (CcProfileComboBox *combo_box,
+ GvcMixerControl *mixer_control,
+ GvcMixerUIDevice *device);
+
+gint cc_profile_combo_box_get_profile_count (CcProfileComboBox *combo_box);
+
+G_END_DECLS
diff --git a/panels/sound/cc-profile-combo-box.ui b/panels/sound/cc-profile-combo-box.ui
new file mode 100644
index 0000000..58f2d9b
--- /dev/null
+++ b/panels/sound/cc-profile-combo-box.ui
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcProfileComboBox" parent="GtkComboBox">
+ <property name="model">profile_model</property>
+ <signal name="changed" handler="profile_changed_cb" object="CcProfileComboBox" swapped="yes"/>
+ <child>
+ <object class="GtkCellRendererText"/>
+ <attributes>
+ <attribute name="text">0</attribute>
+ </attributes>
+ </child>
+ </template>
+ <object class="GtkListStore" id="profile_model">
+ <columns>
+ <!-- column-name title -->
+ <column type="gchararray"/>
+ <!-- column-name profile -->
+ <column type="gchararray"/>
+ </columns>
+ </object>
+</interface>
diff --git a/panels/sound/cc-sound-enums.h b/panels/sound/cc-sound-enums.h
new file mode 100644
index 0000000..bf29d27
--- /dev/null
+++ b/panels/sound/cc-sound-enums.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 Jordan Petridis <jpetridis@gnome.org>
+ *
+ * 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
+ * Lesser 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/>.
+ */
+
+#pragma once
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+ CC_STREAM_TYPE_OUTPUT,
+ CC_STREAM_TYPE_INPUT,
+} CcStreamType;
+
+G_END_DECLS
diff --git a/panels/sound/cc-sound-panel.c b/panels/sound/cc-sound-panel.c
new file mode 100644
index 0000000..0de1ca9
--- /dev/null
+++ b/panels/sound/cc-sound-panel.c
@@ -0,0 +1,318 @@
+/* -*- 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
+ * Lesser 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 "config.h"
+
+#include <libintl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include <glib/gi18n-lib.h>
+#include <glib.h>
+#include <gtk/gtk.h>
+#include <pulse/pulseaudio.h>
+#include <gvc-mixer-control.h>
+
+#include "cc-alert-chooser.h"
+#include "cc-balance-slider.h"
+#include "cc-device-combo-box.h"
+#include "cc-fade-slider.h"
+#include "cc-level-bar.h"
+#include "cc-output-test-dialog.h"
+#include "cc-profile-combo-box.h"
+#include "cc-sound-panel.h"
+#include "cc-sound-resources.h"
+#include "cc-stream-list-box.h"
+#include "cc-subwoofer-slider.h"
+#include "cc-volume-slider.h"
+
+struct _CcSoundPanel
+{
+ CcPanel parent_instance;
+
+ CcBalanceSlider *balance_slider;
+ GtkListBoxRow *fade_row;
+ CcFadeSlider *fade_slider;
+ CcDeviceComboBox *input_device_combo_box;
+ CcLevelBar *input_level_bar;
+ GtkListBox *input_list_box;
+ CcProfileComboBox *input_profile_combo_box;
+ GtkListBoxRow *input_profile_row;
+ CcVolumeSlider *input_volume_slider;
+ GtkSizeGroup *label_size_group;
+ CcDeviceComboBox *output_device_combo_box;
+ GtkListStore *output_device_model;
+ CcLevelBar *output_level_bar;
+ GtkListBox *output_list_box;
+ CcProfileComboBox *output_profile_combo_box;
+ GtkListBoxRow *output_profile_row;
+ CcVolumeSlider *output_volume_slider;
+ CcStreamListBox *stream_list_box;
+ GtkListBoxRow *subwoofer_row;
+ CcSubwooferSlider *subwoofer_slider;
+
+ GvcMixerControl *mixer_control;
+ GSettings *sound_settings;
+};
+
+CC_PANEL_REGISTER (CcSoundPanel, cc_sound_panel)
+
+enum
+{
+ PROP_0,
+ PROP_PARAMETERS
+};
+
+#define KEY_SOUNDS_SCHEMA "org.gnome.desktop.sound"
+
+static void
+allow_amplified_changed_cb (CcSoundPanel *self)
+{
+ cc_volume_slider_set_is_amplified (self->output_volume_slider,
+ g_settings_get_boolean (self->sound_settings, "allow-volume-above-100-percent"));
+}
+
+static void
+set_output_stream (CcSoundPanel *self,
+ GvcMixerStream *stream)
+{
+ GvcChannelMap *map = NULL;
+ gboolean can_fade = FALSE, has_lfe = FALSE;
+
+ cc_volume_slider_set_stream (self->output_volume_slider, stream, CC_STREAM_TYPE_OUTPUT);
+ cc_level_bar_set_stream (self->output_level_bar, stream, CC_STREAM_TYPE_OUTPUT);
+
+ if (stream != NULL)
+ {
+ map = (GvcChannelMap *) gvc_mixer_stream_get_channel_map (stream);
+ can_fade = gvc_channel_map_can_fade (map);
+ has_lfe = gvc_channel_map_has_lfe (map);
+ }
+ cc_fade_slider_set_channel_map (self->fade_slider, map);
+ cc_balance_slider_set_channel_map (self->balance_slider, map);
+ cc_subwoofer_slider_set_channel_map (self->subwoofer_slider, map);
+
+ gtk_widget_set_visible (GTK_WIDGET (self->fade_row), can_fade);
+ gtk_widget_set_visible (GTK_WIDGET (self->subwoofer_row), has_lfe);
+}
+
+static void
+output_device_changed_cb (CcSoundPanel *self)
+{
+ GvcMixerUIDevice *device;
+ GvcMixerStream *stream = NULL;
+
+ device = cc_device_combo_box_get_device (self->output_device_combo_box);
+
+ if (device != NULL)
+ stream = gvc_mixer_control_get_stream_from_device (self->mixer_control, device);
+
+ set_output_stream (self, stream);
+
+ if (device != NULL)
+ gvc_mixer_control_change_output (self->mixer_control, device);
+}
+
+static void
+set_input_stream (CcSoundPanel *self,
+ GvcMixerStream *stream)
+{
+ cc_volume_slider_set_stream (self->input_volume_slider, stream, CC_STREAM_TYPE_INPUT);
+ cc_level_bar_set_stream (self->input_level_bar, stream, CC_STREAM_TYPE_INPUT);
+}
+
+static void
+input_device_changed_cb (CcSoundPanel *self)
+{
+ GvcMixerUIDevice *device;
+ GvcMixerStream *stream = NULL;
+
+ device = cc_device_combo_box_get_device (self->input_device_combo_box);
+
+ if (device != NULL)
+ stream = gvc_mixer_control_get_stream_from_device (self->mixer_control, device);
+
+ set_input_stream (self, stream);
+
+ if (device != NULL)
+ gvc_mixer_control_change_input (self->mixer_control, device);
+}
+
+static void
+output_device_update_cb (CcSoundPanel *self,
+ guint id)
+{
+ GvcMixerUIDevice *device;
+ gboolean has_multi_profiles;
+ GvcMixerStream *stream = NULL;
+
+ device = cc_device_combo_box_get_device (self->output_device_combo_box);
+ cc_profile_combo_box_set_device (self->output_profile_combo_box, self->mixer_control, device);
+ has_multi_profiles = (cc_profile_combo_box_get_profile_count (self->output_profile_combo_box) > 1);
+ gtk_widget_set_visible (GTK_WIDGET (self->output_profile_row),
+ has_multi_profiles);
+
+ if (cc_volume_slider_get_stream (self->output_volume_slider) == NULL)
+ stream = gvc_mixer_control_get_stream_from_device (self->mixer_control, device);
+ if (stream != NULL)
+ set_output_stream (self, stream);
+}
+
+static void
+input_device_update_cb (CcSoundPanel *self,
+ guint id)
+{
+ GvcMixerUIDevice *device;
+ gboolean has_multi_profiles;
+ GvcMixerStream *stream = NULL;
+
+ device = cc_device_combo_box_get_device (self->input_device_combo_box);
+ cc_profile_combo_box_set_device (self->input_profile_combo_box, self->mixer_control, device);
+ has_multi_profiles = (cc_profile_combo_box_get_profile_count (self->input_profile_combo_box) > 1);
+ gtk_widget_set_visible (GTK_WIDGET (self->input_profile_row),
+ has_multi_profiles);
+
+ if (cc_volume_slider_get_stream (self->input_volume_slider) == NULL)
+ stream = gvc_mixer_control_get_stream_from_device (self->mixer_control, device);
+ if (stream != NULL)
+ set_input_stream (self, stream);
+}
+
+static void
+test_output_configuration_button_clicked_cb (CcSoundPanel *self)
+{
+ GvcMixerUIDevice *device;
+ GvcMixerStream *stream = NULL;
+ CcOutputTestDialog *dialog;
+ GtkWidget *toplevel;
+ CcShell *shell;
+
+ device = cc_device_combo_box_get_device (self->output_device_combo_box);
+ if (device != NULL)
+ stream = gvc_mixer_control_get_stream_from_device (self->mixer_control, device);
+
+ shell = cc_panel_get_shell (CC_PANEL (self));
+ toplevel = cc_shell_get_toplevel (shell);
+
+ dialog = cc_output_test_dialog_new (device, stream);
+ gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (toplevel));
+ gtk_window_present (GTK_WINDOW (dialog));
+}
+
+static const char *
+cc_sound_panel_get_help_uri (CcPanel *panel)
+{
+ return "help:gnome-help/media#sound";
+}
+
+static void
+cc_sound_panel_finalize (GObject *object)
+{
+ CcSoundPanel *panel = CC_SOUND_PANEL (object);
+
+ g_clear_object (&panel->mixer_control);
+ g_clear_object (&panel->sound_settings);
+
+ G_OBJECT_CLASS (cc_sound_panel_parent_class)->finalize (object);
+}
+
+static void
+cc_sound_panel_class_init (CcSoundPanelClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ CcPanelClass *panel_class = CC_PANEL_CLASS (klass);
+
+ panel_class->get_help_uri = cc_sound_panel_get_help_uri;
+
+ object_class->finalize = cc_sound_panel_finalize;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/sound/cc-sound-panel.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, balance_slider);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, fade_row);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, fade_slider);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, input_device_combo_box);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, input_level_bar);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, input_list_box);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, input_profile_combo_box);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, input_profile_row);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, input_volume_slider);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, label_size_group);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, output_device_combo_box);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, output_level_bar);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, output_list_box);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, output_profile_combo_box);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, output_profile_row);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, output_volume_slider);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, stream_list_box);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, subwoofer_row);
+ gtk_widget_class_bind_template_child (widget_class, CcSoundPanel, subwoofer_slider);
+
+ gtk_widget_class_bind_template_callback (widget_class, input_device_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, output_device_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, test_output_configuration_button_clicked_cb);
+
+ g_type_ensure (CC_TYPE_ALERT_CHOOSER);
+ g_type_ensure (CC_TYPE_BALANCE_SLIDER);
+ g_type_ensure (CC_TYPE_DEVICE_COMBO_BOX);
+ g_type_ensure (CC_TYPE_FADE_SLIDER);
+ g_type_ensure (CC_TYPE_LEVEL_BAR);
+ g_type_ensure (CC_TYPE_PROFILE_COMBO_BOX);
+ g_type_ensure (CC_TYPE_STREAM_LIST_BOX);
+ g_type_ensure (CC_TYPE_SUBWOOFER_SLIDER);
+ g_type_ensure (CC_TYPE_VOLUME_SLIDER);
+}
+
+static void
+cc_sound_panel_init (CcSoundPanel *self)
+{
+ g_resources_register (cc_sound_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->sound_settings = g_settings_new (KEY_SOUNDS_SCHEMA);
+ g_signal_connect_object (self->sound_settings,
+ "changed::allow-volume-above-100-percent",
+ G_CALLBACK (allow_amplified_changed_cb),
+ self,
+ G_CONNECT_SWAPPED);
+ allow_amplified_changed_cb (self);
+
+ self->mixer_control = gvc_mixer_control_new ("GNOME Settings");
+ gvc_mixer_control_open (self->mixer_control);
+
+ cc_stream_list_box_set_mixer_control (self->stream_list_box, self->mixer_control);
+ cc_volume_slider_set_mixer_control (self->input_volume_slider, self->mixer_control);
+ cc_volume_slider_set_mixer_control (self->output_volume_slider, self->mixer_control);
+ cc_subwoofer_slider_set_mixer_control (self->subwoofer_slider, self->mixer_control);
+ cc_device_combo_box_set_mixer_control (self->input_device_combo_box, self->mixer_control, FALSE);
+ cc_device_combo_box_set_mixer_control (self->output_device_combo_box, self->mixer_control, TRUE);
+ g_signal_connect_object (self->mixer_control,
+ "active-output-update",
+ G_CALLBACK (output_device_update_cb),
+ self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->mixer_control,
+ "active-input-update",
+ G_CALLBACK (input_device_update_cb),
+ self,
+ G_CONNECT_SWAPPED);
+}
diff --git a/panels/sound/cc-sound-panel.h b/panels/sound/cc-sound-panel.h
new file mode 100644
index 0000000..468f2c4
--- /dev/null
+++ b/panels/sound/cc-sound-panel.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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/>.
+ */
+
+#pragma once
+
+#include <shell/cc-panel.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_SOUND_PANEL (cc_sound_panel_get_type ())
+G_DECLARE_FINAL_TYPE (CcSoundPanel, cc_sound_panel, CC, SOUND_PANEL, CcPanel)
+
+G_END_DECLS
diff --git a/panels/sound/cc-sound-panel.ui b/panels/sound/cc-sound-panel.ui
new file mode 100644
index 0000000..33b6dbb
--- /dev/null
+++ b/panels/sound/cc-sound-panel.ui
@@ -0,0 +1,351 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="CcSoundPanel" parent="CcPanel">
+ <child type="content">
+ <object class="AdwPreferencesPage">
+ <child>
+ <object class="AdwPreferencesGroup">
+ <property name="title" translatable="yes">System Volume</property>
+ <child>
+ <object class="AdwPreferencesRow">
+ <accessibility>
+ <property name="label" translatable="yes">Master volume</property>
+ </accessibility>
+ <child>
+ <object class="CcVolumeSlider" id="output_volume_slider">
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <property name="title" translatable="yes">Volume Levels</property>
+ <child>
+ <object class="CcStreamListBox" id="stream_list_box">
+ <property name="label-size-group">label_size_group</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <property name="title" translatable="yes">Output</property>
+ <child>
+ <object class="GtkListBox" id="output_list_box">
+ <property name="selection_mode">none</property>
+ <style>
+ <class name="boxed-list"/>
+ </style>
+ <child>
+ <object class="GtkListBoxRow">
+ <property name="activatable">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="output_device_label">
+ <property name="ellipsize">end</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Output Device</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="hexpand">True</property>
+ <property name="spacing">9</property>
+ <child>
+ <object class="GtkBox">
+ <property name="spacing">6</property>
+ <child>
+ <object class="CcDeviceComboBox" id="output_device_combo_box">
+ <property name="hexpand">True</property>
+ <accessibility>
+ <relation name="labelled-by">output_device_label</relation>
+ </accessibility>
+ <signal name="changed" handler="output_device_changed_cb" object="CcSoundPanel" swapped="yes"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="label" translatable="yes">Test</property>
+ <signal name="clicked" handler="test_output_configuration_button_clicked_cb" object="CcSoundPanel" swapped="yes"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="CcLevelBar" id="output_level_bar" />
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBoxRow" id="output_profile_row">
+ <property name="activatable">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="output_configuration_label">
+ <property name="ellipsize">end</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Configuration</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="spacing">6</property>
+ <child>
+ <object class="CcProfileComboBox" id="output_profile_combo_box">
+ <property name="hexpand">True</property>
+ <accessibility>
+ <relation name="labelled-by">output_configuration_label</relation>
+ </accessibility>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBoxRow">
+ <property name="activatable">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="output_balance_label">
+ <property name="ellipsize">end</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Balance</property>
+ </object>
+ </child>
+ <child>
+ <object class="CcBalanceSlider" id="balance_slider">
+ <property name="hexpand">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBoxRow" id="fade_row">
+ <property name="activatable">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="output_fade_label">
+ <property name="ellipsize">end</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Fade</property>
+ </object>
+ </child>
+ <child>
+ <object class="CcFadeSlider" id="fade_slider">
+ <property name="hexpand">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBoxRow" id="subwoofer_row">
+ <property name="activatable">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="output_subwoofer_label">
+ <property name="ellipsize">end</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Subwoofer</property>
+ </object>
+ </child>
+ <child>
+ <object class="CcSubwooferSlider" id="subwoofer_slider">
+ <property name="hexpand">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <property name="title" translatable="yes">Input</property>
+ <child>
+ <object class="GtkListBox" id="input_list_box">
+ <property name="selection_mode">none</property>
+ <style>
+ <class name="boxed-list"/>
+ </style>
+ <child>
+ <object class="GtkListBoxRow">
+ <property name="activatable">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="input_device_label">
+ <property name="ellipsize">end</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Input Device</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="hexpand">True</property>
+ <property name="spacing">9</property>
+ <child>
+ <object class="CcDeviceComboBox" id="input_device_combo_box">
+ <property name="hexpand">True</property>
+ <accessibility>
+ <relation name="labelled-by">input_device_label</relation>
+ </accessibility>
+ <signal name="changed" handler="input_device_changed_cb" object="CcSoundPanel" swapped="yes"/>
+ </object>
+ </child>
+ <child>
+ <object class="CcLevelBar" id="input_level_bar" />
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBoxRow" id="input_profile_row">
+ <property name="activatable">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="input_configuration_label">
+ <property name="ellipsize">end</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Configuration</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="spacing">6</property>
+ <child>
+ <object class="CcProfileComboBox" id="input_profile_combo_box">
+ <property name="hexpand">True</property>
+ <accessibility>
+ <relation name="labelled-by">input_configuration_label</relation>"
+ </accessibility>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBoxRow">
+ <property name="activatable">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="input_volume_label">
+ <property name="ellipsize">end</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Volume</property>
+ </object>
+ </child>
+ <child>
+ <object class="CcVolumeSlider" id="input_volume_slider">
+ <property name="hexpand">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <property name="title" translatable="yes">Alert Sound</property>
+ <child>
+ <object class="CcAlertChooser">
+ <property name="hexpand">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtkSizeGroup" id="label_size_group">
+ <property name="mode">horizontal</property>
+ <widgets>
+ <widget name="output_device_label"/>
+ <widget name="output_configuration_label"/>
+ <widget name="output_balance_label"/>
+ <widget name="output_fade_label"/>
+ <widget name="output_subwoofer_label"/>
+ <widget name="input_configuration_label"/>
+ <widget name="input_device_label"/>
+ <widget name="input_volume_label"/>
+ </widgets>
+ </object>
+</interface>
diff --git a/panels/sound/cc-speaker-test-button.c b/panels/sound/cc-speaker-test-button.c
new file mode 100644
index 0000000..71824aa
--- /dev/null
+++ b/panels/sound/cc-speaker-test-button.c
@@ -0,0 +1,241 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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 <gsound.h>
+#include <pulse/pulseaudio.h>
+
+#include "cc-sound-resources.h"
+#include "cc-speaker-test-button.h"
+
+struct _CcSpeakerTestButton
+{
+ GtkDialog parent_instance;
+
+ GtkImage *image;
+ GtkLabel *label;
+
+ GCancellable *cancellable;
+ GSoundContext *context;
+ pa_channel_position_t position;
+ gboolean playing;
+ gint event_index;
+};
+
+G_DEFINE_TYPE (CcSpeakerTestButton, cc_speaker_test_button, GTK_TYPE_BUTTON)
+
+#define TEST_SOUND_ID 1
+
+static gboolean
+play_sound (CcSpeakerTestButton *self);
+
+static const gchar *
+get_icon_name (CcSpeakerTestButton *self)
+{
+ switch (self->position)
+ {
+ case PA_CHANNEL_POSITION_FRONT_LEFT:
+ return self->playing ? "audio-speaker-left-testing" : "audio-speaker-left";
+ case PA_CHANNEL_POSITION_FRONT_RIGHT:
+ return self->playing ? "audio-speaker-right-testing" : "audio-speaker-right";
+ case PA_CHANNEL_POSITION_FRONT_CENTER:
+ return self->playing ? "audio-speaker-center-testing" : "audio-speaker-center";
+ case PA_CHANNEL_POSITION_REAR_LEFT:
+ return self->playing ? "audio-speaker-left-back-testing" : "audio-speaker-left-back";
+ case PA_CHANNEL_POSITION_REAR_RIGHT:
+ return self->playing ? "audio-speaker-right-back-testing" : "audio-speaker-right-back";
+ case PA_CHANNEL_POSITION_REAR_CENTER:
+ return self->playing ? "audio-speaker-center-back-testing" : "audio-speaker-center-back";
+ case PA_CHANNEL_POSITION_LFE:
+ return self->playing ? "audio-subwoofer-testing" : "audio-subwoofer";
+ case PA_CHANNEL_POSITION_SIDE_LEFT:
+ return self->playing ? "audio-speaker-left-side-testing" : "audio-speaker-left-side";
+ case PA_CHANNEL_POSITION_SIDE_RIGHT:
+ return self->playing ? "audio-speaker-right-side-testing" : "audio-speaker-right-side";
+ case PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER:
+ return self->playing ? "audio-speaker-front-left-of-center-testing" : "audio-speaker-front-left-of-center";
+ case PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER:
+ return self->playing ? "audio-speaker-front-right-of-center-testing" : "audio-speaker-front-right-of-center";
+ case PA_CHANNEL_POSITION_MONO:
+ return self->playing ? "audio-speaker-mono-testing" : "audio-speaker-mono";
+ default:
+ return "audio-speakers";
+ }
+}
+
+static void
+update_icon (CcSpeakerTestButton *self)
+{
+ gtk_image_set_from_icon_name (self->image, get_icon_name (self));
+}
+
+static GStrv
+get_sound_events (CcSpeakerTestButton *self)
+{
+ switch (self->position)
+ {
+ case PA_CHANNEL_POSITION_FRONT_LEFT:
+ return g_strsplit ("audio-channel-front-left;audio-test-signal;bell", ";", -1);
+ case PA_CHANNEL_POSITION_FRONT_RIGHT:
+ return g_strsplit ("audio-channel-front-right;audio-test-signal;bell", ";", -1);
+ case PA_CHANNEL_POSITION_FRONT_CENTER:
+ return g_strsplit ("audio-channel-front-center;audio-test-signal;bell", ";", -1);
+ case PA_CHANNEL_POSITION_REAR_LEFT:
+ return g_strsplit ("audio-channel-rear-left;audio-test-signal;bell", ";", -1);
+ case PA_CHANNEL_POSITION_REAR_RIGHT:
+ return g_strsplit ("audio-channel-rear-right;audio-test-signal;bell", ";", -1);
+ case PA_CHANNEL_POSITION_REAR_CENTER:
+ return g_strsplit ("audio-channel-rear-center;audio-test-signal;bell", ";", -1);
+ case PA_CHANNEL_POSITION_LFE:
+ return g_strsplit ("audio-channel-lfe;audio-test-signal;bell", ";", -1);
+ case PA_CHANNEL_POSITION_SIDE_LEFT:
+ return g_strsplit ("audio-channel-side-left;audio-test-signal;bell", ";", -1);
+ case PA_CHANNEL_POSITION_SIDE_RIGHT:
+ return g_strsplit ("audio-channel-side-right;audio-test-signal;bell", ";", -1);
+ case PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER:
+ return g_strsplit ("audio-channel-front-left-of-center;audio-test-signal;bell", ";", -1);
+ case PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER:
+ return g_strsplit ("audio-channel-front-right-of-center;audio-test-signal;bell", ";", -1);
+ case PA_CHANNEL_POSITION_MONO:
+ return g_strsplit ("audio-channel-mono;audio-test-signal;bell", ";", -1);
+ default:
+ return g_strsplit ("audio-test-signal;bell", ";", -1);
+ }
+}
+
+static void
+finish_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer userdata)
+{
+ CcSpeakerTestButton *self = userdata;
+ g_autoptr(GError) error = NULL;
+
+ if (!gsound_context_play_full_finish (GSOUND_CONTEXT (object), result, &error))
+ {
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ return;
+
+ if (play_sound (self))
+ return;
+
+ g_warning ("Failed to play sound: %s", error->message);
+ }
+
+ self->playing = FALSE;
+ update_icon (self);
+}
+
+static gboolean
+play_sound (CcSpeakerTestButton *self)
+{
+ g_auto(GStrv) events = NULL;
+
+ /* Stop existing sound */
+ g_cancellable_cancel (self->cancellable);
+ g_clear_object (&self->cancellable);
+ self->cancellable = g_cancellable_new ();
+
+ events = get_sound_events (self);
+ if (events[self->event_index] == NULL)
+ return FALSE;
+
+ gsound_context_play_full (self->context, self->cancellable, finish_cb, self,
+ GSOUND_ATTR_MEDIA_ROLE, "test",
+ GSOUND_ATTR_MEDIA_NAME, pa_channel_position_to_pretty_string (self->position),
+ GSOUND_ATTR_CANBERRA_FORCE_CHANNEL, pa_channel_position_to_string (self->position),
+ GSOUND_ATTR_CANBERRA_ENABLE, "1",
+ GSOUND_ATTR_EVENT_ID, events[self->event_index],
+ NULL);
+ self->event_index++;
+
+ return TRUE;
+}
+
+static void
+clicked_cb (CcSpeakerTestButton *self)
+{
+ if (self->context == NULL)
+ return;
+
+ self->playing = TRUE;
+ update_icon (self);
+
+ /* Play the per-channel sound name or a generic sound */
+ self->event_index = 0;
+ play_sound (self);
+}
+
+static void
+cc_speaker_test_button_dispose (GObject *object)
+{
+ CcSpeakerTestButton *self = CC_SPEAKER_TEST_BUTTON (object);
+
+ g_cancellable_cancel (self->cancellable);
+ g_clear_object (&self->cancellable);
+ g_clear_object (&self->context);
+
+ G_OBJECT_CLASS (cc_speaker_test_button_parent_class)->dispose (object);
+}
+
+void
+cc_speaker_test_button_class_init (CcSpeakerTestButtonClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_speaker_test_button_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/sound/cc-speaker-test-button.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcSpeakerTestButton, image);
+ gtk_widget_class_bind_template_child (widget_class, CcSpeakerTestButton, label);
+
+ gtk_widget_class_bind_template_callback (widget_class, clicked_cb);
+}
+
+void
+cc_speaker_test_button_init (CcSpeakerTestButton *self)
+{
+ g_resources_register (cc_sound_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->cancellable = g_cancellable_new ();
+
+ update_icon (self);
+}
+
+CcSpeakerTestButton *
+cc_speaker_test_button_new (void)
+{
+ return g_object_new (CC_TYPE_SPEAKER_TEST_BUTTON, NULL);
+}
+
+void
+cc_speaker_test_button_set_channel_position (CcSpeakerTestButton *self,
+ GSoundContext *context,
+ pa_channel_position_t position)
+{
+ g_return_if_fail (CC_IS_SPEAKER_TEST_BUTTON (self));
+
+ g_clear_object (&self->context);
+ self->context = g_object_ref (context);
+ self->position = position;
+ gtk_label_set_label (self->label, pa_channel_position_to_pretty_string (position));
+ update_icon (self);
+}
diff --git a/panels/sound/cc-speaker-test-button.h b/panels/sound/cc-speaker-test-button.h
new file mode 100644
index 0000000..32c4c31
--- /dev/null
+++ b/panels/sound/cc-speaker-test-button.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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/>.
+ */
+
+#pragma once
+
+#include <gsound.h>
+#include <gtk/gtk.h>
+#include <pulse/pulseaudio.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_SPEAKER_TEST_BUTTON (cc_speaker_test_button_get_type ())
+G_DECLARE_FINAL_TYPE (CcSpeakerTestButton, cc_speaker_test_button, CC, SPEAKER_TEST_BUTTON, GtkButton)
+
+CcSpeakerTestButton *cc_speaker_test_button_new (void);
+
+void cc_speaker_test_button_set_channel_position (CcSpeakerTestButton *button,
+ GSoundContext *context,
+ pa_channel_position_t position);
+
+G_END_DECLS
diff --git a/panels/sound/cc-speaker-test-button.ui b/panels/sound/cc-speaker-test-button.ui
new file mode 100644
index 0000000..2c8bdf8
--- /dev/null
+++ b/panels/sound/cc-speaker-test-button.ui
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcSpeakerTestButton" parent="GtkButton">
+ <signal name="clicked" handler="clicked_cb" object="CcSpeakerTestButton" swapped="yes"/>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="visible">True</property>
+ <property name="icon-size">large</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label">
+ <property name="visible">True</property>
+ <property name="wrap">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/panels/sound/cc-stream-list-box.c b/panels/sound/cc-stream-list-box.c
new file mode 100644
index 0000000..d981976
--- /dev/null
+++ b/panels/sound/cc-stream-list-box.c
@@ -0,0 +1,248 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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 <pulse/pulseaudio.h>
+#include <gvc-mixer-sink.h>
+#include <gvc-mixer-source.h>
+
+#include "cc-stream-list-box.h"
+#include "cc-stream-row.h"
+#include "cc-sound-enums.h"
+#include "cc-sound-resources.h"
+
+struct _CcStreamListBox
+{
+ GtkBox parent_instance;
+
+ GtkListBox *listbox;
+ GtkSizeGroup *label_size_group;
+ GvcMixerControl *mixer_control;
+ CcStreamType stream_type;
+ guint stream_added_handler_id;
+ guint stream_removed_handler_id;
+};
+
+G_DEFINE_TYPE (CcStreamListBox, cc_stream_list_box, GTK_TYPE_BOX)
+
+enum
+{
+ PROP_0,
+ PROP_LABEL_SIZE_GROUP
+};
+
+static gint
+sort_cb (GtkListBoxRow *row1,
+ GtkListBoxRow *row2,
+ gpointer user_data)
+{
+ CcStreamListBox *self = user_data;
+ GvcMixerStream *stream1, *stream2, *event_sink;
+ g_autofree gchar *name1 = NULL;
+ g_autofree gchar *name2 = NULL;
+
+ stream1 = cc_stream_row_get_stream (CC_STREAM_ROW (row1));
+ stream2 = cc_stream_row_get_stream (CC_STREAM_ROW (row2));
+
+ /* Put the system sound events control at the top */
+ event_sink = gvc_mixer_control_get_event_sink_input (self->mixer_control);
+ if (stream1 == event_sink)
+ return -1;
+ else if (stream2 == event_sink)
+ return 1;
+
+ name1 = g_utf8_casefold (gvc_mixer_stream_get_name (stream1), -1);
+ name2 = g_utf8_casefold (gvc_mixer_stream_get_name (stream2), -1);
+
+ return g_strcmp0 (name1, name2);
+}
+
+static void
+stream_added_cb (CcStreamListBox *self,
+ guint id)
+{
+ GvcMixerStream *stream;
+ const gchar *app_id;
+ CcStreamRow *row;
+
+ stream = gvc_mixer_control_lookup_stream_id (self->mixer_control, id);
+ if (stream == NULL)
+ return;
+
+ app_id = gvc_mixer_stream_get_application_id (stream);
+
+ /* Skip master volume controls */
+ if (g_strcmp0 (app_id, "org.gnome.VolumeControl") == 0 ||
+ g_strcmp0 (app_id, "org.PulseAudio.pavucontrol") == 0)
+ {
+ return;
+ }
+
+ /* Skip streams that aren't volume controls */
+ if (GVC_IS_MIXER_SOURCE (stream) ||
+ GVC_IS_MIXER_SINK (stream) ||
+ gvc_mixer_stream_is_virtual (stream) ||
+ gvc_mixer_stream_is_event_stream (stream))
+ {
+ return;
+ }
+
+ row = cc_stream_row_new (self->label_size_group, stream, id, self->stream_type, self->mixer_control);
+ gtk_list_box_append (self->listbox, GTK_WIDGET (row));
+}
+
+static GtkWidget *
+find_row (CcStreamListBox *self,
+ guint id)
+{
+ GtkWidget *child;
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (self->listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ if (!CC_IS_STREAM_ROW (child))
+ continue;
+
+ if (id == cc_stream_row_get_id (CC_STREAM_ROW (child)))
+ return child;
+ }
+
+ return NULL;
+}
+
+static void
+stream_removed_cb (CcStreamListBox *self,
+ guint id)
+{
+ GtkWidget *row;
+
+ row = find_row (self, id);
+ if (row != NULL)
+ gtk_list_box_remove (self->listbox, row);
+}
+
+static void
+cc_stream_list_box_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CcStreamListBox *self = CC_STREAM_LIST_BOX (object);
+
+ switch (property_id) {
+ case PROP_LABEL_SIZE_GROUP:
+ self->label_size_group = g_value_dup_object (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+cc_stream_list_box_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CcStreamListBox *self = CC_STREAM_LIST_BOX (object);
+
+ switch (property_id) {
+ case PROP_LABEL_SIZE_GROUP:
+ g_value_set_object (value, self->label_size_group);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+cc_stream_list_box_dispose (GObject *object)
+{
+ CcStreamListBox *self = CC_STREAM_LIST_BOX (object);
+
+ g_clear_object (&self->mixer_control);
+
+ G_OBJECT_CLASS (cc_stream_list_box_parent_class)->dispose (object);
+}
+
+void
+cc_stream_list_box_class_init (CcStreamListBoxClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->set_property = cc_stream_list_box_set_property;
+ object_class->get_property = cc_stream_list_box_get_property;
+ object_class->dispose = cc_stream_list_box_dispose;
+
+ g_object_class_install_property (object_class, PROP_LABEL_SIZE_GROUP,
+ g_param_spec_object ("label-size-group",
+ NULL,
+ NULL,
+ GTK_TYPE_SIZE_GROUP,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/sound/cc-stream-list-box.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcStreamListBox, listbox);
+}
+
+void
+cc_stream_list_box_init (CcStreamListBox *self)
+{
+ g_resources_register (cc_sound_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtk_list_box_set_sort_func (self->listbox, sort_cb, self, NULL);
+}
+
+void
+cc_stream_list_box_set_mixer_control (CcStreamListBox *self,
+ GvcMixerControl *mixer_control)
+{
+ g_return_if_fail (CC_IS_STREAM_LIST_BOX (self));
+
+ if (self->mixer_control != NULL)
+ {
+ g_signal_handler_disconnect (self->mixer_control, self->stream_added_handler_id);
+ self->stream_added_handler_id = 0;
+ g_signal_handler_disconnect (self->mixer_control, self->stream_removed_handler_id);
+ self->stream_removed_handler_id = 0;
+ }
+ g_clear_object (&self->mixer_control);
+
+ self->mixer_control = g_object_ref (mixer_control);
+
+ self->stream_added_handler_id = g_signal_connect_object (self->mixer_control,
+ "stream-added",
+ G_CALLBACK (stream_added_cb),
+ self, G_CONNECT_SWAPPED);
+ self->stream_removed_handler_id = g_signal_connect_object (self->mixer_control,
+ "stream-removed",
+ G_CALLBACK (stream_removed_cb),
+ self, G_CONNECT_SWAPPED);
+}
+
+void cc_stream_list_box_set_stream_type (CcStreamListBox *self,
+ CcStreamType stream_type)
+{
+ g_return_if_fail (CC_IS_STREAM_LIST_BOX (self));
+
+ self->stream_type = stream_type;
+}
diff --git a/panels/sound/cc-stream-list-box.h b/panels/sound/cc-stream-list-box.h
new file mode 100644
index 0000000..f2d3075
--- /dev/null
+++ b/panels/sound/cc-stream-list-box.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <pulse/pulseaudio.h>
+#include <gvc-mixer-control.h>
+
+#include "cc-sound-enums.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_STREAM_LIST_BOX (cc_stream_list_box_get_type ())
+G_DECLARE_FINAL_TYPE (CcStreamListBox, cc_stream_list_box, CC, STREAM_LIST_BOX, GtkBox)
+
+void cc_stream_list_box_set_mixer_control (CcStreamListBox *combo_box,
+ GvcMixerControl *mixer_control);
+
+void cc_stream_list_box_set_stream_type (CcStreamListBox *combo_box,
+ CcStreamType type);
+
+G_END_DECLS
diff --git a/panels/sound/cc-stream-list-box.ui b/panels/sound/cc-stream-list-box.ui
new file mode 100644
index 0000000..6050c2c
--- /dev/null
+++ b/panels/sound/cc-stream-list-box.ui
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcStreamListBox" parent="GtkBox">
+ <child>
+ <object class="GtkListBox" id="listbox">
+ <property name="hexpand">True</property>
+ <property name="selection-mode">none</property>
+ <style>
+ <class name="boxed-list" />
+ </style>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/panels/sound/cc-stream-row.c b/panels/sound/cc-stream-row.c
new file mode 100644
index 0000000..8322df7
--- /dev/null
+++ b/panels/sound/cc-stream-row.c
@@ -0,0 +1,142 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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 "cc-sound-resources.h"
+#include "cc-stream-row.h"
+#include "cc-volume-slider.h"
+#include "cc-sound-enums.h"
+
+#define SPEECH_DISPATCHER_PREFIX "speech-dispatcher-"
+
+struct _CcStreamRow
+{
+ GtkListBoxRow parent_instance;
+
+ GtkBox *label_box;
+ GtkLabel *name_label;
+ GtkImage *icon_image;
+ CcVolumeSlider *volume_slider;
+
+ GvcMixerStream *stream;
+ guint id;
+};
+
+G_DEFINE_TYPE (CcStreamRow, cc_stream_row, GTK_TYPE_LIST_BOX_ROW)
+
+static void
+cc_stream_row_dispose (GObject *object)
+{
+ CcStreamRow *self = CC_STREAM_ROW (object);
+
+ g_clear_object (&self->stream);
+
+ G_OBJECT_CLASS (cc_stream_row_parent_class)->dispose (object);
+}
+
+void
+cc_stream_row_class_init (CcStreamRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_stream_row_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/sound/cc-stream-row.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcStreamRow, label_box);
+ gtk_widget_class_bind_template_child (widget_class, CcStreamRow, icon_image);
+ gtk_widget_class_bind_template_child (widget_class, CcStreamRow, name_label);
+ gtk_widget_class_bind_template_child (widget_class, CcStreamRow, volume_slider);
+}
+
+void
+cc_stream_row_init (CcStreamRow *self)
+{
+ g_resources_register (cc_sound_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+CcStreamRow *
+cc_stream_row_new (GtkSizeGroup *size_group,
+ GvcMixerStream *stream,
+ guint id,
+ CcStreamType stream_type,
+ GvcMixerControl *mixer_control)
+{
+ CcStreamRow *self;
+ g_autoptr(GtkIconPaintable) icon_paintable = NULL;
+ g_autoptr(GIcon) gicon = NULL;
+ const gchar *stream_name;
+ const gchar *icon_name;
+ g_autofree gchar *symbolic_icon_name = NULL;
+
+ self = g_object_new (CC_TYPE_STREAM_ROW, NULL);
+ self->stream = g_object_ref (stream);
+ self->id = id;
+
+ icon_name = gvc_mixer_stream_get_icon_name (stream);
+ stream_name = gvc_mixer_stream_get_name (stream);
+
+ if (g_str_has_suffix (icon_name, "-symbolic"))
+ symbolic_icon_name = strdup (icon_name);
+ else
+ symbolic_icon_name = g_strconcat (icon_name, "-symbolic", NULL);
+
+ /* Explicitly lookup for the icon, since some streams may give us an
+ * icon name (e.g. "audio") that doesn't really exist in the theme.
+ */
+ icon_paintable = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_for_display (gdk_display_get_default ()),
+ icon_name,
+ NULL,
+ 24,
+ gtk_widget_get_scale_factor (GTK_WIDGET (self)),
+ GTK_TEXT_DIR_RTL,
+ 0);
+
+ if (icon_paintable)
+ gicon = g_themed_icon_new_with_default_fallbacks (symbolic_icon_name);
+ else if (g_str_has_prefix (stream_name, SPEECH_DISPATCHER_PREFIX))
+ gicon = g_themed_icon_new_with_default_fallbacks ("org.gnome.Settings-accessibility-symbolic");
+ else
+ gicon = g_themed_icon_new_with_default_fallbacks ("application-x-executable-symbolic");
+
+ gtk_image_set_from_gicon (self->icon_image, gicon);
+
+ gtk_label_set_label (self->name_label, gvc_mixer_stream_get_name (stream));
+ cc_volume_slider_set_stream (self->volume_slider, stream, stream_type);
+ cc_volume_slider_set_mixer_control (self->volume_slider, mixer_control);
+
+ gtk_size_group_add_widget (size_group, GTK_WIDGET (self->label_box));
+
+ return self;
+}
+
+GvcMixerStream *
+cc_stream_row_get_stream (CcStreamRow *self)
+{
+ g_return_val_if_fail (CC_IS_STREAM_ROW (self), NULL);
+ return self->stream;
+}
+
+guint
+cc_stream_row_get_id (CcStreamRow *self)
+{
+ g_return_val_if_fail (CC_IS_STREAM_ROW (self), 0);
+ return self->id;
+}
diff --git a/panels/sound/cc-stream-row.h b/panels/sound/cc-stream-row.h
new file mode 100644
index 0000000..9e4de9b
--- /dev/null
+++ b/panels/sound/cc-stream-row.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <pulse/pulseaudio.h>
+#include <gvc-mixer-control.h>
+#include <gvc-mixer-stream.h>
+
+#include "cc-sound-enums.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_STREAM_ROW (cc_stream_row_get_type ())
+G_DECLARE_FINAL_TYPE (CcStreamRow, cc_stream_row, CC, STREAM_ROW, GtkListBoxRow)
+
+CcStreamRow *cc_stream_row_new (GtkSizeGroup *size_group,
+ GvcMixerStream *stream,
+ guint id,
+ CcStreamType stream_type,
+ GvcMixerControl *mixer_control);
+
+GvcMixerStream *cc_stream_row_get_stream (CcStreamRow *row);
+
+guint cc_stream_row_get_id (CcStreamRow *row);
+
+G_END_DECLS
diff --git a/panels/sound/cc-stream-row.ui b/panels/sound/cc-stream-row.ui
new file mode 100644
index 0000000..756f6bb
--- /dev/null
+++ b/panels/sound/cc-stream-row.ui
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcStreamRow" parent="GtkListBoxRow">
+ <property name="activatable">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkBox" id="label_box">
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkImage" id="icon_image">
+ <style>
+ <class name="lowres-icon"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="name_label">
+ <property name="ellipsize">end</property>
+ <property name="xalign">0</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="CcVolumeSlider" id="volume_slider">
+ <property name="hexpand">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/panels/sound/cc-subwoofer-slider.c b/panels/sound/cc-subwoofer-slider.c
new file mode 100644
index 0000000..f209ec2
--- /dev/null
+++ b/panels/sound/cc-subwoofer-slider.c
@@ -0,0 +1,135 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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 <pulse/pulseaudio.h>
+#include <gvc-mixer-control.h>
+
+#include "cc-sound-resources.h"
+#include "cc-subwoofer-slider.h"
+#include "gvc-channel-map-private.h"
+
+struct _CcSubwooferSlider
+{
+ GtkBox parent_instance;
+
+ GtkAdjustment *adjustment;
+
+ GvcChannelMap *channel_map;
+ guint volume_changed_handler_id;
+};
+
+G_DEFINE_TYPE (CcSubwooferSlider, cc_subwoofer_slider, GTK_TYPE_BOX)
+
+static void
+changed_cb (CcSubwooferSlider *self)
+{
+ gdouble value;
+ const pa_channel_map *pa_map;
+ pa_cvolume pa_volume;
+
+ if (self->channel_map == NULL)
+ return;
+
+ value = gtk_adjustment_get_value (self->adjustment);
+ pa_map = gvc_channel_map_get_pa_channel_map (self->channel_map);
+ pa_volume = *gvc_channel_map_get_cvolume (self->channel_map);
+ pa_cvolume_set_position (&pa_volume, pa_map, PA_CHANNEL_POSITION_LFE, value);
+ gvc_channel_map_volume_changed (self->channel_map, &pa_volume, TRUE);
+}
+
+static void
+volume_changed_cb (CcSubwooferSlider *self)
+{
+ const gdouble *volumes;
+
+ volumes = gvc_channel_map_get_volume (self->channel_map);
+ g_signal_handlers_block_by_func (self->adjustment, volume_changed_cb, self);
+ gtk_adjustment_set_value (self->adjustment, volumes[LFE]);
+ g_signal_handlers_unblock_by_func (self->adjustment, volume_changed_cb, self);
+}
+
+static void
+cc_subwoofer_slider_dispose (GObject *object)
+{
+ CcSubwooferSlider *self = CC_SUBWOOFER_SLIDER (object);
+
+ g_clear_object (&self->channel_map);
+
+ G_OBJECT_CLASS (cc_subwoofer_slider_parent_class)->dispose (object);
+}
+
+void
+cc_subwoofer_slider_class_init (CcSubwooferSliderClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_subwoofer_slider_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/sound/cc-subwoofer-slider.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcSubwooferSlider, adjustment);
+
+ gtk_widget_class_bind_template_callback (widget_class, changed_cb);
+}
+
+void
+cc_subwoofer_slider_init (CcSubwooferSlider *self)
+{
+ g_resources_register (cc_sound_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+void
+cc_subwoofer_slider_set_mixer_control (CcSubwooferSlider *self,
+ GvcMixerControl *mixer_control)
+{
+ gdouble vol_max_norm;
+
+ g_return_if_fail (CC_IS_SUBWOOFER_SLIDER (self));
+
+ vol_max_norm = gvc_mixer_control_get_vol_max_norm (mixer_control);
+ gtk_adjustment_set_upper (self->adjustment, vol_max_norm);
+ gtk_adjustment_set_page_increment (self->adjustment, vol_max_norm / 100.0);
+}
+
+void
+cc_subwoofer_slider_set_channel_map (CcSubwooferSlider *self,
+ GvcChannelMap *channel_map)
+{
+ g_return_if_fail (CC_IS_SUBWOOFER_SLIDER (self));
+
+ if (self->channel_map != NULL)
+ {
+ g_signal_handler_disconnect (self->channel_map, self->volume_changed_handler_id);
+ self->volume_changed_handler_id = 0;
+ }
+ g_clear_object (&self->channel_map);
+
+ if (channel_map != NULL)
+ {
+ self->channel_map = g_object_ref (channel_map);
+
+ self->volume_changed_handler_id = g_signal_connect_object (channel_map,
+ "volume-changed",
+ G_CALLBACK (volume_changed_cb),
+ self, G_CONNECT_SWAPPED);
+ volume_changed_cb (self);
+ }
+}
diff --git a/panels/sound/cc-subwoofer-slider.h b/panels/sound/cc-subwoofer-slider.h
new file mode 100644
index 0000000..8b462dc
--- /dev/null
+++ b/panels/sound/cc-subwoofer-slider.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <pulse/pulseaudio.h>
+#include <gvc-channel-map.h>
+#include <gvc-mixer-control.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_SUBWOOFER_SLIDER (cc_subwoofer_slider_get_type ())
+G_DECLARE_FINAL_TYPE (CcSubwooferSlider, cc_subwoofer_slider, CC, SUBWOOFER_SLIDER, GtkBox)
+
+void cc_subwoofer_slider_set_mixer_control (CcSubwooferSlider *slider,
+ GvcMixerControl *mixer_control);
+
+void cc_subwoofer_slider_set_channel_map (CcSubwooferSlider *slider,
+ GvcChannelMap *channel_map);
+
+G_END_DECLS
diff --git a/panels/sound/cc-subwoofer-slider.ui b/panels/sound/cc-subwoofer-slider.ui
new file mode 100644
index 0000000..057da4b
--- /dev/null
+++ b/panels/sound/cc-subwoofer-slider.ui
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcSubwooferSlider" parent="GtkBox">
+ <child>
+ <object class="GtkScale">
+ <property name="visible">True</property>
+ <property name="hexpand">True</property>
+ <property name="draw_value">False</property>
+ <property name="has_origin">False</property>
+ <property name="adjustment">adjustment</property>
+ <accessibility>
+ <property name="label" translatable="yes">Subwoofer</property>
+ </accessibility>
+ </object>
+ </child>
+ </template>
+ <object class="GtkAdjustment" id="adjustment">
+ <signal name="value_changed" handler="changed_cb" object="CcSubwooferSlider" swapped="yes"/>
+ </object>
+</interface>
diff --git a/panels/sound/cc-volume-slider.c b/panels/sound/cc-volume-slider.c
new file mode 100644
index 0000000..5c37765
--- /dev/null
+++ b/panels/sound/cc-volume-slider.c
@@ -0,0 +1,263 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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 <glib/gi18n.h>
+#include <math.h>
+#include <pulse/pulseaudio.h>
+#include <gvc-mixer-control.h>
+
+#include "cc-sound-resources.h"
+#include "cc-volume-slider.h"
+
+struct _CcVolumeSlider
+{
+ GtkBox parent_instance;
+
+ GtkToggleButton *mute_button;
+ GtkAdjustment *volume_adjustment;
+ GtkScale *volume_scale;
+
+ gboolean is_amplified;
+ GvcMixerControl *mixer_control;
+ GvcMixerStream *stream;
+ guint notify_volume_handler_id;
+ guint notify_is_muted_handler_id;
+};
+
+G_DEFINE_TYPE (CcVolumeSlider, cc_volume_slider, GTK_TYPE_BOX)
+
+static void
+update_volume_icon (CcVolumeSlider *self)
+{
+ const gchar *icon_name = NULL;
+ gdouble volume, fraction;
+
+ volume = gtk_adjustment_get_value (self->volume_adjustment);
+ fraction = (100.0 * volume) / gtk_adjustment_get_upper (self->volume_adjustment);
+
+ if (gtk_toggle_button_get_active (self->mute_button))
+ icon_name = "audio-volume-muted-symbolic";
+ else if (fraction < 30.0)
+ icon_name = "audio-volume-low-symbolic";
+ else if (fraction > 30.0 && fraction < 70.0)
+ icon_name = "audio-volume-medium-symbolic";
+ else
+ icon_name = "audio-volume-high-symbolic";
+
+ gtk_button_set_icon_name (GTK_BUTTON (self->mute_button), icon_name);
+}
+
+static void
+volume_changed_cb (CcVolumeSlider *self)
+{
+ gdouble volume, rounded;
+
+ if (self->stream == NULL)
+ return;
+
+ volume = gtk_adjustment_get_value (self->volume_adjustment);
+ rounded = round (volume);
+
+ gtk_toggle_button_set_active (self->mute_button, volume == 0.0);
+
+ if (gvc_mixer_stream_set_volume (self->stream, (pa_volume_t) rounded))
+ gvc_mixer_stream_push_volume (self->stream);
+
+ update_volume_icon (self);
+}
+
+static void
+notify_volume_cb (CcVolumeSlider *self)
+{
+ g_signal_handlers_block_by_func (self->volume_adjustment, volume_changed_cb, self);
+
+ if (gtk_toggle_button_get_active (self->mute_button))
+ gtk_adjustment_set_value (self->volume_adjustment, 0.0);
+ else
+ gtk_adjustment_set_value (self->volume_adjustment, gvc_mixer_stream_get_volume (self->stream));
+
+ update_volume_icon (self);
+
+ g_signal_handlers_unblock_by_func (self->volume_adjustment, volume_changed_cb, self);
+}
+
+static void
+update_ranges (CcVolumeSlider *self)
+{
+ gdouble vol_max_norm;
+
+ if (self->mixer_control == NULL)
+ return;
+
+ vol_max_norm = gvc_mixer_control_get_vol_max_norm (self->mixer_control);
+
+ gtk_scale_clear_marks (self->volume_scale);
+ if (self->is_amplified)
+ {
+ gtk_adjustment_set_upper (self->volume_adjustment, gvc_mixer_control_get_vol_max_amplified (self->mixer_control));
+ gtk_scale_add_mark (self->volume_scale,
+ vol_max_norm,
+ GTK_POS_BOTTOM,
+ C_("volume", "100%"));
+ }
+ else
+ {
+ gtk_adjustment_set_upper (self->volume_adjustment, vol_max_norm);
+ }
+ gtk_adjustment_set_page_increment (self->volume_adjustment, vol_max_norm / 100.0);
+
+ if (self->stream)
+ notify_volume_cb (self);
+}
+
+static void
+mute_button_toggled_cb (CcVolumeSlider *self)
+{
+ if (self->stream == NULL)
+ return;
+
+ gvc_mixer_stream_change_is_muted (self->stream, gtk_toggle_button_get_active (self->mute_button));
+
+ update_volume_icon (self);
+}
+
+static void
+notify_is_muted_cb (CcVolumeSlider *self)
+{
+ g_signal_handlers_block_by_func (self->mute_button, mute_button_toggled_cb, self);
+ gtk_toggle_button_set_active (self->mute_button, gvc_mixer_stream_get_is_muted (self->stream));
+ g_signal_handlers_unblock_by_func (self->mute_button, mute_button_toggled_cb, self);
+ notify_volume_cb (self);
+}
+
+static void
+cc_volume_slider_dispose (GObject *object)
+{
+ CcVolumeSlider *self = CC_VOLUME_SLIDER (object);
+
+ g_clear_object (&self->mixer_control);
+ g_clear_object (&self->stream);
+
+ G_OBJECT_CLASS (cc_volume_slider_parent_class)->dispose (object);
+}
+
+void
+cc_volume_slider_class_init (CcVolumeSliderClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_volume_slider_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/sound/cc-volume-slider.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcVolumeSlider, mute_button);
+ gtk_widget_class_bind_template_child (widget_class, CcVolumeSlider, volume_adjustment);
+ gtk_widget_class_bind_template_child (widget_class, CcVolumeSlider, volume_scale);
+
+ gtk_widget_class_bind_template_callback (widget_class, mute_button_toggled_cb);
+ gtk_widget_class_bind_template_callback (widget_class, volume_changed_cb);
+}
+
+void
+cc_volume_slider_init (CcVolumeSlider *self)
+{
+ g_resources_register (cc_sound_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+void
+cc_volume_slider_set_mixer_control (CcVolumeSlider *self,
+ GvcMixerControl *mixer_control)
+{
+ g_return_if_fail (CC_IS_VOLUME_SLIDER (self));
+
+ g_set_object (&self->mixer_control, mixer_control);
+
+ update_ranges (self);
+}
+
+void
+cc_volume_slider_set_stream (CcVolumeSlider *self,
+ GvcMixerStream *stream,
+ CcStreamType type)
+{
+ g_return_if_fail (CC_IS_VOLUME_SLIDER (self));
+
+ if (self->stream != NULL)
+ {
+ g_signal_handler_disconnect (self->stream, self->notify_volume_handler_id);
+ self->notify_volume_handler_id = 0;
+ g_signal_handler_disconnect (self->stream, self->notify_is_muted_handler_id);
+ self->notify_is_muted_handler_id = 0;
+ }
+ g_clear_object (&self->stream);
+
+ switch (type)
+ {
+ case CC_STREAM_TYPE_INPUT:
+ gtk_button_set_icon_name (GTK_BUTTON (self->mute_button),
+ "microphone-sensitivity-muted-symbolic");
+ break;
+
+ case CC_STREAM_TYPE_OUTPUT:
+ gtk_button_set_icon_name (GTK_BUTTON (self->mute_button),
+ "audio-volume-muted-symbolic");
+ break;
+
+ default:
+ g_assert_not_reached ();
+ break;
+ }
+
+ if (stream != NULL)
+ {
+ self->stream = g_object_ref (stream);
+
+ self->notify_volume_handler_id = g_signal_connect_object (stream,
+ "notify::volume",
+ G_CALLBACK (notify_volume_cb),
+ self, G_CONNECT_SWAPPED);
+ self->notify_is_muted_handler_id = g_signal_connect_object (stream,
+ "notify::is-muted",
+ G_CALLBACK (notify_is_muted_cb),
+ self, G_CONNECT_SWAPPED);
+ notify_volume_cb (self);
+ notify_is_muted_cb (self);
+ }
+}
+
+GvcMixerStream *
+cc_volume_slider_get_stream (CcVolumeSlider *self)
+{
+ g_return_val_if_fail (CC_IS_VOLUME_SLIDER (self), NULL);
+
+ return self->stream;
+}
+
+void
+cc_volume_slider_set_is_amplified (CcVolumeSlider *self,
+ gboolean is_amplified)
+{
+ g_return_if_fail (CC_IS_VOLUME_SLIDER (self));
+
+ self->is_amplified = is_amplified;
+
+ update_ranges (self);
+}
diff --git a/panels/sound/cc-volume-slider.h b/panels/sound/cc-volume-slider.h
new file mode 100644
index 0000000..b038590
--- /dev/null
+++ b/panels/sound/cc-volume-slider.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2018 Canonical Ltd.
+ *
+ * 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
+ * Lesser 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <pulse/pulseaudio.h>
+#include <gvc-mixer-control.h>
+#include <gvc-mixer-stream.h>
+
+#include "cc-sound-enums.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_VOLUME_SLIDER (cc_volume_slider_get_type ())
+G_DECLARE_FINAL_TYPE (CcVolumeSlider, cc_volume_slider, CC, VOLUME_SLIDER, GtkBox)
+
+void cc_volume_slider_set_mixer_control (CcVolumeSlider *slider,
+ GvcMixerControl *mixer_control);
+
+void cc_volume_slider_set_stream (CcVolumeSlider *slider,
+ GvcMixerStream *stream,
+ CcStreamType type);
+
+void cc_volume_slider_set_is_amplified (CcVolumeSlider *slider,
+ gboolean is_amplified);
+
+GvcMixerStream *cc_volume_slider_get_stream (CcVolumeSlider *slider);
+
+G_END_DECLS
diff --git a/panels/sound/cc-volume-slider.ui b/panels/sound/cc-volume-slider.ui
new file mode 100644
index 0000000..a3de463
--- /dev/null
+++ b/panels/sound/cc-volume-slider.ui
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcVolumeSlider" parent="GtkBox">
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkScale" id="volume_scale">
+ <property name="visible">True</property>
+ <property name="adjustment">volume_adjustment</property>
+ <property name="draw_value">False</property>
+ <property name="hexpand">True</property>
+ <accessibility>
+ <property name="label" translatable="yes">Volume</property>
+ </accessibility>
+ </object>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="mute_button">
+ <property name="visible">True</property>
+ <property name="icon_name">audio-volume-muted-symbolic</property>
+ <accessibility>
+ <property name="label" translatable="yes">Mute</property>
+ </accessibility>
+ <signal name="toggled" handler="mute_button_toggled_cb" object="CcVolumeSlider" swapped="yes"/>
+ <style>
+ <class name="flat" />
+ </style>
+ </object>
+ </child>
+ </template>
+ <object class="GtkAdjustment" id="volume_adjustment">
+ <signal name="value_changed" handler="volume_changed_cb" object="CcVolumeSlider" swapped="yes"/>
+ </object>
+</interface>
diff --git a/panels/sound/gnome-sound-panel.desktop.in.in b/panels/sound/gnome-sound-panel.desktop.in.in
new file mode 100644
index 0000000..8b02861
--- /dev/null
+++ b/panels/sound/gnome-sound-panel.desktop.in.in
@@ -0,0 +1,19 @@
+[Desktop Entry]
+Name=Sound
+Comment=Change sound levels, inputs, outputs, and alert sounds
+Exec=gnome-control-center sound
+# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
+Icon=org.gnome.Settings-sound-symbolic
+Terminal=false
+Type=Application
+NoDisplay=true
+StartupNotify=true
+Categories=GNOME;GTK;Settings;X-GNOME-Settings-Panel;X-GNOME-DevicesSettings;
+OnlyShowIn=GNOME;Unity;
+X-GNOME-Bugzilla-Bugzilla=GNOME
+X-GNOME-Bugzilla-Product=gnome-control-center
+X-GNOME-Bugzilla-Component=sound
+X-GNOME-Bugzilla-Version=@VERSION@
+X-GNOME-Settings-Panel=sound
+# Translators: Search terms to find the Sound panel. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
+Keywords=Card;Microphone;Volume;Fade;Balance;Bluetooth;Headset;Audio;Output;Input;
diff --git a/panels/sound/gvc-mixer-stream-private.h b/panels/sound/gvc-mixer-stream-private.h
new file mode 100644
index 0000000..e9b1552
--- /dev/null
+++ b/panels/sound/gvc-mixer-stream-private.h
@@ -0,0 +1,30 @@
+/* -*- 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#pragma once
+
+#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
diff --git a/panels/sound/icons/audio-speaker-center-back-testing.svg b/panels/sound/icons/audio-speaker-center-back-testing.svg
new file mode 100644
index 0000000..d704b47
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-center-back-testing.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.5"><path d="M41 202h2.485l2.968-3 .547.016v12h-.475L43.485 208H41Z" style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.50000005;marker:none;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0" transform="matrix(0 -2 -2 0 434.063 122)"/><path style="color:#bebebe;display:inline;overflow:visible;visibility:visible;opacity:.1;fill:none;stroke:none;stroke-width:.5;marker:none" d="M41 197h16v16H41z" transform="matrix(0 -2 -2 0 434.063 122)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.50000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M56 205c0-2.814-1-5.172-2.585-7H52v1.48c1.265 1.52 2 3.22 2 5.52 0 2.3-.78 4-2 5.52V212h1.381c1.466-1.64 2.62-4.186 2.62-7z" transform="matrix(0 -2 -2 0 434.063 122)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.50000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M53 205c0-2.166-.739-4.02-2-5h-1v2c.607.789 1 1.76 1 3 0 1.241-.393 2.22-1 3v2h1c1.223-.995 2-2.873 2-5z" transform="matrix(0 -2 -2 0 434.063 122)"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;stroke:none;stroke-width:.50000005;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0" d="M50 205c0-1.257-.311-2.216-1-3h-1v6h1c.672-.837 1-1.742 1-3z" transform="matrix(0 -2 -2 0 434.063 122)"/></g></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-center-back.svg b/panels/sound/icons/audio-speaker-center-back.svg
new file mode 100644
index 0000000..a776f48
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-center-back.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.5"><path d="M41 202h2.485l2.968-3 .547.016v12h-.475L43.485 208H41Z" style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.5;marker:none" transform="matrix(0 -2 -2 0 434.063 122)"/><path style="color:#bebebe;display:inline;overflow:visible;visibility:visible;opacity:.1;fill:none;stroke:none;stroke-width:.5;marker:none" d="M41 197h16v16H41z" transform="matrix(0 -2 -2 0 434.063 122)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.16391027;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M56 205c0-2.814-1-5.172-2.585-7H52v1.48c1.265 1.52 2 3.22 2 5.52 0 2.3-.78 4-2 5.52V212h1.381c1.466-1.64 2.62-4.186 2.62-7z" transform="matrix(0 -2 -2 0 434.063 122)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.16391027;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M53 205c0-2.166-.739-4.02-2-5h-1v2c.607.789 1 1.76 1 3 0 1.241-.393 2.22-1 3v2h1c1.223-.995 2-2.873 2-5z" transform="matrix(0 -2 -2 0 434.063 122)"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;stroke:none;stroke-width:1.16391027;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" d="M50 205c0-1.257-.311-2.216-1-3h-1v6h1c.672-.837 1-1.742 1-3z" transform="matrix(0 -2 -2 0 434.063 122)"/></g></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-center-testing.svg b/panels/sound/icons/audio-speaker-center-testing.svg
new file mode 100644
index 0000000..99b31a2
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-center-testing.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.5"><path d="M41 202h2.485l2.968-3 .547.016v12h-.475L43.485 208H41Z" style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.5;marker:none" transform="matrix(0 2 -2 0 434.063 -74)"/><path style="color:#bebebe;display:inline;overflow:visible;visibility:visible;opacity:.1;fill:none;stroke:none;stroke-width:.5;marker:none" d="M41 197h16v16H41z" transform="matrix(0 2 -2 0 434.063 -74)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M56 205c0-2.814-1-5.172-2.585-7H52v1.48c1.265 1.52 2 3.22 2 5.52 0 2.3-.78 4-2 5.52V212h1.381c1.466-1.64 2.62-4.186 2.62-7z" transform="matrix(0 2 -2 0 434.063 -74)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M53 205c0-2.166-.739-4.02-2-5h-1v2c.607.789 1 1.76 1 3 0 1.241-.393 2.22-1 3v2h1c1.223-.995 2-2.873 2-5z" transform="matrix(0 2 -2 0 434.063 -74)"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;stroke:none;stroke-width:.5;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0" d="M50 205c0-1.257-.311-2.216-1-3h-1v6h1c.672-.837 1-1.742 1-3z" transform="matrix(0 2 -2 0 434.063 -74)"/></g></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-center.svg b/panels/sound/icons/audio-speaker-center.svg
new file mode 100644
index 0000000..1c2f505
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-center.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.5"><path d="M41 202h2.485l2.968-3 .547.016v12h-.475L43.485 208H41Z" style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.5;marker:none" transform="matrix(0 2 -2 0 434.063 -74)"/><path style="color:#bebebe;display:inline;overflow:visible;visibility:visible;opacity:.1;fill:none;stroke:none;stroke-width:.5;marker:none" d="M41 197h16v16H41z" transform="matrix(0 2 -2 0 434.063 -74)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.16391015;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M56 205c0-2.814-1-5.172-2.585-7H52v1.48c1.265 1.52 2 3.22 2 5.52 0 2.3-.78 4-2 5.52V212h1.381c1.466-1.64 2.62-4.186 2.62-7z" transform="matrix(0 2 -2 0 434.063 -74)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.16391015;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M53 205c0-2.166-.739-4.02-2-5h-1v2c.607.789 1 1.76 1 3 0 1.241-.393 2.22-1 3v2h1c1.223-.995 2-2.873 2-5z" transform="matrix(0 2 -2 0 434.063 -74)"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;stroke:none;stroke-width:1.16391015;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" d="M50 205c0-1.257-.311-2.216-1-3h-1v6h1c.672-.837 1-1.742 1-3z" transform="matrix(0 2 -2 0 434.063 -74)"/></g></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-left-back-testing.svg b/panels/sound/icons/audio-speaker-left-back-testing.svg
new file mode 100644
index 0000000..51a5d82
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-left-back-testing.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.5"><path d="M41 202h2.485l2.968-3 .547.016v12h-.475L43.485 208H41Z" style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.5;marker:none" transform="rotate(-45 -404.912 306.25) scale(2)"/><path style="color:#bebebe;display:inline;overflow:visible;visibility:visible;opacity:.1;fill:none;stroke:none;stroke-width:.5;marker:none" d="M41 197h16v16H41z" transform="rotate(-45 -404.912 306.25) scale(2)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.50000004;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M56 205c0-2.814-1-5.172-2.585-7H52v1.48c1.265 1.52 2 3.22 2 5.52 0 2.3-.78 4-2 5.52V212h1.381c1.466-1.64 2.62-4.186 2.62-7z" transform="rotate(-45 -404.912 306.25) scale(2)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.50000004;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M53 205c0-2.166-.739-4.02-2-5h-1v2c.607.789 1 1.76 1 3 0 1.241-.393 2.22-1 3v2h1c1.223-.995 2-2.873 2-5z" transform="rotate(-45 -404.912 306.25) scale(2)"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;stroke:none;stroke-width:.50000004;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0" d="M50 205c0-1.257-.311-2.216-1-3h-1v6h1c.672-.837 1-1.742 1-3z" transform="rotate(-45 -404.912 306.25) scale(2)"/></g></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-left-back.svg b/panels/sound/icons/audio-speaker-left-back.svg
new file mode 100644
index 0000000..6d06375
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-left-back.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.5"><path d="M41 202h2.485l2.968-3 .547.016v12h-.475L43.485 208H41Z" style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.5;marker:none" transform="rotate(-45 -404.912 306.25) scale(2)"/><path style="color:#bebebe;display:inline;overflow:visible;visibility:visible;opacity:.1;fill:none;stroke:none;stroke-width:.5;marker:none" d="M41 197h16v16H41z" transform="rotate(-45 -404.912 306.25) scale(2)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.16391024;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M56 205c0-2.814-1-5.172-2.585-7H52v1.48c1.265 1.52 2 3.22 2 5.52 0 2.3-.78 4-2 5.52V212h1.381c1.466-1.64 2.62-4.186 2.62-7z" transform="rotate(-45 -404.912 306.25) scale(2)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.16391024;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M53 205c0-2.166-.739-4.02-2-5h-1v2c.607.789 1 1.76 1 3 0 1.241-.393 2.22-1 3v2h1c1.223-.995 2-2.873 2-5z" transform="rotate(-45 -404.912 306.25) scale(2)"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;stroke:none;stroke-width:1.16391024;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" d="M50 205c0-1.257-.311-2.216-1-3h-1v6h1c.672-.837 1-1.742 1-3z" transform="rotate(-45 -404.912 306.25) scale(2)"/></g></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-left-side-testing.svg b/panels/sound/icons/audio-speaker-left-side-testing.svg
new file mode 100644
index 0000000..316557e
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-left-side-testing.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.5"><path d="M41 202h2.485l2.968-3 .547.016v12h-.475L43.485 208H41Z" style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.5;marker:none" transform="matrix(2 0 0 2 -73.938 -386)"/><path style="color:#bebebe;display:inline;overflow:visible;visibility:visible;opacity:.1;fill:none;stroke:none;stroke-width:.5;marker:none" d="M41 197h16v16H41z" transform="matrix(2 0 0 2 -73.938 -386)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.50000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M56 205c0-2.814-1-5.172-2.585-7H52v1.48c1.265 1.52 2 3.22 2 5.52 0 2.3-.78 4-2 5.52V212h1.381c1.466-1.64 2.62-4.186 2.62-7z" transform="matrix(2 0 0 2 -73.938 -386)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.50000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M53 205c0-2.166-.739-4.02-2-5h-1v2c.607.789 1 1.76 1 3 0 1.241-.393 2.22-1 3v2h1c1.223-.995 2-2.873 2-5z" transform="matrix(2 0 0 2 -73.938 -386)"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;stroke:none;stroke-width:.50000002;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0" d="M50 205c0-1.257-.311-2.216-1-3h-1v6h1c.672-.837 1-1.742 1-3z" transform="matrix(2 0 0 2 -73.938 -386)"/></g></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-left-side.svg b/panels/sound/icons/audio-speaker-left-side.svg
new file mode 100644
index 0000000..091f3eb
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-left-side.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.5"><path d="M41 202h2.485l2.968-3 .547.016v12h-.475L43.485 208H41Z" style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.5;marker:none" transform="matrix(2 0 0 2 -73.938 -386)"/><path style="color:#bebebe;display:inline;overflow:visible;visibility:visible;opacity:.1;fill:none;stroke:none;stroke-width:.5;marker:none" d="M41 197h16v16H41z" transform="matrix(2 0 0 2 -73.938 -386)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.16391021;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M56 205c0-2.814-1-5.172-2.585-7H52v1.48c1.265 1.52 2 3.22 2 5.52 0 2.3-.78 4-2 5.52V212h1.381c1.466-1.64 2.62-4.186 2.62-7z" transform="matrix(2 0 0 2 -73.938 -386)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.16391021;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M53 205c0-2.166-.739-4.02-2-5h-1v2c.607.789 1 1.76 1 3 0 1.241-.393 2.22-1 3v2h1c1.223-.995 2-2.873 2-5z" transform="matrix(2 0 0 2 -73.938 -386)"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;stroke:none;stroke-width:1.16391021;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" d="M50 205c0-1.257-.311-2.216-1-3h-1v6h1c.672-.837 1-1.742 1-3z" transform="matrix(2 0 0 2 -73.938 -386)"/></g></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-left-testing.svg b/panels/sound/icons/audio-speaker-left-testing.svg
new file mode 100644
index 0000000..a0cbd73
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-left-testing.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.5"><path d="M41 202h2.485l2.968-3 .547.016v12h-.475L43.485 208H41Z" style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.5;marker:none" transform="rotate(45 526.975 127.75) scale(2)"/><path style="color:#bebebe;display:inline;overflow:visible;visibility:visible;opacity:.1;fill:none;stroke:none;stroke-width:.5;marker:none" d="M41 197h16v16H41z" transform="rotate(45 526.975 127.75) scale(2)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.50000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M56 205c0-2.814-1-5.172-2.585-7H52v1.48c1.265 1.52 2 3.22 2 5.52 0 2.3-.78 4-2 5.52V212h1.381c1.466-1.64 2.62-4.186 2.62-7z" transform="rotate(45 526.975 127.75) scale(2)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.50000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M53 205c0-2.166-.739-4.02-2-5h-1v2c.607.789 1 1.76 1 3 0 1.241-.393 2.22-1 3v2h1c1.223-.995 2-2.873 2-5z" transform="rotate(45 526.975 127.75) scale(2)"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;stroke:none;stroke-width:.50000002;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0" d="M50 205c0-1.257-.311-2.216-1-3h-1v6h1c.672-.837 1-1.742 1-3z" transform="rotate(45 526.975 127.75) scale(2)"/></g></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-left.svg b/panels/sound/icons/audio-speaker-left.svg
new file mode 100644
index 0000000..4eef45c
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-left.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.5"><path d="M41 202h2.485l2.968-3 .547.016v12h-.475L43.485 208H41Z" style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.5;marker:none" transform="rotate(45 526.975 127.75) scale(2)"/><path style="color:#bebebe;display:inline;overflow:visible;visibility:visible;opacity:.1;fill:none;stroke:none;stroke-width:.5;marker:none" d="M41 197h16v16H41z" transform="rotate(45 526.975 127.75) scale(2)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.1639102;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M56 205c0-2.814-1-5.172-2.585-7H52v1.48c1.265 1.52 2 3.22 2 5.52 0 2.3-.78 4-2 5.52V212h1.381c1.466-1.64 2.62-4.186 2.62-7z" transform="rotate(45 526.975 127.75) scale(2)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.1639102;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M53 205c0-2.166-.739-4.02-2-5h-1v2c.607.789 1 1.76 1 3 0 1.241-.393 2.22-1 3v2h1c1.223-.995 2-2.873 2-5z" transform="rotate(45 526.975 127.75) scale(2)"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;stroke:none;stroke-width:1.1639102;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" d="M50 205c0-1.257-.311-2.216-1-3h-1v6h1c.672-.837 1-1.742 1-3z" transform="rotate(45 526.975 127.75) scale(2)"/></g></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-mono-testing.svg b/panels/sound/icons/audio-speaker-mono-testing.svg
new file mode 100644
index 0000000..ccd86c6
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-mono-testing.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><path style="color:#000;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate" d="M48.003 24c0 5.628-2 10.343-5.171 14h-2.83v-2.961c2.53-3.039 4-6.44 4-11.039 0-4.6-1.558-8-4-11.039V10h2.763c2.932 3.28 5.238 8.372 5.238 14z"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate" d="M42.003 24c0 4.333-1.478 8.04-4 10h-2v-4c1.213-1.578 2-3.518 2-6s-.787-4.439-2-6v-4h2c2.446 1.99 4 5.747 4 10z"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#729fcf;fill-opacity:.52697096;stroke:none;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate" d="M36.003 24c0 2.515-.624 4.431-2 6h-2V18h2c1.344 1.674 2 3.485 2 6z"/><path d="M.065 24c0 5.628 2 10.343 5.171 14h2.83v-2.961c-2.53-3.039-4-6.44-4-11.039 0-4.6 1.558-8 4-11.039V10H5.302C2.37 13.28.065 18.372.065 24Z" style="color:#000;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate"/><path d="M6.065 24c0 4.333 1.478 8.04 4 10h2v-4c-1.213-1.578-2-3.518-2-6s.787-4.439 2-6v-4h-2c-2.446 1.99-4 5.747-4 10z" style="color:#000;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate"/><path d="M12.065 24c0 2.515.624 4.431 2 6h2V18h-2c-1.344 1.674-2 3.485-2 6z" style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#729fcf;fill-opacity:.52697096;stroke:none;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate"/><circle style="opacity:1;vector-effect:none;fill:#729fcf;fill-opacity:1;stroke:none;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;paint-order:normal" cx="24.02" cy="24.076" r="6.022"/></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-mono.svg b/panels/sound/icons/audio-speaker-mono.svg
new file mode 100644
index 0000000..1e36297
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-mono.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><path style="color:#000;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate" d="M48.003 24c0 5.628-2 10.343-5.171 14h-2.83v-2.961c2.53-3.039 4-6.44 4-11.039 0-4.6-1.558-8-4-11.039V10h2.763c2.932 3.28 5.238 8.372 5.238 14z"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate" d="M42.003 24c0 4.333-1.478 8.04-4 10h-2v-4c1.213-1.578 2-3.518 2-6s-.787-4.439-2-6v-4h2c2.446 1.99 4 5.747 4 10z"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#000;fill-opacity:.23529412;stroke:none;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate" d="M36.003 24c0 2.515-.624 4.431-2 6h-2V18h2c1.344 1.674 2 3.485 2 6z"/><path d="M.065 24c0 5.628 2 10.343 5.171 14h2.83v-2.961c-2.53-3.039-4-6.44-4-11.039 0-4.6 1.558-8 4-11.039V10H5.302C2.37 13.28.065 18.372.065 24Z" style="color:#000;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate"/><path d="M6.065 24c0 4.333 1.478 8.04 4 10h2v-4c-1.213-1.578-2-3.518-2-6s.787-4.439 2-6v-4h-2c-2.446 1.99-4 5.747-4 10z" style="color:#000;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate"/><path d="M12.065 24c0 2.515.624 4.431 2 6h2V18h-2c-1.344 1.674-2 3.485-2 6z" style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#000;fill-opacity:.23529412;stroke:none;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate"/><circle style="opacity:1;vector-effect:none;fill:#000;fill-opacity:1;stroke:none;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;paint-order:normal" cx="24.02" cy="24.076" r="6.022"/></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-right-back-testing.svg b/panels/sound/icons/audio-speaker-right-back-testing.svg
new file mode 100644
index 0000000..62b6056
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-right-back-testing.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.5"><path d="M41 202h2.485l2.968-3 .547.016v12h-.475L43.485 208H41Z" style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.5;marker:none" transform="scale(-2 2) rotate(-45 -214.487 182.171)"/><path style="color:#bebebe;display:inline;overflow:visible;visibility:visible;opacity:.1;fill:none;stroke:none;stroke-width:.5;marker:none" d="M41 197h16v16H41z" transform="scale(-2 2) rotate(-45 -214.487 182.171)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.50000004;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M56 205c0-2.814-1-5.172-2.585-7H52v1.48c1.265 1.52 2 3.22 2 5.52 0 2.3-.78 4-2 5.52V212h1.381c1.466-1.64 2.62-4.186 2.62-7z" transform="scale(-2 2) rotate(-45 -214.487 182.171)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.50000004;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M53 205c0-2.166-.739-4.02-2-5h-1v2c.607.789 1 1.76 1 3 0 1.241-.393 2.22-1 3v2h1c1.223-.995 2-2.873 2-5z" transform="scale(-2 2) rotate(-45 -214.487 182.171)"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;stroke:none;stroke-width:.50000004;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0" d="M50 205c0-1.257-.311-2.216-1-3h-1v6h1c.672-.837 1-1.742 1-3z" transform="scale(-2 2) rotate(-45 -214.487 182.171)"/></g></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-right-back.svg b/panels/sound/icons/audio-speaker-right-back.svg
new file mode 100644
index 0000000..91fbb27
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-right-back.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.5"><path d="M41 202h2.485l2.968-3 .547.016v12h-.475L43.485 208H41Z" style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.5;marker:none" transform="scale(-2 2) rotate(-45 -214.487 182.171)"/><path style="color:#bebebe;display:inline;overflow:visible;visibility:visible;opacity:.1;fill:none;stroke:none;stroke-width:.5;marker:none" d="M41 197h16v16H41z" transform="scale(-2 2) rotate(-45 -214.487 182.171)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.16391024;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M56 205c0-2.814-1-5.172-2.585-7H52v1.48c1.265 1.52 2 3.22 2 5.52 0 2.3-.78 4-2 5.52V212h1.381c1.466-1.64 2.62-4.186 2.62-7z" transform="scale(-2 2) rotate(-45 -214.487 182.171)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.16391024;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M53 205c0-2.166-.739-4.02-2-5h-1v2c.607.789 1 1.76 1 3 0 1.241-.393 2.22-1 3v2h1c1.223-.995 2-2.873 2-5z" transform="scale(-2 2) rotate(-45 -214.487 182.171)"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;stroke:none;stroke-width:1.16391024;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" d="M50 205c0-1.257-.311-2.216-1-3h-1v6h1c.672-.837 1-1.742 1-3z" transform="scale(-2 2) rotate(-45 -214.487 182.171)"/></g></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-right-side-testing.svg b/panels/sound/icons/audio-speaker-right-side-testing.svg
new file mode 100644
index 0000000..ea28b66
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-right-side-testing.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.5"><path d="M41 202h2.485l2.968-3 .547.016v12h-.475L43.485 208H41Z" style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.5;marker:none" transform="matrix(-2 0 0 2 122.063 -386)"/><path style="color:#bebebe;display:inline;overflow:visible;visibility:visible;opacity:.1;fill:none;stroke:none;stroke-width:.5;marker:none" d="M41 197h16v16H41z" transform="matrix(-2 0 0 2 122.063 -386)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.50000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M56 205c0-2.814-1-5.172-2.585-7H52v1.48c1.265 1.52 2 3.22 2 5.52 0 2.3-.78 4-2 5.52V212h1.381c1.466-1.64 2.62-4.186 2.62-7z" transform="matrix(-2 0 0 2 122.063 -386)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.50000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M53 205c0-2.166-.739-4.02-2-5h-1v2c.607.789 1 1.76 1 3 0 1.241-.393 2.22-1 3v2h1c1.223-.995 2-2.873 2-5z" transform="matrix(-2 0 0 2 122.063 -386)"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;stroke:none;stroke-width:.50000002;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0" d="M50 205c0-1.257-.311-2.216-1-3h-1v6h1c.672-.837 1-1.742 1-3z" transform="matrix(-2 0 0 2 122.063 -386)"/></g></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-right-side.svg b/panels/sound/icons/audio-speaker-right-side.svg
new file mode 100644
index 0000000..295eead
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-right-side.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.5"><path d="M41 202h2.485l2.968-3 .547.016v12h-.475L43.485 208H41Z" style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.5;marker:none" transform="matrix(-2 0 0 2 122.063 -386)"/><path style="color:#bebebe;display:inline;overflow:visible;visibility:visible;opacity:.1;fill:none;stroke:none;stroke-width:.5;marker:none" d="M41 197h16v16H41z" transform="matrix(-2 0 0 2 122.063 -386)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.16391021;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M56 205c0-2.814-1-5.172-2.585-7H52v1.48c1.265 1.52 2 3.22 2 5.52 0 2.3-.78 4-2 5.52V212h1.381c1.466-1.64 2.62-4.186 2.62-7z" transform="matrix(-2 0 0 2 122.063 -386)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.16391021;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M53 205c0-2.166-.739-4.02-2-5h-1v2c.607.789 1 1.76 1 3 0 1.241-.393 2.22-1 3v2h1c1.223-.995 2-2.873 2-5z" transform="matrix(-2 0 0 2 122.063 -386)"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;stroke:none;stroke-width:1.16391021;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" d="M50 205c0-1.257-.311-2.216-1-3h-1v6h1c.672-.837 1-1.742 1-3z" transform="matrix(-2 0 0 2 122.063 -386)"/></g></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-right-testing.svg b/panels/sound/icons/audio-speaker-right-testing.svg
new file mode 100644
index 0000000..6449f7b
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-right-testing.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.5"><path d="M41 202h2.485l2.968-3 .547.016v12h-.475L43.485 208H41Z" style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.5;marker:none" transform="scale(-2 2) rotate(45 251.456 34.829)"/><path style="color:#bebebe;display:inline;overflow:visible;visibility:visible;opacity:.1;fill:none;stroke:none;stroke-width:.5;marker:none" d="M41 197h16v16H41z" transform="scale(-2 2) rotate(45 251.456 34.829)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.50000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M56 205c0-2.814-1-5.172-2.585-7H52v1.48c1.265 1.52 2 3.22 2 5.52 0 2.3-.78 4-2 5.52V212h1.381c1.466-1.64 2.62-4.186 2.62-7z" transform="scale(-2 2) rotate(45 251.456 34.829)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.50000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M53 205c0-2.166-.739-4.02-2-5h-1v2c.607.789 1 1.76 1 3 0 1.241-.393 2.22-1 3v2h1c1.223-.995 2-2.873 2-5z" transform="scale(-2 2) rotate(45 251.456 34.829)"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;stroke:none;stroke-width:.50000002;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0" d="M50 205c0-1.257-.311-2.216-1-3h-1v6h1c.672-.837 1-1.742 1-3z" transform="scale(-2 2) rotate(45 251.456 34.829)"/></g></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-right.svg b/panels/sound/icons/audio-speaker-right.svg
new file mode 100644
index 0000000..ce49905
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-right.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.5"><path d="M41 202h2.485l2.968-3 .547.016v12h-.475L43.485 208H41Z" style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.5;marker:none" transform="scale(-2 2) rotate(45 251.456 34.829)"/><path style="color:#bebebe;display:inline;overflow:visible;visibility:visible;opacity:.1;fill:none;stroke:none;stroke-width:.5;marker:none" d="M41 197h16v16H41z" transform="scale(-2 2) rotate(45 251.456 34.829)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.1639102;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M56 205c0-2.814-1-5.172-2.585-7H52v1.48c1.265 1.52 2 3.22 2 5.52 0 2.3-.78 4-2 5.52V212h1.381c1.466-1.64 2.62-4.186 2.62-7z" transform="scale(-2 2) rotate(45 251.456 34.829)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:1.1639102;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M53 205c0-2.166-.739-4.02-2-5h-1v2c.607.789 1 1.76 1 3 0 1.241-.393 2.22-1 3v2h1c1.223-.995 2-2.873 2-5z" transform="scale(-2 2) rotate(45 251.456 34.829)"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000;fill-opacity:.23529412;stroke:none;stroke-width:1.1639102;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" d="M50 205c0-1.257-.311-2.216-1-3h-1v6h1c.672-.837 1-1.742 1-3z" transform="scale(-2 2) rotate(45 251.456 34.829)"/></g></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-speaker-testing.svg b/panels/sound/icons/audio-speaker-testing.svg
new file mode 100644
index 0000000..6449f7b
--- /dev/null
+++ b/panels/sound/icons/audio-speaker-testing.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.5"><path d="M41 202h2.485l2.968-3 .547.016v12h-.475L43.485 208H41Z" style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.5;marker:none" transform="scale(-2 2) rotate(45 251.456 34.829)"/><path style="color:#bebebe;display:inline;overflow:visible;visibility:visible;opacity:.1;fill:none;stroke:none;stroke-width:.5;marker:none" d="M41 197h16v16H41z" transform="scale(-2 2) rotate(45 251.456 34.829)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.50000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M56 205c0-2.814-1-5.172-2.585-7H52v1.48c1.265 1.52 2 3.22 2 5.52 0 2.3-.78 4-2 5.52V212h1.381c1.466-1.64 2.62-4.186 2.62-7z" transform="scale(-2 2) rotate(45 251.456 34.829)"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:.50000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none" d="M53 205c0-2.166-.739-4.02-2-5h-1v2c.607.789 1 1.76 1 3 0 1.241-.393 2.22-1 3v2h1c1.223-.995 2-2.873 2-5z" transform="scale(-2 2) rotate(45 251.456 34.829)"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:.52697096;stroke:none;stroke-width:.50000002;marker:none;enable-background:accumulate;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0" d="M50 205c0-1.257-.311-2.216-1-3h-1v6h1c.672-.837 1-1.742 1-3z" transform="scale(-2 2) rotate(45 251.456 34.829)"/></g></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-subwoofer-testing.svg b/panels/sound/icons/audio-subwoofer-testing.svg
new file mode 100644
index 0000000..a161b1e
--- /dev/null
+++ b/panels/sound/icons/audio-subwoofer-testing.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.50000006"><path style="fill:#729fcf;fill-opacity:1;stroke:none;stroke-width:.75" d="M9.996 12C8.89 12 8 13.046 8 14.313v15.124C8 30.704 8.832 32 9.938 32h2c0 1.266.948 2 2.054 2h1.992c1.106 0 1.954-.734 1.954-2h11.976c0 1.266.933 2 2.04 2h1.987c1.106 0 1.996-.734 1.996-2h2.004c1.106 0 1.996-1.296 1.996-2.563V14.313c0-1.266-.89-2.312-1.996-2.312Zm9.049 2a4.995 4.995 0 0 1 4.99 5c0 2.761-2.234 5-4.99 5a4.995 4.995 0 0 1-4.99-5c0-2.761 2.234-5 4.99-5zm11.97 1.969a3.016 3.016 0 1 1 0 6.031 3.016 3.016 0 0 1 0-6.031ZM19 16a3 3 0 1 0 0 6 3 3 0 0 0 0-6z" transform="translate(0 12)"/></g><path style="color:#000;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:1.00000006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal" d="M25 5c5.628 0 10.343 2 14 5.171V13h-2.961C33 10.47 29.599 9 25 9c-4.6 0-8 1.56-11.039 4H11v-2.763C14.28 7.306 19.372 5 25 5Z"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#729fcf;fill-opacity:.52697096;fill-rule:nonzero;stroke:none;stroke-width:1.00000006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal" d="M25 11c4.333 0 8.04 1.478 10 4v2h-4c-1.578-1.213-3.518-2-6-2s-4.439.787-6 2h-4v-2c1.99-2.446 5.747-4 10-4Z"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#729fcf;fill-opacity:.52697096;stroke:none;stroke-width:1.00000006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;marker:none;enable-background:accumulate;font-variant-east_asian:normal" d="M25 17c2.515 0 4.431.623 6 2v2H19v-2c1.674-1.344 3.485-2 6-2z"/></svg> \ No newline at end of file
diff --git a/panels/sound/icons/audio-subwoofer.svg b/panels/sound/icons/audio-subwoofer.svg
new file mode 100644
index 0000000..8dd0457
--- /dev/null
+++ b/panels/sound/icons/audio-subwoofer.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.0"><defs><linearGradient id="c"><stop style="stop-color:#555753;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient><linearGradient id="b"><stop offset="0" style="stop-color:#2e3436;stop-opacity:1"/><stop style="stop-color:#eeeeec;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop style="stop-color:#babdb6;stop-opacity:1" offset="0"/><stop offset="1" style="stop-color:#eeeeec;stop-opacity:1"/></linearGradient></defs><g style="display:inline;stroke-width:.50000006"><path style="fill:#000;fill-opacity:1;stroke:none;stroke-width:.75" d="M9.996 12C8.89 12 8 13.046 8 14.313v15.124C8 30.704 8.832 32 9.938 32h2c0 1.266.948 2 2.054 2h1.992c1.106 0 1.954-.734 1.954-2h11.976c0 1.266.933 2 2.04 2h1.987c1.106 0 1.996-.734 1.996-2h2.004c1.106 0 1.996-1.296 1.996-2.563V14.313c0-1.266-.89-2.312-1.996-2.312Zm9.049 2a4.995 4.995 0 0 1 4.99 5c0 2.761-2.234 5-4.99 5a4.995 4.995 0 0 1-4.99-5c0-2.761 2.234-5 4.99-5zm11.97 1.969a3.016 3.016 0 1 1 0 6.031 3.016 3.016 0 0 1 0-6.031ZM19 16a3 3 0 1 0 0 6 3 3 0 0 0 0-6z" transform="translate(0 12)"/></g><path style="color:#000;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:2.32782054;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" d="M25 5c5.628 0 10.343 2 14 5.171V13h-2.961C33 10.47 29.599 9 25 9c-4.6 0-8 1.56-11.039 4H11v-2.763C14.28 7.306 19.372 5 25 5Z"/><path style="color:#000;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#000;fill-opacity:.23529412;fill-rule:nonzero;stroke:none;stroke-width:2.32782054;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" d="M25 11c4.333 0 8.04 1.478 10 4v2h-4c-1.578-1.213-3.518-2-6-2s-4.439.787-6 2h-4v-2c1.99-2.446 5.747-4 10-4Z"/><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#000;fill-opacity:.23529412;stroke:none;stroke-width:2.32782054;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" d="M25 17c2.515 0 4.431.623 6 2v2H19v-2c1.674-1.344 3.485-2 6-2z"/></svg> \ No newline at end of file
diff --git a/panels/sound/icons/org.gnome.Settings-sound-symbolic.svg b/panels/sound/icons/org.gnome.Settings-sound-symbolic.svg
new file mode 100644
index 0000000..ddfa354
--- /dev/null
+++ b/panels/sound/icons/org.gnome.Settings-sound-symbolic.svg
@@ -0,0 +1 @@
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M12.04.004a.946.946 0 0 0-.727.281L8 3.578V5h1.39l3.305-3.3c.407-.403.36-1.04.032-1.391a1.026 1.026 0 0 0-.688-.305zM7 1.008a.989.989 0 0 0-.77.351L3 5H2C.906 5 0 5.844 0 7v2c0 1.09.91 2 2 2h1l3.23 3.64c.211.255.493.364.77.36zM8 7v2h6c.75 0 1-.5 1-1s-.25-1-1-1zm0 4v1.422l3.324 3.293c.403.41 1.063.347 1.414-.024a1.02 1.02 0 0 0-.031-1.39L9.391 11zm0 0" fill="#2e3436"/></svg> \ No newline at end of file
diff --git a/panels/sound/meson.build b/panels/sound/meson.build
new file mode 100644
index 0000000..458377d
--- /dev/null
+++ b/panels/sound/meson.build
@@ -0,0 +1,112 @@
+panels_list += cappletname
+desktop = 'gnome-@0@-panel.desktop'.format(cappletname)
+
+desktop_in = configure_file(
+ input: desktop + '.in.in',
+ output: desktop + '.in',
+ configuration: desktop_conf
+)
+
+i18n.merge_file(
+ type: 'desktop',
+ input: desktop_in,
+ output: desktop,
+ po_dir: po_dir,
+ install: true,
+ install_dir: control_center_desktopdir
+)
+
+deps = common_deps + [
+ libgvc_dep,
+ libxml_dep,
+ m_dep,
+ pulse_dep,
+ pulse_mainloop_dep,
+ dependency('gsound'),
+]
+
+cflags += [
+ '-DSOUND_DATA_DIR="@0@"'.format(join_paths(control_center_datadir, 'sounds')),
+]
+
+sources = files(
+ 'cc-alert-chooser.c',
+ 'cc-balance-slider.c',
+ 'cc-device-combo-box.c',
+ 'cc-fade-slider.c',
+ 'cc-level-bar.c',
+ 'cc-output-test-dialog.c',
+ 'cc-profile-combo-box.c',
+ 'cc-sound-panel.c',
+ 'cc-speaker-test-button.c',
+ 'cc-stream-list-box.c',
+ 'cc-stream-row.c',
+ 'cc-subwoofer-slider.c',
+ 'cc-volume-slider.c',
+)
+
+resource_data = files(
+ 'icons/audio-speaker-center-back.svg',
+ 'icons/audio-speaker-center-back-testing.svg',
+ 'icons/audio-speaker-center.svg',
+ 'icons/audio-speaker-center-testing.svg',
+ 'icons/audio-speaker-left-back.svg',
+ 'icons/audio-speaker-left-back-testing.svg',
+ 'icons/audio-speaker-left-side.svg',
+ 'icons/audio-speaker-left-side-testing.svg',
+ 'icons/audio-speaker-left.svg',
+ 'icons/audio-speaker-left-testing.svg',
+ 'icons/audio-speaker-mono.svg',
+ 'icons/audio-speaker-mono-testing.svg',
+ 'icons/audio-speaker-right-back.svg',
+ 'icons/audio-speaker-right-back-testing.svg',
+ 'icons/audio-speaker-right-side.svg',
+ 'icons/audio-speaker-right-side-testing.svg',
+ 'icons/audio-speaker-right.svg',
+ 'icons/audio-speaker-right-testing.svg',
+ 'icons/audio-speaker-testing.svg',
+ 'cc-alert-chooser.ui',
+ 'cc-balance-slider.ui',
+ 'cc-device-combo-box.ui',
+ 'cc-fade-slider.ui',
+ 'cc-output-test-dialog.ui',
+ 'cc-profile-combo-box.ui',
+ 'cc-sound-panel.ui',
+ 'cc-speaker-test-button.ui',
+ 'cc-stream-row.ui',
+ 'cc-subwoofer-slider.ui',
+ 'cc-volume-slider.ui',
+)
+
+sources += gnome.compile_resources(
+ 'cc-' + cappletname + '-resources',
+ cappletname + '.gresource.xml',
+ c_name: 'cc_' + cappletname.underscorify (),
+ dependencies: resource_data,
+ export: true
+)
+
+panels_libs += static_library(
+ cappletname,
+ sources: sources,
+ include_directories: [top_inc, common_inc],
+ dependencies: deps,
+ c_args: cflags,
+)
+
+sound_data = files(
+ 'sounds/click.ogg',
+ 'sounds/hum.ogg',
+ 'sounds/string.ogg',
+ 'sounds/swing.ogg'
+)
+
+install_data(
+ sound_data,
+ install_dir: join_paths(control_center_datadir, 'sounds', 'gnome', 'default', 'alerts')
+)
+
+install_data(
+ 'icons/org.gnome.Settings-sound-symbolic.svg',
+ install_dir: join_paths(control_center_icondir, 'hicolor', 'scalable', 'apps')
+)
diff --git a/panels/sound/sound.gresource.xml b/panels/sound/sound.gresource.xml
new file mode 100644
index 0000000..cdac713
--- /dev/null
+++ b/panels/sound/sound.gresource.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/control-center/sound">
+ <file preprocess="xml-stripblanks">cc-alert-chooser.ui</file>
+ <file preprocess="xml-stripblanks">cc-balance-slider.ui</file>
+ <file preprocess="xml-stripblanks">cc-device-combo-box.ui</file>
+ <file preprocess="xml-stripblanks">cc-fade-slider.ui</file>
+ <file preprocess="xml-stripblanks">cc-output-test-dialog.ui</file>
+ <file preprocess="xml-stripblanks">cc-profile-combo-box.ui</file>
+ <file preprocess="xml-stripblanks">cc-sound-panel.ui</file>
+ <file preprocess="xml-stripblanks">cc-speaker-test-button.ui</file>
+ <file preprocess="xml-stripblanks">cc-stream-list-box.ui</file>
+ <file preprocess="xml-stripblanks">cc-stream-row.ui</file>
+ <file preprocess="xml-stripblanks">cc-subwoofer-slider.ui</file>
+ <file preprocess="xml-stripblanks">cc-volume-slider.ui</file>
+ </gresource>
+ <gresource prefix="/org/gnome/Settings/icons/scalable/devices">
+ <file alias="audio-speaker-center-back.svg">icons/audio-speaker-center-back.svg</file>
+ <file alias="audio-speaker-center-back-testing.svg">icons/audio-speaker-center-back-testing.svg</file>
+ <file alias="audio-speaker-center.svg">icons/audio-speaker-center.svg</file>
+ <file alias="audio-speaker-center-testing.svg">icons/audio-speaker-center-testing.svg</file>
+ <file alias="audio-speaker-left-back.svg">icons/audio-speaker-left-back.svg</file>
+ <file alias="audio-speaker-left-back-testing.svg">icons/audio-speaker-left-back-testing.svg</file>
+ <file alias="audio-speaker-left-side.svg">icons/audio-speaker-left-side.svg</file>
+ <file alias="audio-speaker-left-side-testing.svg">icons/audio-speaker-left-side-testing.svg</file>
+ <file alias="audio-speaker-left.svg">icons/audio-speaker-left.svg</file>
+ <file alias="audio-speaker-left-testing.svg">icons/audio-speaker-left-testing.svg</file>
+ <file alias="audio-speaker-mono.svg">icons/audio-speaker-mono.svg</file>
+ <file alias="audio-speaker-mono-testing.svg">icons/audio-speaker-mono-testing.svg</file>
+ <file alias="audio-speaker-right-back.svg">icons/audio-speaker-right-back.svg</file>
+ <file alias="audio-speaker-right-back-testing.svg">icons/audio-speaker-right-back-testing.svg</file>
+ <file alias="audio-speaker-right-side.svg">icons/audio-speaker-right-side.svg</file>
+ <file alias="audio-speaker-right-side-testing.svg">icons/audio-speaker-right-side-testing.svg</file>
+ <file alias="audio-speaker-right.svg">icons/audio-speaker-right.svg</file>
+ <file alias="audio-speaker-right-testing.svg">icons/audio-speaker-right-testing.svg</file>
+ <file alias="audio-speaker-testing.svg">icons/audio-speaker-testing.svg</file>
+ <file alias="audio-subwoofer.svg">icons/audio-subwoofer.svg</file>
+ <file alias="audio-subwoofer-testing.svg">icons/audio-subwoofer-testing.svg</file>
+ </gresource>
+</gresources>
diff --git a/panels/sound/sounds/click.ogg b/panels/sound/sounds/click.ogg
new file mode 100644
index 0000000..10f4a49
--- /dev/null
+++ b/panels/sound/sounds/click.ogg
Binary files differ
diff --git a/panels/sound/sounds/hum.ogg b/panels/sound/sounds/hum.ogg
new file mode 100644
index 0000000..09a1a7b
--- /dev/null
+++ b/panels/sound/sounds/hum.ogg
Binary files differ
diff --git a/panels/sound/sounds/string.ogg b/panels/sound/sounds/string.ogg
new file mode 100644
index 0000000..7b03130
--- /dev/null
+++ b/panels/sound/sounds/string.ogg
Binary files differ
diff --git a/panels/sound/sounds/swing.ogg b/panels/sound/sounds/swing.ogg
new file mode 100644
index 0000000..4798e61
--- /dev/null
+++ b/panels/sound/sounds/swing.ogg
Binary files differ