/* * Copyright (C) 2014 Michal Ratajsky * * 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 . */ #include "config.h" #include #include #include #ifdef HAVE_UDEV #include #include #endif #include #include #include "alsa-backend.h" #include "alsa-device.h" #include "alsa-stream.h" #define BACKEND_NAME "ALSA" #define BACKEND_PRIORITY 20 #define BACKEND_FLAGS MATE_MIXER_BACKEND_NO_FLAGS #define ALSA_DEVICE_GET_ID(d) \ (g_object_get_data (G_OBJECT (d), "__matemixer_alsa_device_id")) #define ALSA_DEVICE_SET_ID(d,id) \ (g_object_set_data_full (G_OBJECT (d), \ "__matemixer_alsa_device_id", \ g_strdup (id), \ g_free)) struct _AlsaBackendPrivate { GSource *timeout_source; GList *streams; GList *devices; GHashTable *devices_ids; #ifdef HAVE_UDEV struct { struct udev *udev; struct udev_monitor *monitor; guint fd_source; int fd; } udev; #endif }; static void alsa_backend_dispose (GObject *object); static void alsa_backend_finalize (GObject *object); G_DEFINE_DYNAMIC_TYPE_EXTENDED (AlsaBackend, alsa_backend, MATE_MIXER_TYPE_BACKEND, 0, G_ADD_PRIVATE_DYNAMIC(AlsaBackend)) static gboolean alsa_backend_open (MateMixerBackend *backend); static void alsa_backend_close (MateMixerBackend *backend); static const GList *alsa_backend_list_devices (MateMixerBackend *backend); static const 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_device_by_name (AlsaBackend *alsa, const gchar *name); static void remove_device_by_list_item (AlsaBackend *alsa, GList *item); 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 void free_stream_list (AlsaBackend *alsa); static gint compare_devices (gconstpointer a, gconstpointer b); static gint compare_device_name (gconstpointer a, gconstpointer b); 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_flags = BACKEND_FLAGS; 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; } /* 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 = alsa_backend_get_instance_private (alsa); 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_ids); G_OBJECT_CLASS (alsa_backend_parent_class)->finalize (object); } #ifdef HAVE_UDEV static gboolean udev_event_ok (struct udev_device *dev) { const char *action = udev_device_get_action (dev); if (action && !strcmp (action, "remove")) return TRUE; /* * See /lib/udev/rules.d/78-sound-card.rules * why "add" events are not handled here. */ if ((!action || !strcmp (action, "change")) && udev_device_get_property_value (dev, "SOUND_INITIALIZED")) return TRUE; return FALSE; } static gboolean udev_monitor_cb (gint fd, GIOCondition condition, gpointer user_data) { AlsaBackend *alsa = user_data; struct udev_device *dev; dev = udev_monitor_receive_device (alsa->priv->udev.monitor); if (!dev) return TRUE; if (!udev_event_ok (dev)) { udev_device_unref (dev); return TRUE; } read_devices (alsa); udev_device_unref (dev); return TRUE; } static gboolean udev_monitor_setup (AlsaBackend *alsa) { alsa->priv->udev.udev = udev_new (); if (!alsa->priv->udev.udev) return FALSE; alsa->priv->udev.monitor = udev_monitor_new_from_netlink (alsa->priv->udev.udev, "udev"); if (!alsa->priv->udev.monitor) { udev_unref (alsa->priv->udev.udev); alsa->priv->udev.udev = NULL; return FALSE; } if (udev_monitor_filter_add_match_subsystem_devtype (alsa->priv->udev.monitor, "sound", NULL) < 0 || udev_monitor_enable_receiving (alsa->priv->udev.monitor) < 0) { udev_monitor_unref (alsa->priv->udev.monitor); udev_unref (alsa->priv->udev.udev); alsa->priv->udev.udev = NULL; return FALSE; } alsa->priv->udev.fd = udev_monitor_get_fd (alsa->priv->udev.monitor); if (alsa->priv->udev.fd < 0) { udev_monitor_unref (alsa->priv->udev.monitor); udev_unref (alsa->priv->udev.udev); alsa->priv->udev.udev = NULL; return FALSE; } alsa->priv->udev.fd_source = g_unix_fd_add (alsa->priv->udev.fd, G_IO_IN, udev_monitor_cb, alsa); return TRUE; } static void udev_monitor_cleanup (AlsaBackend *alsa) { if (!alsa->priv->udev.udev) return; g_source_remove (alsa->priv->udev.fd_source); udev_monitor_unref (alsa->priv->udev.monitor); udev_unref (alsa->priv->udev.udev); } #endif static void timeout_source_setup (AlsaBackend *alsa) { /* Poll ALSA for changes every second, this only discovers added or removed * 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 ()); } static void timeout_source_cleanup (AlsaBackend *alsa) { if (!alsa->priv->timeout_source) return; g_source_destroy (alsa->priv->timeout_source); } static gboolean alsa_backend_open (MateMixerBackend *backend) { AlsaBackend *alsa; g_return_val_if_fail (ALSA_IS_BACKEND (backend), FALSE); alsa = ALSA_BACKEND (backend); #ifdef HAVE_UDEV if (!udev_monitor_setup (alsa)) #endif timeout_source_setup (alsa); /* 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); timeout_source_cleanup (alsa); #ifdef HAVE_UDEV udev_monitor_cleanup (alsa); #endif if (alsa->priv->devices != NULL) { g_list_free_full (alsa->priv->devices, g_object_unref); alsa->priv->devices = NULL; } free_stream_list (alsa); g_hash_table_remove_all (alsa->priv->devices_ids); _mate_mixer_backend_set_state (backend, MATE_MIXER_STATE_IDLE); } static const GList * alsa_backend_list_devices (MateMixerBackend *backend) { g_return_val_if_fail (ALSA_IS_BACKEND (backend), NULL); return ALSA_BACKEND (backend)->priv->devices; } static const GList * alsa_backend_list_streams (MateMixerBackend *backend) { AlsaBackend *alsa; g_return_val_if_fail (ALSA_IS_BACKEND (backend), NULL); alsa = ALSA_BACKEND (backend); if (alsa->priv->streams == NULL) { GList *list; /* Walk through the list of devices and create the stream list, each * device has at most one input and one output stream */ list = g_list_last (alsa->priv->devices); while (list != NULL) { AlsaDevice *device = ALSA_DEVICE (list->data); AlsaStream *stream; stream = alsa_device_get_output_stream (device); if (stream != NULL) alsa->priv->streams = g_list_prepend (alsa->priv->streams, g_object_ref (stream)); stream = alsa_device_get_input_stream (device); if (stream != NULL) alsa->priv->streams = g_list_prepend (alsa->priv->streams, g_object_ref (stream)); list = list->prev; } } return alsa->priv->streams; } static gboolean read_devices (AlsaBackend *alsa) { gint num; gint ret; gchar card[16]; gboolean added = 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) added = 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) added = TRUE; } /* If any card has been added, make sure we have the most suitable default * input and output streams */ if (added == 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. * * Make sure it is removed from the list of devices if it fails to be * read. This commonly happens with the "default" device, which is not * reassigned by ALSA when the sound card is removed or the sound mixer * quits. */ ret = snd_ctl_open (&ctl, card, 0); if (ret < 0) { remove_device_by_name (alsa, card); 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)); remove_device_by_name (alsa, card); 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; } ALSA_DEVICE_SET_ID (device, id); add_device (alsa, device); snd_ctl_close (ctl); return TRUE; } static void add_device (AlsaBackend *alsa, AlsaDevice *device) { /* Takes reference of device */ alsa->priv->devices = g_list_insert_sorted_with_data (alsa->priv->devices, device, (GCompareDataFunc) compare_devices, NULL); /* Keep track of device identifiers */ g_hash_table_add (alsa->priv->devices_ids, g_strdup (ALSA_DEVICE_GET_ID (device))); 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_connect_swapped (G_OBJECT (device), "closed", G_CALLBACK (free_stream_list), alsa); g_signal_connect_swapped (G_OBJECT (device), "stream-added", G_CALLBACK (free_stream_list), alsa); g_signal_connect_swapped (G_OBJECT (device), "stream-removed", G_CALLBACK (free_stream_list), alsa); g_signal_emit_by_name (G_OBJECT (alsa), "device-added", mate_mixer_device_get_name (MATE_MIXER_DEVICE (device))); /* 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) { GList *item; item = g_list_find (alsa->priv->devices, device); if (item != NULL) remove_device_by_list_item (alsa, item); } static void remove_device_by_name (AlsaBackend *alsa, const gchar *name) { GList *item; item = g_list_find_custom (alsa->priv->devices, name, compare_device_name); if (item != NULL) remove_device_by_list_item (alsa, item); } static void remove_device_by_list_item (AlsaBackend *alsa, GList *item) { AlsaDevice *device; device = ALSA_DEVICE (item->data); g_signal_handlers_disconnect_by_func (G_OBJECT (device), G_CALLBACK (remove_device), alsa); /* May emit removed signals */ if (alsa_device_is_open (device) == TRUE) alsa_device_close (device); g_signal_handlers_disconnect_by_data (G_OBJECT (device), alsa); alsa->priv->devices = g_list_delete_link (alsa->priv->devices, item); g_hash_table_remove (alsa->priv->devices_ids, ALSA_DEVICE_GET_ID (device)); /* The list may have been invalidated by device signals */ free_stream_list (alsa); g_signal_emit_by_name (G_OBJECT (alsa), "device-removed", mate_mixer_device_get_name (MATE_MIXER_DEVICE (device))); g_object_unref (device); } static void remove_stream (AlsaBackend *alsa, const gchar *name) { MateMixerStream *stream; stream = mate_mixer_backend_get_default_input_stream (MATE_MIXER_BACKEND (alsa)); 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) { GList *list; list = alsa->priv->devices; while (list != NULL) { AlsaDevice *device = ALSA_DEVICE (list->data); AlsaStream *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; } list = list->next; } /* 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) { GList *list; list = alsa->priv->devices; while (list != NULL) { AlsaDevice *device = ALSA_DEVICE (list->data); AlsaStream *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; } list = list->next; } /* In the worst case unset the default stream */ _mate_mixer_backend_set_default_output_stream (MATE_MIXER_BACKEND (alsa), NULL); } static void free_stream_list (AlsaBackend *alsa) { if (alsa->priv->streams == NULL) return; g_list_free_full (alsa->priv->streams, g_object_unref); alsa->priv->streams = NULL; } static gint compare_devices (gconstpointer a, gconstpointer b) { MateMixerDevice *d1 = MATE_MIXER_DEVICE (a); MateMixerDevice *d2 = MATE_MIXER_DEVICE (b); return strcmp (mate_mixer_device_get_name (d1), mate_mixer_device_get_name (d2)); } static gint compare_device_name (gconstpointer a, gconstpointer b) { MateMixerDevice *device = MATE_MIXER_DEVICE (a); const gchar *name = (const gchar *) b; return strcmp (mate_mixer_device_get_name (device), name); }