diff options
Diffstat (limited to 'backends/oss/oss-backend.c')
-rw-r--r-- | backends/oss/oss-backend.c | 587 |
1 files changed, 587 insertions, 0 deletions
diff --git a/backends/oss/oss-backend.c b/backends/oss/oss-backend.c new file mode 100644 index 0000000..2b5eca7 --- /dev/null +++ b/backends/oss/oss-backend.c @@ -0,0 +1,587 @@ +/* + * 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 <stdio.h> +#include <string.h> +#include <errno.h> +#include <glib.h> +#include <glib/gstdio.h> +#include <glib/gi18n.h> +#include <glib-object.h> + +#include <libmatemixer/matemixer.h> +#include <libmatemixer/matemixer-private.h> + +#include "oss-backend.h" +#include "oss-common.h" +#include "oss-device.h" +#include "oss-stream.h" + +#define BACKEND_NAME "OSS" +#define BACKEND_PRIORITY 9 + +#if !defined(__linux__) && !defined(__NetBSD__) && !defined(__OpenBSD__) + /* At least on systems based on FreeBSD we will need to read device names + * from the sndstat file, but avoid even trying that on systems where this + * is not needed and the file is not present */ +#define OSS_PATH_SNDSTAT "/dev/sndstat" +#endif + +#define OSS_MAX_DEVICES 32 + +struct _OssBackendPrivate +{ + gchar *default_device; + GSource *timeout_source; + GHashTable *devices; +}; + +static void oss_backend_class_init (OssBackendClass *klass); +static void oss_backend_class_finalize (OssBackendClass *klass); +static void oss_backend_init (OssBackend *oss); +static void oss_backend_dispose (GObject *object); +static void oss_backend_finalize (GObject *object); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-function" +G_DEFINE_DYNAMIC_TYPE (OssBackend, oss_backend, MATE_MIXER_TYPE_BACKEND) +#pragma clang diagnostic pop + +static gboolean oss_backend_open (MateMixerBackend *backend); +static void oss_backend_close (MateMixerBackend *backend); +static GList * oss_backend_list_devices (MateMixerBackend *backend); +static GList * oss_backend_list_streams (MateMixerBackend *backend); + +static gboolean read_devices (OssBackend *oss); + +static gboolean read_device (OssBackend *oss, + const gchar *path, + gboolean *added); + +static gchar * read_device_label (OssBackend *oss, + const gchar *path, + gint fd); + +static gchar * read_device_label_sndstat (OssBackend *oss, + const gchar *sndstat, + const gchar *path, + guint index) G_GNUC_UNUSED; + +static void add_device (OssBackend *oss, + OssDevice *device); +static void remove_device (OssBackend *oss, + OssDevice *device); + +static void remove_stream (OssBackend *oss, + const gchar *name); + +static void select_default_input_stream (OssBackend *oss); +static void select_default_output_stream (OssBackend *oss); + +static MateMixerBackendInfo info; + +void +backend_module_init (GTypeModule *module) +{ + oss_backend_register_type (module); + + info.name = BACKEND_NAME; + info.priority = BACKEND_PRIORITY; + info.g_type = OSS_TYPE_BACKEND; + info.backend_type = MATE_MIXER_BACKEND_OSS; +} + +const MateMixerBackendInfo *backend_module_get_info (void) +{ + return &info; +} + +static void +oss_backend_class_init (OssBackendClass *klass) +{ + GObjectClass *object_class; + MateMixerBackendClass *backend_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->dispose = oss_backend_dispose; + object_class->finalize = oss_backend_finalize; + + backend_class = MATE_MIXER_BACKEND_CLASS (klass); + backend_class->open = oss_backend_open; + backend_class->close = oss_backend_close; + backend_class->list_devices = oss_backend_list_devices; + backend_class->list_streams = oss_backend_list_streams; + + g_type_class_add_private (object_class, sizeof (OssBackendPrivate)); +} + +/* Called in the code generated by G_DEFINE_DYNAMIC_TYPE() */ +static void +oss_backend_class_finalize (OssBackendClass *klass) +{ +} + +static void +oss_backend_init (OssBackend *oss) +{ + oss->priv = G_TYPE_INSTANCE_GET_PRIVATE (oss, + OSS_TYPE_BACKEND, + OssBackendPrivate); + + oss->priv->devices = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + g_object_unref); +} + +static void +oss_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) + oss_backend_close (backend); + + G_OBJECT_CLASS (oss_backend_parent_class)->dispose (object); +} + +static void +oss_backend_finalize (GObject *object) +{ + OssBackend *oss; + + oss = OSS_BACKEND (object); + + g_hash_table_destroy (oss->priv->devices); + + G_OBJECT_CLASS (oss_backend_parent_class)->finalize (object); +} + +static gboolean +oss_backend_open (MateMixerBackend *backend) +{ + OssBackend *oss; + + g_return_val_if_fail (OSS_IS_BACKEND (backend), FALSE); + + oss = OSS_BACKEND (backend); + + /* Discover added or removed OSS devices every second */ + oss->priv->timeout_source = g_timeout_source_new_seconds (1); + g_source_set_callback (oss->priv->timeout_source, + (GSourceFunc) read_devices, + oss, + NULL); + g_source_attach (oss->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 (oss); + + _mate_mixer_backend_set_state (backend, MATE_MIXER_STATE_READY); + return TRUE; +} + +void +oss_backend_close (MateMixerBackend *backend) +{ + OssBackend *oss; + + g_return_if_fail (OSS_IS_BACKEND (backend)); + + oss = OSS_BACKEND (backend); + + g_source_destroy (oss->priv->timeout_source); + g_hash_table_remove_all (oss->priv->devices); + + g_free (oss->priv->default_device); + oss->priv->default_device = NULL; + + _mate_mixer_backend_set_state (backend, MATE_MIXER_STATE_IDLE); +} + +static GList * +oss_backend_list_devices (MateMixerBackend *backend) +{ + GList *list; + + g_return_val_if_fail (OSS_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 (OSS_BACKEND (backend)->priv->devices); + if (list != NULL) + g_list_foreach (list, (GFunc) g_object_ref, NULL); + + return list; +} + +static GList * +oss_backend_list_streams (MateMixerBackend *backend) +{ + OssBackend *oss; + GHashTableIter iter; + gpointer value; + GList *list = NULL; + + g_return_val_if_fail (OSS_IS_BACKEND (backend), NULL); + + oss = OSS_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, oss->priv->devices); + + while (g_hash_table_iter_next (&iter, NULL, &value)) { + OssDevice *device = OSS_DEVICE (value); + OssStream *stream; + + stream = oss_device_get_output_stream (device); + if (stream != NULL) + list = g_list_prepend (list, stream); + stream = oss_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 (OssBackend *oss) +{ + gint i; + gboolean added = FALSE; + + for (i = 0; i < OSS_MAX_DEVICES; i++) { + gboolean added_current; + gchar *path = g_strdup_printf ("/dev/mixer%i", i); + + /* On recent FreeBSD both /dev/mixer and /dev/mixer0 point to the same + * mixer device, on NetBSD and OpenBSD /dev/mixer is a symlink to one + * of the real mixer device nodes, on Linux /dev/mixer is the first + * device and /dev/mixer1 is the second device. + * Handle all of these cases by trying /dev/mixer if /dev/mixer0 fails */ + if (read_device (oss, path, &added_current) == FALSE && i == 0) + read_device (oss, "/dev/mixer", &added_current); + + if (added_current) + added = TRUE; + + g_free (path); + } + + /* 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 (oss); + select_default_output_stream (oss); + } + return G_SOURCE_CONTINUE; +} + +static gboolean +read_device (OssBackend *oss, const gchar *path, gboolean *added) +{ + OssDevice *device; + gint fd; + gchar *bname; + gchar *label; + + device = g_hash_table_lookup (oss->priv->devices, path); + *added = FALSE; + + fd = g_open (path, O_RDWR, 0); + if (fd == -1) { + if (errno != ENOENT && errno != ENXIO) + g_debug ("%s: %s", path, g_strerror (errno)); + + if (device != NULL) + remove_device (oss, device); + return FALSE; + } + + /* Don't proceed if the device is already known, opening the device was + * still tested to be absolutely sure that the device is removed it case + * it has disappeared, but normally the device's polling facility should + * handle this by itself */ + if (device != NULL) { + close (fd); + return TRUE; + } + + bname = g_path_get_basename (path); + label = read_device_label (oss, path, fd); + + device = oss_device_new (bname, label, path, fd); + g_free (bname); + g_free (label); + + close (fd); + + if ((*added = oss_device_open (device)) == TRUE) + add_device (oss, device); + + g_object_unref (device); + return *added; +} + +static gchar * +read_device_label (OssBackend *oss, const gchar *path, gint fd) +{ + guint index; + +#ifdef SOUND_MIXER_INFO + do { + struct mixer_info info; + + /* Prefer device name supplied by the system, but this calls fails + * with EINVAL on FreeBSD */ + if (ioctl (fd, SOUND_MIXER_INFO, &info) == 0) + return g_strdup (info.name); + } while (0); +#endif + + index = (guint) g_ascii_strtoull (path + sizeof ("/dev/mixer") - 1, + NULL, + 10); +#ifdef OSS_PATH_SNDSTAT + /* If the ioctl doesn't succeed, assume that the mixer device number + * matches the pcm number in the sndstat file, this is a bit desperate, but + * it should be correct on FreeBSD */ + do { + gchar *label; + + label = read_device_label_sndstat (oss, OSS_PATH_SNDSTAT, path, index); + if (label != NULL) + return label; + } while (0); +#endif + + return g_strdup_printf (_("OSS Mixer %d"), index); +} + +static gchar * +read_device_label_sndstat (OssBackend *oss, + const gchar *sndstat, + const gchar *path, + guint index) +{ + FILE *fp; + gchar *label = NULL; + gchar *prefix; + gchar line[512]; + + fp = g_fopen (sndstat, "r"); + if (fp == NULL) { + g_debug ("Failed to open %s: %s", sndstat, g_strerror (errno)); + return NULL; + } + + /* Example line: + * pcm0: <ATI R6xx (HDMI)> (play) default */ + prefix = g_strdup_printf ("pcm%u: ", index); + + while (fgets (line, sizeof (line), fp) != NULL) { + gchar *p; + + if (g_str_has_prefix (line, prefix) == FALSE) + continue; + + p = strchr (line, '<'); + if (p != NULL && *p && *(++p)) { + gchar *end = strchr (p, '>'); + + if (end != NULL) { + label = g_strndup (p, end - p); + + /* Normally the default OSS device is /dev/dsp, but on FreeBSD + * /dev/dsp doesn't physically exist on the filesystem, but is + * managed by the kernel according to the user-settable default + * device, in sndstat the default card definition ends with the + * word "default" */ + if (g_str_has_suffix (line, "default")) { + g_free (oss->priv->default_device); + + oss->priv->default_device = g_strdup (path); + } + } else { + g_debug ("Failed to read sndstat line: %s", line); + } + break; + } + } + + g_free (prefix); + fclose (fp); + + return label; +} + +static void +add_device (OssBackend *oss, OssDevice *device) +{ + const gchar *name; + + name = mate_mixer_device_get_name (MATE_MIXER_DEVICE (device)); + + g_hash_table_insert (oss->priv->devices, + g_strdup (oss_device_get_path (device)), + g_object_ref (device)); + + // XXX make device emit it when closed + g_signal_connect_swapped (G_OBJECT (device), + "stream-removed", + G_CALLBACK (remove_stream), + oss); + + g_signal_emit_by_name (G_OBJECT (oss), "device-added", name); + + oss_device_load (device); +} + +static void +remove_device (OssBackend *oss, OssDevice *device) +{ + const gchar *name; + const gchar *path; + + path = oss_device_get_path (device); + name = mate_mixer_device_get_name (MATE_MIXER_DEVICE (device)); + + g_signal_handlers_disconnect_by_func (G_OBJECT (device), + G_CALLBACK (remove_stream), + oss); + + // XXX close the device and make it remove streams + g_hash_table_remove (oss->priv->devices, path); + + g_signal_emit_by_name (G_OBJECT (oss), + "device-removed", + name); +} + +static void +remove_stream (OssBackend *oss, const gchar *name) +{ + MateMixerStream *stream; + + stream = mate_mixer_backend_get_default_input_stream (MATE_MIXER_BACKEND (oss)); + + // 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 (oss); + + stream = mate_mixer_backend_get_default_output_stream (MATE_MIXER_BACKEND (oss)); + + if (stream != NULL && strcmp (mate_mixer_stream_get_name (stream), name) == 0) + select_default_output_stream (oss); +} + +static void +select_default_input_stream (OssBackend *oss) +{ + OssDevice *device = NULL; + OssStream *stream; + gint i; + + /* Always prefer stream in the "default" device */ + if (oss->priv->default_device != NULL) + device = g_hash_table_lookup (oss->priv->devices, oss->priv->default_device); + if (device != NULL) { + stream = oss_device_get_input_stream (device); + if (stream != NULL) { + _mate_mixer_backend_set_default_input_stream (MATE_MIXER_BACKEND (oss), + MATE_MIXER_STREAM (stream)); + return; + } + } + + for (i = 0; i < OSS_MAX_DEVICES; i++) { + gchar *path = g_strdup_printf ("/dev/mixer%i", i); + + device = g_hash_table_lookup (oss->priv->devices, path); + if (device == NULL && i == 0) + device = g_hash_table_lookup (oss->priv->devices, "/dev/mixer"); + + if (device != NULL) { + stream = oss_device_get_input_stream (device); + if (stream != NULL) { + _mate_mixer_backend_set_default_input_stream (MATE_MIXER_BACKEND (oss), + MATE_MIXER_STREAM (stream)); + g_free (path); + return; + } + } + g_free (path); + } + + /* In the worst case unset the default stream */ + _mate_mixer_backend_set_default_input_stream (MATE_MIXER_BACKEND (oss), NULL); +} + +static void +select_default_output_stream (OssBackend *oss) +{ + OssDevice *device = NULL; + OssStream *stream; + gint i; + + /* Always prefer stream in the "default" device */ + if (oss->priv->default_device != NULL) + device = g_hash_table_lookup (oss->priv->devices, oss->priv->default_device); + if (device != NULL) { + stream = oss_device_get_output_stream (device); + if (stream != NULL) { + _mate_mixer_backend_set_default_output_stream (MATE_MIXER_BACKEND (oss), + MATE_MIXER_STREAM (stream)); + return; + } + } + + for (i = 0; i < OSS_MAX_DEVICES; i++) { + gchar *path = g_strdup_printf ("/dev/mixer%i", i); + + device = g_hash_table_lookup (oss->priv->devices, path); + if (device == NULL && i == 0) + device = g_hash_table_lookup (oss->priv->devices, "/dev/mixer"); + + if (device != NULL) { + stream = oss_device_get_output_stream (device); + if (stream != NULL) { + _mate_mixer_backend_set_default_output_stream (MATE_MIXER_BACKEND (oss), + MATE_MIXER_STREAM (stream)); + g_free (path); + return; + } + } + g_free (path); + } + + /* In the worst case unset the default stream */ + _mate_mixer_backend_set_default_output_stream (MATE_MIXER_BACKEND (oss), NULL); +} |