From 34bb514b5fd1d6f91ba9a7b3a70b0ea0c6014250 Mon Sep 17 00:00:00 2001 From: Clemens Ladisch Date: Mon, 6 Dec 2010 14:07:48 +0100 Subject: [PATCH] alsamixer: use cubic scale for volume bars Instead of mapping the raw volume values linearly to the screen, use a mapping where the bar height is proportional to the audible volume, i.e., where the amplitude is the cube of the bar height. Signed-off-by: Clemens Ladisch --- alsamixer/Makefile.am | 1 + alsamixer/mixer_display.c | 53 ++++------- alsamixer/mixer_widget.c | 120 +++++++++---------------- alsamixer/volume_mapping.c | 180 +++++++++++++++++++++++++++++++++++++ alsamixer/volume_mapping.h | 19 ++++ 5 files changed, 260 insertions(+), 113 deletions(-) create mode 100644 alsamixer/volume_mapping.c create mode 100644 alsamixer/volume_mapping.h diff --git a/alsamixer/Makefile.am b/alsamixer/Makefile.am index 1de47c6..8a82323 100644 --- a/alsamixer/Makefile.am +++ b/alsamixer/Makefile.am @@ -15,6 +15,7 @@ alsamixer_SOURCES = card_select.c card_select.h \ proc_files.c proc_files.h \ textbox.c textbox.h \ utils.c utils.h \ + volume_mapping.c volume_mapping.h \ widget.c widget.h man_MANS = alsamixer.1 EXTRA_DIST = alsamixer.1 diff --git a/alsamixer/mixer_display.c b/alsamixer/mixer_display.c index 20d6d6a..51a1546 100644 --- a/alsamixer/mixer_display.c +++ b/alsamixer/mixer_display.c @@ -17,10 +17,12 @@ * along with this program. If not, see . */ +#define _C99_SOURCE /* lrint() */ #include "aconfig.h" #include #include #include +#include #include CURSESINC #include #include "gettext_curses.h" @@ -28,6 +30,7 @@ #include "mem.h" #include "colors.h" #include "widget.h" +#include "volume_mapping.h" #include "mixer_widget.h" #include "mixer_controls.h" #include "mixer_display.h" @@ -390,24 +393,14 @@ static void display_string_centered_in_control(int y, int col, const char *s, in display_string_in_field(y, x, s, width, ALIGN_CENTER); } -static long clamp(long value, long min, long max) -{ - if (value < min) - return min; - if (value > max) - return max; - return value; -} - static void display_control(unsigned int control_index) { struct control *control; int col; int i, c; int left, frame_left; - int bar_height, value; - long volumes[2]; - long min, max; + int bar_height; + double volumes[2]; int switches[2]; unsigned int index; const char *s; @@ -452,35 +445,22 @@ static void display_control(unsigned int control_index) waddch(mixer_widget.window, ACS_LRCORNER); } if (control->flags & (TYPE_PVOLUME | TYPE_CVOLUME)) { - int (*get_vol_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long *); + double (*get_vol_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t); if (control->flags & TYPE_PVOLUME) - get_vol_func = snd_mixer_selem_get_playback_volume; + get_vol_func = get_normalized_playback_volume; else - get_vol_func = snd_mixer_selem_get_capture_volume; - err = get_vol_func(control->elem, control->volume_channels[0], &volumes[0]); - if (err >= 0 && (control->flags & HAS_VOLUME_1)) - err = get_vol_func(control->elem, control->volume_channels[1], &volumes[1]); + get_vol_func = get_normalized_capture_volume; + volumes[0] = get_vol_func(control->elem, control->volume_channels[0]); + if (control->flags & HAS_VOLUME_1) + volumes[1] = get_vol_func(control->elem, control->volume_channels[1]); else volumes[1] = volumes[0]; - if (err < 0) - return; - if (control->flags & TYPE_PVOLUME) - err = snd_mixer_selem_get_playback_volume_range(control->elem, &min, &max); - else - err = snd_mixer_selem_get_capture_volume_range(control->elem, &min, &max); - if (err < 0) - return; - if (min >= max) - max = min + 1; - volumes[0] = clamp(volumes[0], min, max); - volumes[1] = clamp(volumes[1], min, max); if (control->flags & IS_ACTIVE) wattrset(mixer_widget.window, 0); for (c = 0; c < 2; c++) { - bar_height = ((volumes[c] - min) * volume_height + - max - min - 1) / (max - min); + bar_height = lrint(volumes[c] * volume_height); for (i = 0; i < volume_height; ++i) { chtype ch; if (i + 1 > bar_height) @@ -505,19 +485,18 @@ static void display_control(unsigned int control_index) } if (control->flags & IS_ACTIVE) wattrset(mixer_widget.window, attr_mixer_active); - value = ((volumes[0] - min) * 100 + (max - min) / 2) / (max - min); if (!(control->flags & HAS_VOLUME_1)) { - sprintf(buf, "%d", value); + sprintf(buf, "%d", lrint(volumes[0] * 100)); display_string_in_field(values_y, frame_left - 2, buf, 8, ALIGN_CENTER); } else { - mvwprintw(mixer_widget.window, values_y, frame_left - 2, "%3d", value); + mvwprintw(mixer_widget.window, values_y, frame_left - 2, + "%3d", lrint(volumes[0] * 100)); if (control->flags & IS_ACTIVE) wattrset(mixer_widget.window, attr_ctl_frame); waddstr(mixer_widget.window, "<>"); if (control->flags & IS_ACTIVE) wattrset(mixer_widget.window, attr_mixer_active); - value = ((volumes[1] - min) * 100 + (max - min) / 2) / (max - min); - wprintw(mixer_widget.window, "%-3d", value); + wprintw(mixer_widget.window, "%-3d", lrint(volumes[1] * 100)); } } diff --git a/alsamixer/mixer_widget.c b/alsamixer/mixer_widget.c index adb4568..fb352d3 100644 --- a/alsamixer/mixer_widget.c +++ b/alsamixer/mixer_widget.c @@ -33,6 +33,7 @@ #include "textbox.h" #include "proc_files.h" #include "card_select.h" +#include "volume_mapping.h" #include "mixer_controls.h" #include "mixer_display.h" #include "mixer_widget.h" @@ -295,82 +296,57 @@ static void change_enum_relative(struct control *control, int delta) static void change_volume_to_percent(struct control *control, int value, unsigned int channels) { - int (*get_range_func)(snd_mixer_elem_t *, long *, long *); - int (*set_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long); - long min, max; - int err; + int (*set_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, double, int); if (!(control->flags & HAS_VOLUME_1)) channels = LEFT; - if (control->flags & TYPE_PVOLUME) { - get_range_func = snd_mixer_selem_get_playback_volume_range; - set_func = snd_mixer_selem_set_playback_volume; - } else { - get_range_func = snd_mixer_selem_get_capture_volume_range; - set_func = snd_mixer_selem_set_capture_volume; - } - err = get_range_func(control->elem, &min, &max); - if (err < 0) - return; + if (control->flags & TYPE_PVOLUME) + set_func = set_normalized_playback_volume; + else + set_func = set_normalized_capture_volume; if (channels & LEFT) - set_func(control->elem, control->volume_channels[0], min + (max - min) * value / 100); + set_func(control->elem, control->volume_channels[0], value / 100.0, 0); if (channels & RIGHT) - set_func(control->elem, control->volume_channels[1], min + (max - min) * value / 100); + set_func(control->elem, control->volume_channels[1], value / 100.0, 0); } -static void change_volume_relative(struct control *control, long delta, unsigned int channels) +static double clamp_volume(double v) { - int (*get_range_func)(snd_mixer_elem_t *, long *, long *); - int (*get_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long *); - int (*set_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long); - long min, max; - long left, right; - long value; - int err; + if (v < 0) + return 0; + if (v > 1) + return 1; + return v; +} + +static void change_volume_relative(struct control *control, int delta, unsigned int channels) +{ + double (*get_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t); + int (*set_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, double, int); + double left, right; + int dir; if (!(control->flags & HAS_VOLUME_1)) channels = LEFT; if (control->flags & TYPE_PVOLUME) { - get_range_func = snd_mixer_selem_get_playback_volume_range; - get_func = snd_mixer_selem_get_playback_volume; - set_func = snd_mixer_selem_set_playback_volume; + get_func = get_normalized_playback_volume; + set_func = set_normalized_playback_volume; } else { - get_range_func = snd_mixer_selem_get_capture_volume_range; - get_func = snd_mixer_selem_get_capture_volume; - set_func = snd_mixer_selem_set_capture_volume; + get_func = get_normalized_capture_volume; + set_func = set_normalized_capture_volume; } - err = get_range_func(control->elem, &min, &max); - if (err < 0) - return; + if (channels & LEFT) + left = get_func(control->elem, control->volume_channels[0]); + if (channels & RIGHT) + right = get_func(control->elem, control->volume_channels[1]); + dir = delta > 0 ? 1 : -1; if (channels & LEFT) { - err = get_func(control->elem, control->volume_channels[0], &left); - if (err < 0) - return; + left = clamp_volume(left + delta / 100.0); + set_func(control->elem, control->volume_channels[0], left, dir); } if (channels & RIGHT) { - err = get_func(control->elem, control->volume_channels[1], &right); - if (err < 0) - return; - } - if (max - min > 100) - delta = (delta * (max - min) + (delta > 0 ? 99 : -99)) / 100; - if (channels & LEFT) { - value = left + delta; - if (value < min) - value = min; - else if (value > max) - value = max; - if (value != left) - set_func(control->elem, control->volume_channels[0], value); - } - if (channels & RIGHT) { - value = right + delta; - if (value < min) - value = min; - else if (value > max) - value = max; - if (value != right) - set_func(control->elem, control->volume_channels[1], value); + right = clamp_volume(right + delta / 100.0); + set_func(control->elem, control->volume_channels[1], right, dir); } } @@ -460,34 +436,26 @@ static void toggle_capture(unsigned int channels) static void balance_volumes(void) { struct control *control; - long left, right; + double left, right; int err; control = get_focus_control(TYPE_PVOLUME | TYPE_CVOLUME); if (!control || !(control->flags & HAS_VOLUME_1)) return; if (control->flags & TYPE_PVOLUME) { - err = snd_mixer_selem_get_playback_volume(control->elem, control->volume_channels[0], &left); - if (err < 0) - return; - err = snd_mixer_selem_get_playback_volume(control->elem, control->volume_channels[1], &right); - if (err < 0) - return; + left = get_normalized_playback_volume(control->elem, control->volume_channels[0]); + right = get_normalized_playback_volume(control->elem, control->volume_channels[1]); } else { - err = snd_mixer_selem_get_capture_volume(control->elem, control->volume_channels[0], &left); - if (err < 0) - return; - err = snd_mixer_selem_get_capture_volume(control->elem, control->volume_channels[1], &right); - if (err < 0) - return; + left = get_normalized_capture_volume(control->elem, control->volume_channels[0]); + right = get_normalized_capture_volume(control->elem, control->volume_channels[1]); } left = (left + right) / 2; if (control->flags & TYPE_PVOLUME) { - snd_mixer_selem_set_playback_volume(control->elem, control->volume_channels[0], left); - snd_mixer_selem_set_playback_volume(control->elem, control->volume_channels[1], left); + set_normalized_playback_volume(control->elem, control->volume_channels[0], left, 0); + set_normalized_playback_volume(control->elem, control->volume_channels[1], left, 0); } else { - snd_mixer_selem_set_capture_volume(control->elem, control->volume_channels[0], left); - snd_mixer_selem_set_capture_volume(control->elem, control->volume_channels[1], left); + set_normalized_capture_volume(control->elem, control->volume_channels[0], left, 0); + set_normalized_capture_volume(control->elem, control->volume_channels[1], left, 0); } display_controls(); } diff --git a/alsamixer/volume_mapping.c b/alsamixer/volume_mapping.c new file mode 100644 index 0000000..9cacad8 --- /dev/null +++ b/alsamixer/volume_mapping.c @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2010 Clemens Ladisch + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * The functions in this file map the value ranges of ALSA mixer controls onto + * the interval 0..1. + * + * The mapping is designed so that the position in the interval is proportional + * to the volume as a human ear would perceive it (i.e., the position is the + * cubic root of the linear sample multiplication factor). For controls with + * a small range (24 dB or less), the mapping is linear in the dB values so + * that each step has the same size visually. Only for controls without dB + * information, a linear mapping of the hardware volume register values is used + * (this is the same algorithm as used in the old alsamixer). + * + * When setting the volume, 'dir' is the rounding direction: + * -1/0/1 = down/nearest/up. + */ + +#define _ISOC99_SOURCE /* lrint() */ +#define _GNU_SOURCE /* exp10() */ +#include "aconfig.h" +#include +#include +#include "volume_mapping.h" + +#define MAX_LINEAR_DB_SCALE 24 + +static inline bool use_linear_dB_scale(long dBmin, long dBmax) +{ + return dBmax - dBmin <= MAX_LINEAR_DB_SCALE * 100; +} + +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); +} + +enum ctl_dir { PLAYBACK, CAPTURE }; + +static int (* const get_dB_range[2])(snd_mixer_elem_t *, long *, long *) = { + snd_mixer_selem_get_playback_dB_range, + snd_mixer_selem_get_capture_dB_range, +}; +static int (* const get_raw_range[2])(snd_mixer_elem_t *, long *, long *) = { + snd_mixer_selem_get_playback_volume_range, + snd_mixer_selem_get_capture_volume_range, +}; +static int (* const get_dB[2])(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long *) = { + snd_mixer_selem_get_playback_dB, + snd_mixer_selem_get_capture_dB, +}; +static int (* const get_raw[2])(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long *) = { + snd_mixer_selem_get_playback_volume, + snd_mixer_selem_get_capture_volume, +}; +static int (* const set_dB[2])(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long, int) = { + snd_mixer_selem_set_playback_dB, + snd_mixer_selem_set_capture_dB, +}; +static int (* const set_raw[2])(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long) = { + snd_mixer_selem_set_playback_volume, + snd_mixer_selem_set_capture_volume, +}; + +static double get_normalized_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel, + enum ctl_dir ctl_dir) +{ + long min, max, value; + double normalized, min_norm; + int err; + + err = get_dB_range[ctl_dir](elem, &min, &max); + if (err < 0 || min >= max) { + err = get_raw_range[ctl_dir](elem, &min, &max); + if (err < 0 || min == max) + return 0; + + err = get_raw[ctl_dir](elem, channel, &value); + if (err < 0) + return 0; + + return (value - min) / (double)(max - min); + } + + err = get_dB[ctl_dir](elem, channel, &value); + if (err < 0) + return 0; + + if (use_linear_dB_scale(min, max)) + return (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 normalized; +} + +static int set_normalized_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel, + double volume, + int dir, + enum ctl_dir ctl_dir) +{ + long min, max, value; + double min_norm; + int err; + + err = get_dB_range[ctl_dir](elem, &min, &max); + if (err < 0 || min >= max) { + err = get_raw_range[ctl_dir](elem, &min, &max); + if (err < 0) + return err; + + value = lrint_dir(volume * (max - min), dir) + min; + return set_raw[ctl_dir](elem, channel, value); + } + + if (use_linear_dB_scale(min, max)) { + value = lrint_dir(volume * (max - min), dir) + min; + return set_dB[ctl_dir](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 set_dB[ctl_dir](elem, channel, value, dir); +} + +double get_normalized_playback_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel) +{ + return get_normalized_volume(elem, channel, PLAYBACK); +} + +double get_normalized_capture_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel) +{ + return get_normalized_volume(elem, channel, CAPTURE); +} + +int set_normalized_playback_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel, + double volume, + int dir) +{ + return set_normalized_volume(elem, channel, volume, dir, PLAYBACK); +} + +int set_normalized_capture_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel, + double volume, + int dir) +{ + return set_normalized_volume(elem, channel, volume, dir, CAPTURE); +} diff --git a/alsamixer/volume_mapping.h b/alsamixer/volume_mapping.h new file mode 100644 index 0000000..d4251d6 --- /dev/null +++ b/alsamixer/volume_mapping.h @@ -0,0 +1,19 @@ +#ifndef VOLUME_MAPPING_H_INCLUDED +#define VOLUME_MAPPING_H_INCLUDED + +#include + +double get_normalized_playback_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel); +double get_normalized_capture_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel); +int set_normalized_playback_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel, + double volume, + int dir); +int set_normalized_capture_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel, + double volume, + int dir); + +#endif