diff options
Diffstat (limited to 'backends/alsa/alsa-backend.c')
-rw-r--r-- | backends/alsa/alsa-backend.c | 508 |
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); +} |