summaryrefslogtreecommitdiff
path: root/backends/oss/oss-backend.c
diff options
context:
space:
mode:
Diffstat (limited to 'backends/oss/oss-backend.c')
-rw-r--r--backends/oss/oss-backend.c587
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);
+}