[SF#462] volumealsa plugin volume mapping from alsamixer
authorPeter <ombalaxitabou@users.sf.net>
Sat, 22 Nov 2014 13:22:21 +0000 (13:22 +0000)
committerAndriy Grytsenko <andrej@rep.kiev.ua>
Sat, 22 Nov 2014 13:33:59 +0000 (15:33 +0200)
plugins/volumealsa/volumealsa.c

index 2056334..c285d54 100644 (file)
@@ -16,6 +16,9 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  */
 
+#define _ISOC99_SOURCE /* lrint() */
+#define _GNU_SOURCE /* exp10() */
+
 #include <gtk/gtk.h>
 #include <stdlib.h>
 #include <fcntl.h>
@@ -25,6 +28,7 @@
 #include <gdk-pixbuf/gdk-pixbuf.h>
 #include <alsa/asoundlib.h>
 #include <poll.h>
+#include <math.h>
 #include <libfm/fm-gtk.h>
 #include "plugin.h"
 #include "misc.h"
 #define ICONS_VOLUME_LOW    "volume-low"
 #define ICONS_MUTE          "mute"
 
+#ifdef __UCLIBC__
+/* 10^x = 10^(log e^x) = (e^x)^log10 = e^(x * log 10) */
+# define M_LN10                2.30258509299404568402  /* log_e 10 */
+#define exp10(x) (exp((x) * log(10)))
+#endif /* __UCLIBC__ */
+
+#define MAX_LINEAR_DB_SCALE 24
+
 typedef struct {
 
     /* Graphics. */
@@ -54,6 +66,7 @@ typedef struct {
     snd_mixer_elem_t * master_element;         /* The Master element */
     guint mixer_evt_idle;                      /* Timer to handle restarting poll */
     guint restart_idle;
+    gint alsamixer_mapping;
 
     /* unloading and error handling */
     GIOChannel **channels;                      /* Channels that we listen to */
@@ -196,7 +209,8 @@ static gboolean asound_initialize(VolumeALSAPlugin * vol)
                     return FALSE;
 
     /* Set the playback volume range as we wish it. */
-    snd_mixer_selem_set_playback_volume_range(vol->master_element, 0, 100);
+    if ( ! vol->alsamixer_mapping)
+        snd_mixer_selem_set_playback_volume_range(vol->master_element, 0, 100);
 
     /* Listen to events from ALSA. */
     int n_fds = snd_mixer_poll_descriptors_count(vol->mixer);
@@ -260,39 +274,144 @@ static gboolean asound_is_muted(VolumeALSAPlugin * vol)
     return (value == 0);
 }
 
+static long lrint_dir(double x, int dir)
+{
+    if (dir > 0)
+        return lrint(ceil(x));
+    else if (dir < 0)
+        return lrint(floor(x));
+    else
+        return lrint(x);
+}
+
+static inline gboolean use_linear_dB_scale(long dBmin, long dBmax)
+{
+    return dBmax - dBmin <= MAX_LINEAR_DB_SCALE * 100;
+}
+
+static long get_normalized_volume(snd_mixer_elem_t *elem,
+                                    snd_mixer_selem_channel_id_t channel)
+{
+    long min, max, value;
+    double normalized, min_norm;
+    int err;
+
+    err = snd_mixer_selem_get_playback_dB_range(elem, &min, &max);
+    if (err < 0 || min >= max) {
+        err = snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
+        if (err < 0 || min == max)
+            return 0;
+
+        err = snd_mixer_selem_get_playback_volume(elem, channel, &value);
+        if (err < 0)
+            return 0;
+
+        return lrint(100.0 * (value - min) / (double)(max - min));
+    }
+
+    err = snd_mixer_selem_get_playback_dB(elem, channel, &value);
+    if (err < 0)
+        return 0;
+
+    if (use_linear_dB_scale(min, max))
+        return lrint(100.0 * (value - min) / (double)(max - min));
+
+    normalized = exp10((value - max) / 6000.0);
+    if (min != SND_CTL_TLV_DB_GAIN_MUTE) {
+        min_norm = exp10((min - max) / 6000.0);
+        normalized = (normalized - min_norm) / (1 - min_norm);
+    }
+
+    return lrint(100.0 * normalized);
+}
+
 /* Get the volume from the sound system.
  * This implementation returns the average of the Front Left and Front Right channels. */
 static int asound_get_volume(VolumeALSAPlugin * vol)
 {
     long aleft = 0;
     long aright = 0;
+
     if (vol->master_element != NULL)
     {
-        snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, &aleft);
-        snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, &aright);
+        if ( ! vol->alsamixer_mapping)
+        {
+            snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, &aleft);
+            snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, &aright);
+        }
+        else
+        {
+            aleft = get_normalized_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT);
+            aright = get_normalized_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT);
+        }
     }
     return (aleft + aright) >> 1;
 }
 
