summaryrefslogtreecommitdiff
path: root/backends/alsa/alsa-backend.c
diff options
context:
space:
mode:
Diffstat (limited to 'backends/alsa/alsa-backend.c')
-rw-r--r--backends/alsa/alsa-backend.c508
1 files changed, 508 insertions, 0 deletions
diff --git a/backends/alsa/alsa-backend.c b/backends/alsa/alsa-backend.c
new file mode 100644
index 0000000..7a17b85
--- /dev/null
+++ b/backends/alsa/alsa-backend.c
@@ -0,0 +1,508 @@
+/*
+ * Copyright (C) 2014 Michal Ratajsky <[email protected]>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the licence, or (at your option) any later version.
+ *
+ * This library 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 Lesser General Public
+ * License along with this library; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+#include <glib-object.h>
+#include <alsa/asoundlib.h>
+
+#include <libmatemixer/matemixer.h>
+#include <libmatemixer/matemixer-private.h>
+
+#include "alsa-backend.h"
+#include "alsa-device.h"
+#include "alsa-stream.h"
+
+#define BACKEND_NAME "ALSA"
+#define BACKEND_PRIORITY 9
+
+struct _AlsaBackendPrivate
+{
+ GSource *timeout_source;
+ GHashTable *devices;
+ GHashTable *devices_ids;
+};
+
+static void alsa_backend_class_init (AlsaBackendClass *klass);
+static void alsa_backend_class_finalize (AlsaBackendClass *klass);
+static void alsa_backend_init (AlsaBackend *alsa);
+static void alsa_backend_dispose (GObject *object);
+static void alsa_backend_finalize (GObject *object);
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunused-function"
+G_DEFINE_DYNAMIC_TYPE (AlsaBackend, alsa_backend, MATE_MIXER_TYPE_BACKEND)
+#pragma clang diagnostic pop
+
+static gboolean alsa_backend_open (MateMixerBackend *backend);
+static void alsa_backend_close (MateMixerBackend *backend);
+static GList * alsa_backend_list_devices (MateMixerBackend *backend);
+static GList * alsa_backend_list_streams (MateMixerBackend *backend);
+
+static gboolean read_devices (AlsaBackend *alsa);
+
+static gboolean read_device (AlsaBackend *alsa,
+ const gchar *card);
+
+static void add_device (AlsaBackend *alsa,
+ AlsaDevice *device);
+
+static void remove_device (AlsaBackend *alsa,
+ AlsaDevice *device);
+static void remove_stream (AlsaBackend *alsa,
+ const gchar *name);
+
+static void select_default_input_stream (AlsaBackend *alsa);
+static void select_default_output_stream (AlsaBackend *alsa);
+
+static MateMixerBackendInfo info;
+
+void
+backend_module_init (GTypeModule *module)
+{
+ alsa_backend_register_type (module);
+
+ info.name = BACKEND_NAME;
+ info.priority = BACKEND_PRIORITY;
+ info.g_type = ALSA_TYPE_BACKEND;
+ info.backend_type = MATE_MIXER_BACKEND_ALSA;
+}
+
+const MateMixerBackendInfo *backend_module_get_info (void)
+{
+ return &info;
+}
+
+static void
+alsa_backend_class_init (AlsaBackendClass *klass)
+{
+ GObjectClass *object_class;
+ MateMixerBackendClass *backend_class;
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = alsa_backend_dispose;
+ object_class->finalize = alsa_backend_finalize;
+
+ backend_class = MATE_MIXER_BACKEND_CLASS (klass);
+ backend_class->open = alsa_backend_open;
+ backend_class->close = alsa_backend_close;
+ backend_class->list_devices = alsa_backend_list_devices;
+ backend_class->list_streams = alsa_backend_list_streams;
+
+ g_type_class_add_private (object_class, sizeof (AlsaBackendPrivate));
+}
+
+/* Called in the code generated by G_DEFINE_DYNAMIC_TYPE() */
+static void
+alsa_backend_class_finalize (AlsaBackendClass *klass)
+{
+}
+
+static void
+alsa_backend_init (AlsaBackend *alsa)
+{
+ alsa->priv = G_TYPE_INSTANCE_GET_PRIVATE (alsa,
+ ALSA_TYPE_BACKEND,
+ AlsaBackendPrivate);
+
+ alsa->priv->devices = g_hash_table_new_full (g_str_hash,
+ g_str_equal,
+ g_free,
+ g_object_unref);
+
+ alsa->priv->devices_ids = g_hash_table_new_full (g_str_hash,
+ g_str_equal,
+ g_free,
+ NULL);
+}
+
+static void
+alsa_backend_dispose (GObject *object)
+{
+ MateMixerBackend *backend;
+ MateMixerState state;
+
+ backend = MATE_MIXER_BACKEND (object);
+
+ state = mate_mixer_backend_get_state (backend);
+ if (state != MATE_MIXER_STATE_IDLE)
+ alsa_backend_close (backend);
+
+ G_OBJECT_CLASS (alsa_backend_parent_class)->dispose (object);
+}
+
+static void
+alsa_backend_finalize (GObject *object)
+{
+ AlsaBackend *alsa;
+
+ alsa = ALSA_BACKEND (object);
+
+ g_hash_table_unref (alsa->priv->devices);
+ g_hash_table_unref (alsa->priv->devices_ids);
+
+ G_OBJECT_CLASS (alsa_backend_parent_class)->finalize (object);
+}
+
+static gboolean
+alsa_backend_open (MateMixerBackend *backend)
+{
+ AlsaBackend *alsa;
+
+ g_return_val_if_fail (ALSA_IS_BACKEND (backend), FALSE);
+
+ alsa = ALSA_BACKEND (backend);
+
+ /* Poll ALSA for changes every 500 milliseconds, this actually only
+ * discovers added or changed sound cards, sound card related events
+ * are handled by AlsaDevices */
+ alsa->priv->timeout_source = g_timeout_source_new_seconds (1);
+ g_source_set_callback (alsa->priv->timeout_source,
+ (GSourceFunc) read_devices,
+ alsa,
+ NULL);
+ g_source_attach (alsa->priv->timeout_source,
+ g_main_context_get_thread_default ());
+
+ /* Read the initial list of devices so we have some starting point, there
+ * isn't really a way to detect errors here, failing to add a device may
+ * be a device-related problem so make the backend always open successfully */
+ read_devices (alsa);
+
+ _mate_mixer_backend_set_state (backend, MATE_MIXER_STATE_READY);
+ return TRUE;
+}
+
+void
+alsa_backend_close (MateMixerBackend *backend)
+{
+ AlsaBackend *alsa;
+
+ g_return_if_fail (ALSA_IS_BACKEND (backend));
+
+ alsa = ALSA_BACKEND (backend);
+
+ g_source_destroy (alsa->priv->timeout_source);
+
+ g_hash_table_remove_all (alsa->priv->devices);
+ g_hash_table_remove_all (alsa->priv->devices_ids);
+
+ _mate_mixer_backend_set_state (backend, MATE_MIXER_STATE_IDLE);
+}
+
+static GList *
+alsa_backend_list_devices (MateMixerBackend *backend)
+{
+ GList *list;
+
+ g_return_val_if_fail (ALSA_IS_BACKEND (backend), NULL);
+
+ /* Convert the hash table to a linked list, this list is expected to be
+ * cached in the main library */
+ list = g_hash_table_get_values (ALSA_BACKEND (backend)->priv->devices);
+ if (list != NULL)
+ g_list_foreach (list, (GFunc) g_object_ref, NULL);
+
+ return list;
+}
+
+static GList *
+alsa_backend_list_streams (MateMixerBackend *backend)
+{
+ AlsaBackend *alsa;
+ GHashTableIter iter;
+ gpointer value;
+ GList *list = NULL;
+
+ g_return_val_if_fail (ALSA_IS_BACKEND (backend), NULL);
+
+ alsa = ALSA_BACKEND (backend);
+
+ /* We don't keep a list or hash table of all streams here, instead walk
+ * through the list of devices and create the list manually, each device
+ * has at most one input and one output stream */
+ g_hash_table_iter_init (&iter, alsa->priv->devices);
+
+ while (g_hash_table_iter_next (&iter, NULL, &value)) {
+ AlsaDevice *device = ALSA_DEVICE (value);
+ AlsaStream *stream;
+
+ stream = alsa_device_get_output_stream (device);
+ if (stream != NULL)
+ list = g_list_prepend (list, stream);
+ stream = alsa_device_get_input_stream (device);
+ if (stream != NULL)
+ list = g_list_prepend (list, stream);
+ }
+
+ if (list != NULL)
+ g_list_foreach (list, (GFunc) g_object_ref, NULL);
+
+ return list;
+}
+
+static gboolean
+read_devices (AlsaBackend *alsa)
+{
+ gint num;
+ gint ret;
+ gchar card[16];
+ gboolean changed = FALSE;
+
+ /* Read the default device first, it will be either one of the hardware cards
+ * that will be queried later, or a software mixer */
+ if (read_device (alsa, "default") == TRUE)
+ changed = TRUE;
+
+ for (num = -1;;) {
+ /* Read number of the next sound card */
+ ret = snd_card_next (&num);
+ if (ret < 0 ||
+ num < 0)
+ break;
+
+ g_snprintf (card, sizeof (card), "hw:%d", num);
+
+ if (read_device (alsa, card) == TRUE)
+ changed = TRUE;
+ }
+
+ /* If any card has been added, make sure we have the most suitable default
+ * input and output streams */
+ if (changed == TRUE) {
+ select_default_input_stream (alsa);
+ select_default_output_stream (alsa);
+ }
+ return G_SOURCE_CONTINUE;
+}
+
+static gboolean
+read_device (AlsaBackend *alsa, const gchar *card)
+{
+ AlsaDevice *device;
+ snd_ctl_t *ctl;
+ snd_ctl_card_info_t *info;
+ const gchar *id;
+ gint ret;
+
+ /* The device may be already known, remove it if it's known and fails
+ * to be read, this happens for example when PulseAudio is killed */
+ device = g_hash_table_lookup (alsa->priv->devices, card);
+
+ ret = snd_ctl_open (&ctl, card, 0);
+ if (ret < 0) {
+ g_warning ("Failed to open ALSA control for %s: %s",
+ card,
+ snd_strerror (ret));
+ if (device != NULL)
+ remove_device (alsa, device);
+ return FALSE;
+ }
+
+ snd_ctl_card_info_alloca (&info);
+
+ ret = snd_ctl_card_info (ctl, info);
+ if (ret < 0) {
+ g_warning ("Failed to read card info: %s", snd_strerror (ret));
+ if (device != NULL)
+ remove_device (alsa, device);
+
+ snd_ctl_close (ctl);
+ return FALSE;
+ }
+
+ id = snd_ctl_card_info_get_id (info);
+
+ /* We also keep a list of device identifiers to be sure no card is
+ * added twice, this could commonly happen because some card may
+ * also be assigned to the "default" ALSA device */
+ if (g_hash_table_contains (alsa->priv->devices_ids, id) == TRUE) {
+ snd_ctl_close (ctl);
+ return FALSE;
+ }
+
+ device = alsa_device_new (card, snd_ctl_card_info_get_name (info));
+
+ if (alsa_device_open (device) == FALSE) {
+ g_object_unref (device);
+ snd_ctl_close (ctl);
+ return FALSE;
+ }
+
+ g_object_set_data_full (G_OBJECT (device),
+ "__matemixer_alsa_device_id",
+ g_strdup (id),
+ g_free);
+
+ add_device (alsa, device);
+
+ snd_ctl_close (ctl);
+ return TRUE;
+}
+
+static void
+add_device (AlsaBackend *alsa, AlsaDevice *device)
+{
+ const gchar *name;
+
+ name = mate_mixer_device_get_name (MATE_MIXER_DEVICE (device));
+
+ g_hash_table_insert (alsa->priv->devices,
+ g_strdup (name),
+ g_object_ref (device));
+
+ /* Remember the device identifier, use a single string copy as we only free
+ * the hash table key */
+ g_hash_table_add (alsa->priv->devices_ids,
+ g_strdup (g_object_get_data (G_OBJECT (device),
+ "__matemixer_alsa_device_id")));
+
+ g_signal_connect_swapped (G_OBJECT (device),
+ "closed",
+ G_CALLBACK (remove_device),
+ alsa);
+ g_signal_connect_swapped (G_OBJECT (device),
+ "stream-removed",
+ G_CALLBACK (remove_stream),
+ alsa);
+
+ g_signal_emit_by_name (G_OBJECT (alsa), "device-added", name);
+
+ /* Load the device elements after emitting device-added, because the load
+ * function will most likely emit stream-added on the device and backend */
+ alsa_device_load (device);
+}
+
+static void
+remove_device (AlsaBackend *alsa, AlsaDevice *device)
+{
+ const gchar *name;
+
+ name = mate_mixer_device_get_name (MATE_MIXER_DEVICE (device));
+
+ g_signal_handlers_disconnect_by_func (G_OBJECT (device),
+ G_CALLBACK (remove_device),
+ alsa);
+ g_signal_handlers_disconnect_by_func (G_OBJECT (device),
+ G_CALLBACK (remove_stream),
+ alsa);
+
+ /* Remove the device */
+ g_hash_table_remove (alsa->priv->devices_ids,
+ g_object_get_data (G_OBJECT (device),
+ "__matemixer_alsa_device_id"));
+
+ // XXX close the device and make it remove streams
+ g_hash_table_remove (alsa->priv->devices, name);
+ g_signal_emit_by_name (G_OBJECT (alsa),
+ "device-removed",
+ name);
+}
+
+static void
+remove_stream (AlsaBackend *alsa, const gchar *name)
+{
+ MateMixerStream *stream;
+
+ stream = mate_mixer_backend_get_default_input_stream (MATE_MIXER_BACKEND (alsa));
+
+ // XXX see if the change happens after stream is removed or before
+ if (stream != NULL && strcmp (mate_mixer_stream_get_name (stream), name) == 0)
+ select_default_input_stream (alsa);
+
+ stream = mate_mixer_backend_get_default_output_stream (MATE_MIXER_BACKEND (alsa));
+
+ if (stream != NULL && strcmp (mate_mixer_stream_get_name (stream), name) == 0)
+ select_default_output_stream (alsa);
+}
+
+static void
+select_default_input_stream (AlsaBackend *alsa)
+{
+ AlsaDevice *device;
+ AlsaStream *stream;
+ gchar card[16];
+ gint num;
+
+ /* Always prefer stream in the "default" device */
+ device = g_hash_table_lookup (alsa->priv->devices, "default");
+ if (device != NULL) {
+ stream = alsa_device_get_input_stream (device);
+ if (stream != NULL) {
+ _mate_mixer_backend_set_default_input_stream (MATE_MIXER_BACKEND (alsa),
+ MATE_MIXER_STREAM (stream));
+ return;
+ }
+ }
+
+ /* If there is no input stream in the default device, search the cards in
+ * the correct order */
+ for (num = 0;; num++) {
+ g_snprintf (card, sizeof (card), "hw:%d", num);
+
+ device = g_hash_table_lookup (alsa->priv->devices, card);
+ if (device == NULL)
+ break;
+ stream = alsa_device_get_input_stream (device);
+ if (stream != NULL) {
+ _mate_mixer_backend_set_default_input_stream (MATE_MIXER_BACKEND (alsa),
+ MATE_MIXER_STREAM (stream));
+ return;
+ }
+ }
+
+ /* In the worst case unset the default stream */
+ _mate_mixer_backend_set_default_input_stream (MATE_MIXER_BACKEND (alsa), NULL);
+}
+
+static void
+select_default_output_stream (AlsaBackend *alsa)
+{
+ AlsaDevice *device;
+ AlsaStream *stream;
+ gchar card[16];
+ gint num;
+
+ /* Always prefer stream in the "default" device */
+ device = g_hash_table_lookup (alsa->priv->devices, "default");
+ if (device != NULL) {
+ stream = alsa_device_get_output_stream (device);
+ if (stream != NULL) {
+ _mate_mixer_backend_set_default_output_stream (MATE_MIXER_BACKEND (alsa),
+ MATE_MIXER_STREAM (stream));
+ return;
+ }
+ }
+
+ /* If there is no input stream in the default device, search the cards in
+ * the correct order */
+ for (num = 0;; num++) {
+ g_snprintf (card, sizeof (card), "hw:%d", num);
+
+ device = g_hash_table_lookup (alsa->priv->devices, card);
+ if (device == NULL)
+ break;
+ stream = alsa_device_get_output_stream (device);
+ if (stream != NULL) {
+ _mate_mixer_backend_set_default_output_stream (MATE_MIXER_BACKEND (alsa),
+ MATE_MIXER_STREAM (stream));
+ return;
+ }
+ }
+
+ /* In the worst case unset the default stream */
+ _mate_mixer_backend_set_default_output_stream (MATE_MIXER_BACKEND (alsa), NULL);
+}