fade: Add the ALSA mixer implementation.
authorAndre Noll <maan@systemlinux.org>
Mon, 16 Jul 2012 19:25:51 +0000 (21:25 +0200)
committerAndre Noll <maan@systemlinux.org>
Sun, 7 Oct 2012 09:12:07 +0000 (11:12 +0200)
This provides another mixer API which relies on the libasound library
of ALSA.

The manual is changed accordingly to mention that para_fade now
supports both OSS and ALSA.

alsa_mix.c [new file with mode: 0644]
configure.ac
error.h
m4/gengetopt/fade.m4
web/manual.m4

diff --git a/alsa_mix.c b/alsa_mix.c
new file mode 100644 (file)
index 0000000..5a58354
--- /dev/null
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2012 Andre Noll <maan@systemlinux.org>
+ *
+ * Licensed under the GPL v2. For licencing details see COPYING.
+ */
+
+/** \file alsa_mix.c The ALSA mixer plugin. */
+
+#include <regex.h>
+#include <alsa/asoundlib.h>
+
+#include "para.h"
+#include "error.h"
+#include "mix.h"
+#include "string.h"
+
+struct mixer_handle {
+       /* The ALSA mixer handle */
+       snd_mixer_t *mixer;
+       /* Copy of the alsa device name, e.g. "hw:0". */
+       char *card;
+       /* ALSA's representation of the given mixer channel. */
+       snd_mixer_elem_t *elem;
+       /* Set in ->set_channel(), used for ->get() and ->set(). */
+       long pmin, pmax;
+};
+
+static void alsa_mix_close(struct mixer_handle **handle)
+{
+       struct mixer_handle *h;
+
+       if (!handle)
+               return;
+       h = *handle;
+       if (h) {
+               PARA_INFO_LOG("closing mixer handle\n");
+               if (h->mixer) /* nec. */
+                       snd_mixer_close(h->mixer);
+               free(h->card);
+               free(h);
+               /*
+                * The global ALSA configuration is cached for next usage,
+                * which causes valgrind to report many memory leaks. Calling
+                * snd_config_update_free_global() frees the cache.
+                */
+               snd_config_update_free_global();
+       }
+       *handle = NULL;
+}
+
+static int alsa_mix_open(const char *dev, struct mixer_handle **handle)
+{
+       int ret;
+       char *msg;
+       struct mixer_handle *h;
+
+       PARA_INFO_LOG("snd_mixer_{open,attach,register,load}\n");
+       *handle = NULL;
+       h = para_calloc(sizeof(*h));
+       h->card = para_strdup(dev? dev : "hw:0");
+       ret = snd_mixer_open(&h->mixer, 0);
+       if (ret < 0) {
+               msg = make_message("snd_mixer_open() failed: %s",
+                       snd_strerror(ret));
+               goto fail;
+       }
+       ret = snd_mixer_attach(h->mixer, h->card);
+       if (ret < 0) {
+               msg = make_message("mixer attach error (%s): %s", h->card,
+                       snd_strerror(ret));
+               goto fail;
+       }
+       ret = snd_mixer_selem_register(h->mixer, NULL, NULL);
+       if (ret < 0) {
+               msg = make_message("mixer register error (%s): %s", h->card,
+                       snd_strerror(ret));
+               goto fail;
+       }
+       ret = snd_mixer_load(h->mixer);
+       if (ret < 0) {
+               msg = make_message("mixer load error (%s): %s", h->card,
+                       snd_strerror(ret));
+               goto fail;
+       }
+       /* success */
+       *handle = h;
+       return 1;
+fail:
+       PARA_NOTICE_LOG("%s\n", msg);
+       free(msg);
+       alsa_mix_close(&h);
+       return -E_ALSA_MIX_OPEN;
+}
+
+static bool channel_has_playback(snd_mixer_selem_channel_id_t chn,
+               snd_mixer_elem_t *elem)
+{
+       return snd_mixer_selem_has_playback_channel(elem, chn);
+}
+
+static char *alsa_mix_get_channels(struct mixer_handle *handle)
+{
+       char *list = NULL;
+       snd_mixer_selem_id_t *sid;
+       snd_mixer_elem_t *elem;
+
+       snd_mixer_selem_id_alloca(&sid);
+       elem = snd_mixer_first_elem(handle->mixer);
+       for (; elem; elem = snd_mixer_elem_next(elem)) {
+               char *tmp = list;
+               const char *name;
+
+               if (!channel_has_playback(0, elem))
+                       continue;
+               if (!snd_mixer_selem_has_playback_volume(elem))
+                       continue;
+               snd_mixer_selem_get_id(elem, sid);
+               name = snd_mixer_selem_id_get_name(sid);
+               list = make_message("%s%s%s",
+                       list? list : "",
+                       list? ", " : "",
+                       name);
+               free(tmp);
+       }
+       return list;
+}
+
+static int alsa_mix_set_channel(struct mixer_handle *h,
+               const char *mixer_channel)
+{
+       int ret, selem_id = 0;
+       snd_mixer_selem_id_t *sid;
+
+       if (!mixer_channel)
+               mixer_channel = "Master";
+       snd_mixer_selem_id_alloca(&sid);
+       snd_mixer_selem_id_set_index(sid, selem_id);
+       snd_mixer_selem_id_set_name(sid, mixer_channel);
+       h->elem = snd_mixer_find_selem(h->mixer, sid);
+       if (!h->elem) {
+               PARA_NOTICE_LOG("unable to find simple control '%s',%i\n",
+                       snd_mixer_selem_id_get_name(sid),
+                       snd_mixer_selem_id_get_index(sid));
+               return -E_ALSA_MIX_BAD_ELEM;
+       }
+       ret = snd_mixer_selem_get_playback_volume_range(h->elem,
+               &h->pmin, &h->pmax);
+       if (ret < 0) {
+               PARA_NOTICE_LOG("unable to get %s range (%s): %s\n",
+                       mixer_channel, h->card, snd_strerror(ret));
+               return -E_ALSA_MIX_BAD_ELEM;
+       }
+       if (h->pmin < 0 || h->pmax < 0 || h->pmin >= h->pmax) {
+               PARA_NOTICE_LOG("alsa reported %s range %ld-%ld (%s)\n",
+                       mixer_channel, h->pmin, h->pmax, h->card);
+               return -E_ALSA_MIX_BAD_ELEM;
+       }
+       return 1;
+}
+
+static int alsa_mix_get(struct mixer_handle *h)
+{
+       snd_mixer_selem_channel_id_t chn;
+       int n = 0;
+       float avg = 0;
+
+       /* compute average over all channels */
+       for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++) {
+               long val;
+               int ret;
+
+               if (!channel_has_playback(chn, h->elem))
+                       continue;
+               ret = snd_mixer_selem_get_playback_volume(h->elem, chn, &val);
+               if (ret < 0) {
+                       PARA_NOTICE_LOG("unable to get value for channel %d (%s): %s\n",
+                               (int)chn, h->card, snd_strerror(ret));
+                       return -E_ALSA_MIX_GET_VAL;
+               }
+               /* update the rolling average */
+               avg = (val + n * avg) / (n + 1);
+               n++;
+       }
+       /* scale to 0..100 */
+       avg = 100 * (avg - h->pmin) / (h->pmax - h->pmin);
+       return avg;
+}
+
+static int alsa_mix_set(struct mixer_handle *h, int val)
+{
+       int ret;
+       snd_mixer_selem_channel_id_t chn;
+       long scaled = val / 100.0 * (h->pmax - h->pmin) + h->pmin;
+
+       PARA_INFO_LOG("new value: %d\n", val);
+       for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++) {
+               if (!channel_has_playback(chn, h->elem))
+                       continue;
+               ret = snd_mixer_selem_set_playback_volume(h->elem, chn, scaled);
+               if (ret < 0) {
+                       PARA_NOTICE_LOG("unable to set value for channel %d (%s): %s\n",
+                               (int)chn, h->card, snd_strerror(ret));
+                       return -E_ALSA_MIX_SET_VAL;
+               }
+       }
+       return 1;
+}
+
+/**
+ * The init function of the ALSA mixer.
+ *
+ * \param self The structure to initialize.
+ *
+ * \sa struct \ref mixer, \ref oss_mix_init().
+ */
+void alsa_mix_init(struct mixer *self)
+{
+       self->open = alsa_mix_open;
+       self->get_channels = alsa_mix_get_channels;
+       self->set_channel = alsa_mix_set_channel;
+       self->close = alsa_mix_close;
+       self->get = alsa_mix_get;
+       self->set = alsa_mix_set;
+}
index b601049..93b60e6 100644 (file)
@@ -861,16 +861,20 @@ if test "$have_alsa" = "yes"; then
 fi
 
 if test "$have_alsa" = "yes"; then