+static int set_normalized_volume(snd_mixer_elem_t *elem,
+                                 snd_mixer_selem_channel_id_t channel,
+                                 int vol,
+                                 int dir)
+{
+    long min, max, value;
+    double min_norm, volume;
+    int err;
+
+    volume = vol / 100.0;
+
+    err = snd_mixer_selem_get_playback_dB_range(elem, &min, &max);
+    if (err < 0 || min >= max) {
+        err = snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
+        if (err < 0)
+            return err;
+
+        value = lrint_dir(volume * (max - min), dir) + min;
+        return snd_mixer_selem_set_playback_volume(elem, channel, value);
+    }
+
+    if (use_linear_dB_scale(min, max)) {
+        value = lrint_dir(volume * (max - min), dir) + min;
+        return snd_mixer_selem_set_playback_dB(elem, channel, value, dir);
+    }
+
+    if (min != SND_CTL_TLV_DB_GAIN_MUTE) {
+        min_norm = exp10((min - max) / 6000.0);
+        volume = volume * (1 - min_norm) + min_norm;
+    }
+    value = lrint_dir(6000.0 * log10(volume), dir) + max;
+
+    return snd_mixer_selem_set_playback_dB(elem, channel, value, dir);
+}
+
 /* Set the volume to the sound system.
  * This implementation sets the Front Left and Front Right channels to the specified value. */
 static void asound_set_volume(VolumeALSAPlugin * vol, int volume)
 {
+    int dir = volume - asound_get_volume(vol);
+
+    /* Volume is set to the correct value already */
+    if (dir == 0)
+        return;
+
     if (vol->master_element != NULL)
     {
-        snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, volume);
-        snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, volume);
+        if ( ! vol->alsamixer_mapping)
+        {
+            snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, volume);
+            snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, volume);
+        }
+        else
+        {
+            set_normalized_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, volume, dir);
+            set_normalized_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, volume, dir);
+        }
     }
 }
 
 /*** Graphics ***/
 
-static void volumealsa_update_current_icon(VolumeALSAPlugin * vol)
+static void volumealsa_update_current_icon(VolumeALSAPlugin * vol, gboolean mute, int level)
 {
-    /* Mute status. */
-    gboolean mute = asound_is_muted(vol);
-    int level = asound_get_volume(vol);
-
     /* Change icon according to mute / volume */
     const char* icon_panel="audio-volume-muted-panel";
     const char* icon_fallback=ICONS_MUTE;
@@ -301,12 +420,12 @@ static void volumealsa_update_current_icon(VolumeALSAPlugin * vol)
          icon_panel = "audio-volume-muted-panel";
          icon_fallback=ICONS_MUTE;
     }
-    else if (level >= 75)
+    else if (level >= 66)
     {
          icon_panel = "audio-volume-high-panel";
          icon_fallback=ICONS_VOLUME_HIGH;
     }
-    else if (level >= 50)
+    else if (level >= 33)
     {
          icon_panel = "audio-volume-medium-panel";
          icon_fallback=ICONS_VOLUME_MEDIUM;
@@ -328,7 +447,7 @@ static void volumealsa_update_display(VolumeALSAPlugin * vol)
     gboolean mute = asound_is_muted(vol);
     int level = asound_get_volume(vol);
 
-    volumealsa_update_current_icon(vol);
+    volumealsa_update_current_icon(vol, mute, level);
 
     /* Change icon, fallback to default icon if theme doesn't exsit */
     lxpanel_image_change_icon(vol->tray_icon, vol->icon_panel, vol->icon_fallback);
@@ -342,7 +461,7 @@ static void volumealsa_update_display(VolumeALSAPlugin * vol)
     if (vol->volume_scale != NULL)
     {
         g_signal_handler_block(vol->volume_scale, vol->volume_scale_handler);
-        gtk_range_set_value(GTK_RANGE(vol->volume_scale), asound_get_volume(vol));
+        gtk_range_set_value(GTK_RANGE(vol->volume_scale), level);
         g_signal_handler_unblock(vol->volume_scale, vol->volume_scale_handler);
     }
 
@@ -508,13 +627,6 @@ static GtkWidget *volumealsa_constructor(LXPanel *panel, config_setting_t *setti
     VolumeALSAPlugin * vol = g_new0(VolumeALSAPlugin, 1);
     GtkWidget *p;
 
-    /* Initialize ALSA.  If that fails, present nothing. */
-    if ( ! asound_initialize(vol))
-    {
-        volumealsa_destructor(vol);
-        return NULL;
-    }
-
     /* Allocate top level widget and set into Plugin widget pointer. */
     vol->panel = panel;
     vol->plugin = p = gtk_event_box_new();
@@ -523,6 +635,16 @@ static GtkWidget *volumealsa_constructor(LXPanel *panel, config_setting_t *setti
     gtk_widget_add_events(p, GDK_BUTTON_PRESS_MASK);
     gtk_widget_set_tooltip_text(p, _("Volume control"));
 
+    /* Read config necessary for proper initialization of ALSA. */
+    config_setting_lookup_int(vol->settings, "UseAlsamixerVolumeMapping", &vol->alsamixer_mapping);
+
+    /* Initialize ALSA.  If that fails, present nothing. */
+    if ( ! asound_initialize(vol))
+    {
+        volumealsa_destructor(vol);
+        return NULL;
+    }
+
     /* Allocate icon as a child of top level. */
     vol->tray_icon = lxpanel_image_new_for_icon(panel, "audio-volume-muted-panel",
                                                 -1, ICONS_MUTE);