-       all_errlist_objs="$all_errlist_objs alsa_write"
        audiod_errlist_objs="$audiod_errlist_objs alsa_write"
        audiod_cmdline_objs="$audiod_cmdline_objs add_cmdline(alsa_write)"
        audiod_ldflags="$audiod_ldflags -lasound"
-
        write_errlist_objs="$write_errlist_objs alsa_write"
        write_cmdline_objs="$write_cmdline_objs add_cmdline(alsa_write)"
        write_ldflags="$write_ldflags -lasound"
+       fade_errlist_objs="$fade_errlist_objs alsa_mix"
+       fade_ldflags="$fade_ldflags -lasound"
+       all_errlist_objs="$all_errlist_objs alsa_write alsa_mix"
+
        writers="$writers alsa"
        default_writer="ALSA_WRITE"
+       mixers="${mixers}alsa "
+       default_mixer="ALSA_MIX"
 fi
 
 CPPFLAGS="$OLD_CPPFLAGS"
@@ -909,7 +913,7 @@ if test -n "$mixers"; then
                init functions of the supported mixers)
        array="$(for i in $mixers; do printf '{.init = '$i'_mix_init},'; done)"
        AC_DEFINE_UNQUOTED(MIXER_ARRAY, $array, array of supported mixers)
-       mixer_summary="mixers supported by para_fade: $mixers"
+       mixer_summary="supported mixers:: $mixers, default: $default_mixer"
 else
        AC_MSG_WARN([no mixer support])
        mixer_summary="para_fade: no"
diff --git a/error.h b/error.h
index c7c5776..8579bd8 100644 (file)
--- a/error.h
+++ b/error.h
@@ -39,6 +39,11 @@ extern const char **para_errlist[];
 #define OSS_MIX_ERRORS \
        PARA_ERROR(OSS_MIXER_CHANNEL, "invalid mixer channel"), \
 
+#define ALSA_MIX_ERRORS \
+       PARA_ERROR(ALSA_MIX_OPEN, "could not open mixer"), \
+       PARA_ERROR(ALSA_MIX_BAD_ELEM, "invalid/unsupported control element"), \
+       PARA_ERROR(ALSA_MIX_GET_VAL, "could not read control element state"), \
+       PARA_ERROR(ALSA_MIX_SET_VAL, "could not set control element state"), \
 
 #define SIDEBAND_ERRORS \
        PARA_ERROR(BAD_BAND, "invalid or unexpected band designator"), \
index e5d08bf..42f135f 100644 (file)
@@ -38,8 +38,10 @@ option "mixer-device" m
        string typestr = "device"
        optional
        details = "
-               If this option is not given '/dev/mixer' is used as
-               the default.
+               The default device (used if this option is not given)
+               depends on the selected mixer API. For ALSA, the
+               default is 'hw:0' which corresponds to the first sound
+               device. For OSS, '/dev/mixer' is used as the default.
        "
 
 option "mixer-channel" C
@@ -48,11 +50,18 @@ option "mixer-channel" C
        string typestr = "channel"
        optional
        details = "
-               The possible values are 'volume', 'bass', 'treble',
-               'synth', 'pcm', 'speaker', 'line', 'mic', 'cd', 'imix',
-               'altpcm', 'reclev', 'igain', 'ogain'. However, not all
-               listed channels might be supported on any particular
-               hardware. The default mixer channel is 'volume'.
+               For the ALSA mixer API, the possible values are
+               determined at runtime depending on the hardware and
+               can be printed by specifying an invalid mixer channel,
+               for example --mixer-channel help. The default channel
+               is 'Master'.
+
+               For OSS the possible values are invariably 'volume',
+               'bass', 'treble', 'synth', 'pcm', 'speaker', 'line',
+               'mic', 'cd', 'imix', 'altpcm', 'reclev', 'igain',
+               'ogain'. However, not all listed channels might be
+               supported on any particular hardware. The default
+               channel is 'volume'.
        "
 
 section "Options for sleep mode"
index 5a348bb..56ee346 100644 (file)
@@ -183,7 +183,7 @@ can be added easily.
 
 *para_fade*
 
-An (OSS-only) alarm clock and volume-fader.
+An alarm clock and volume-fader for OSS and ALSA.
 
 -----------
 Quick start