diff --git a/apps/features.txt b/apps/features.txt
index 2262f7502e..bafaa11599 100644
--- a/apps/features.txt
+++ b/apps/features.txt
@@ -296,3 +296,7 @@ hibylinux
(CONFIG_KEYPAD == IRIVER_H10_PAD)
clear_settings_on_hold
#endif
+
+#if defined(HAVE_PERCEPTUAL_VOLUME)
+perceptual_volume
+#endif
diff --git a/apps/gui/list.c b/apps/gui/list.c
index 83d12289f2..652279a9de 100644
--- a/apps/gui/list.c
+++ b/apps/gui/list.c
@@ -663,12 +663,10 @@ bool gui_synclist_do_button(struct gui_synclist * lists, int *actionptr)
#ifdef HAVE_VOLUME_IN_LIST
case ACTION_LIST_VOLUP:
- global_settings.volume += sound_steps(SOUND_VOLUME);
- setvol();
+ adjust_volume(1);
return true;
case ACTION_LIST_VOLDOWN:
- global_settings.volume -= sound_steps(SOUND_VOLUME);
- setvol();
+ adjust_volume(-1);
return true;
#endif
case ACTION_STD_PREV:
diff --git a/apps/gui/quickscreen.c b/apps/gui/quickscreen.c
index 356f74b283..221dfe3111 100644
--- a/apps/gui/quickscreen.c
+++ b/apps/gui/quickscreen.c
@@ -378,14 +378,12 @@ static int gui_syncquickscreen_run(struct gui_quickscreen * qs, int button_enter
else if (button == button_enter)
can_quit = true;
else if (button == ACTION_QS_VOLUP) {
- global_settings.volume += sound_steps(SOUND_VOLUME);
- setvol();
+ adjust_volume(1);
FOR_NB_SCREENS(i)
skin_update(CUSTOM_STATUSBAR, i, SKIN_REFRESH_NON_STATIC);
}
else if (button == ACTION_QS_VOLDOWN) {
- global_settings.volume -= sound_steps(SOUND_VOLUME);
- setvol();
+ adjust_volume(-1);
FOR_NB_SCREENS(i)
skin_update(CUSTOM_STATUSBAR, i, SKIN_REFRESH_NON_STATIC);
}
diff --git a/apps/gui/wps.c b/apps/gui/wps.c
index 4b0c7c056f..fe656034b9 100644
--- a/apps/gui/wps.c
+++ b/apps/gui/wps.c
@@ -841,9 +841,9 @@ long gui_wps_show(void)
case ACTION_WPS_VOLUP: /* fall through */
case ACTION_WPS_VOLDOWN:
if (button == ACTION_WPS_VOLUP)
- global_settings.volume += sound_steps(SOUND_VOLUME);
+ adjust_volume(1);
else
- global_settings.volume -= sound_steps(SOUND_VOLUME);
+ adjust_volume(-1);
setvol();
FOR_NB_SCREENS(i)
diff --git a/apps/lang/english.lang b/apps/lang/english.lang
index 3ad2921abe..232541e33f 100644
--- a/apps/lang/english.lang
+++ b/apps/lang/english.lang
@@ -15163,7 +15163,7 @@
id: LANG_DIRECT
- desc: in the pictureflow settings
+ desc: in the pictureflow settings, also a volume adjustment mode
user: core
*: "Direct"
@@ -16559,3 +16559,54 @@
*: "Play Last Shuffled"
+
+ id: LANG_VOLUME_ADJUST_MODE
+ desc: in system settings
+ user: core
+
+ *: none
+ perceptual_volume: "Volume Adjustment Mode"
+
+
+ *: none
+ perceptual_volume: "Volume Adjustment Mode"
+
+
+ *: none
+ perceptual_volume: "Volume Adjustment Mode"
+
+
+
+ id: LANG_VOLUME_ADJUST_NORM_STEPS
+ desc: in system settings
+ user: core
+
+ *: none
+ perceptual_volume: "Number of Volume Steps"
+
+
+ *: none
+ perceptual_volume: "Number of Volume Steps"
+
+
+ *: none
+ perceptual_volume: "Number of Volume Steps"
+
+
+
+ id: LANG_PERCEPTUAL
+ desc: in system settings -> volume adjustment mode
+ user: core
+
+ *: none
+ perceptual_volume: "Perceptual"
+
+
+ *: none
+ perceptual_volume: "Perceptual"
+
+
+ *: none
+ perceptual_volume: "Perceptual"
+
+
diff --git a/apps/menus/settings_menu.c b/apps/menus/settings_menu.c
index 9d1314c269..bfb69a9942 100644
--- a/apps/menus/settings_menu.c
+++ b/apps/menus/settings_menu.c
@@ -338,6 +338,11 @@ MAKE_MENU(limits_menu, ID2P(LANG_LIMITS_MENU), 0, Icon_NOICON,
,&default_glyphs
);
+#ifdef HAVE_PERCEPTUAL_VOLUME
+/* Volume adjustment */
+MENUITEM_SETTING(volume_adjust_mode, &global_settings.volume_adjust_mode, NULL);
+MENUITEM_SETTING(volume_adjust_norm_steps, &global_settings.volume_adjust_norm_steps, NULL);
+#endif
/* Keyclick menu */
MENUITEM_SETTING(keyclick, &global_settings.keyclick, NULL);
@@ -424,6 +429,10 @@ MAKE_MENU(system_menu, ID2P(LANG_SYSTEM),
&disk_menu,
#endif
&limits_menu,
+#ifdef HAVE_PERCEPTUAL_VOLUME
+ &volume_adjust_mode,
+ &volume_adjust_norm_steps,
+#endif
#ifdef HAVE_QUICKSCREEN
&shortcuts_replaces_quickscreen,
#endif
diff --git a/apps/misc.c b/apps/misc.c
index fd840749cb..e10fceb9af 100644
--- a/apps/misc.c
+++ b/apps/misc.c
@@ -824,6 +824,113 @@ void setvol(void)
settings_save();
}
+#ifdef HAVE_PERCEPTUAL_VOLUME
+static short norm_tab[MAX_NORM_VOLUME_STEPS+2];
+static int norm_tab_num_steps;
+static int norm_tab_size;
+
+static void update_norm_tab(void)
+{
+ const int lim = global_settings.volume_adjust_norm_steps;
+ if (lim == norm_tab_num_steps)
+ return;
+ norm_tab_num_steps = lim;
+
+ const int min = sound_min(SOUND_VOLUME);
+ const int max = sound_max(SOUND_VOLUME);
+ const int step = sound_steps(SOUND_VOLUME);
+
+ /* Ensure the table contains the minimum volume */
+ norm_tab[0] = min;
+ norm_tab_size = 1;
+
+ for (int i = 0; i < lim; ++i)
+ {
+ int vol = from_normalized_volume(i, min, max, lim);
+ int rem = vol % step;
+
+ vol -= rem;
+ if (abs(rem) > step/2)
+ vol += rem < 0 ? -step : step;
+
+ /* Add volume step, ignoring any duplicate entries that may
+ * occur due to rounding */
+ if (vol != norm_tab[norm_tab_size-1])
+ norm_tab[norm_tab_size++] = vol;
+ }
+
+ /* Ensure the table contains the maximum volume */
+ if (norm_tab[norm_tab_size-1] != max)
+ norm_tab[norm_tab_size++] = max;
+}
+
+void set_normalized_volume(int vol)
+{
+ update_norm_tab();
+
+ if (vol < 0)
+ vol = 0;
+ if (vol >= norm_tab_size)
+ vol = norm_tab_size - 1;
+
+ global_settings.volume = norm_tab[vol];
+}
+
+int get_normalized_volume(void)
+{
+ update_norm_tab();
+
+ int a = 0, b = norm_tab_size - 1;
+ while (a != b)
+ {
+ int i = (a + b + 1) / 2;
+ if (global_settings.volume < norm_tab[i])
+ b = i - 1;
+ else
+ a = i;
+ }
+
+ return a;
+}
+#else
+void set_normalized_volume(int vol)
+{
+ global_settings.volume = vol * sound_steps(SOUND_VOLUME);
+}
+
+int get_normalized_volume(void)
+{
+ return global_settings.volume / sound_steps(SOUND_VOLUME);
+}
+#endif
+
+void adjust_volume(int steps)
+{
+#ifdef HAVE_PERCEPTUAL_VOLUME
+ adjust_volume_ex(steps, global_settings.volume_adjust_mode);
+#else
+ adjust_volume_ex(steps, VOLUME_ADJUST_DIRECT);
+#endif
+}
+
+void adjust_volume_ex(int steps, enum volume_adjust_mode mode)
+{
+ switch (mode)
+ {
+ case VOLUME_ADJUST_PERCEPTUAL:
+#ifdef HAVE_PERCEPTUAL_VOLUME
+ set_normalized_volume(get_normalized_volume() + steps);
+ break;
+#endif
+ case VOLUME_ADJUST_DIRECT:
+ default:
+ global_settings.volume += steps * sound_steps(SOUND_VOLUME);
+ break;
+ }
+
+ setvol();
+}
+
char* strrsplt(char* str, int c)
{
char* s = strrchr(str, c);
diff --git a/apps/misc.h b/apps/misc.h
index 72b8735c8a..b7a9a5c42c 100644
--- a/apps/misc.h
+++ b/apps/misc.h
@@ -137,8 +137,22 @@ void check_bootfile(bool do_rolo);
#endif
#endif
+enum volume_adjust_mode
+{
+ VOLUME_ADJUST_DIRECT, /* adjust in units of the volume step size */
+ VOLUME_ADJUST_PERCEPTUAL, /* adjust using perceptual steps */
+};
+
+/* min/max values for global_settings.volume_adjust_norm_steps */
+#define MIN_NORM_VOLUME_STEPS 10
+#define MAX_NORM_VOLUME_STEPS 100
+
/* check range, set volume and save settings */
void setvol(void);
+void set_normalized_volume(int vol);
+int get_normalized_volume(void);
+void adjust_volume(int steps);
+void adjust_volume_ex(int steps, enum volume_adjust_mode mode);
#ifdef HAVE_LCD_COLOR
int hex_to_rgb(const char* hex, int* color);
diff --git a/apps/plugin.c b/apps/plugin.c
index 00fac21b8d..cdbe340ddd 100644
--- a/apps/plugin.c
+++ b/apps/plugin.c
@@ -828,6 +828,7 @@ static const struct plugin_api rockbox_api = {
#if defined(HAVE_TAGCACHE)
tagtree_subentries_do_action,
#endif
+ adjust_volume,
};
static int plugin_buffer_handle;
diff --git a/apps/plugin.h b/apps/plugin.h
index 20df7e72f2..286a5e2794 100644
--- a/apps/plugin.h
+++ b/apps/plugin.h
@@ -158,7 +158,7 @@ int plugin_open(const char *plugin, const char *parameter);
#define PLUGIN_MAGIC 0x526F634B /* RocK */
/* increase this every time the api struct changes */
-#define PLUGIN_API_VERSION 264
+#define PLUGIN_API_VERSION 265
/* update this to latest version if a change to the api struct breaks
backwards compatibility (and please take the opportunity to sort in any
@@ -954,6 +954,7 @@ struct plugin_api {
#ifdef HAVE_TAGCACHE
bool (*tagtree_subentries_do_action)(bool (*action_cb)(const char *file_name));
#endif
+ void (*adjust_volume)(int steps);
};
/* plugin header */
diff --git a/apps/plugins/lrcplayer.c b/apps/plugins/lrcplayer.c
index 71e5310638..de31733671 100644
--- a/apps/plugins/lrcplayer.c
+++ b/apps/plugins/lrcplayer.c
@@ -2625,16 +2625,10 @@ static int handle_button(void)
ff_rewind(0, false);
break;
case ACTION_WPS_VOLDOWN:
- limit = rb->sound_min(SOUND_VOLUME);
- if (--rb->global_settings->volume < limit)
- rb->global_settings->volume = limit;
- rb->sound_set(SOUND_VOLUME, rb->global_settings->volume);
+ rb->adjust_volume(-1);
break;
case ACTION_WPS_VOLUP:
- limit = rb->sound_max(SOUND_VOLUME);
- if (++rb->global_settings->volume > limit)
- rb->global_settings->volume = limit;
- rb->sound_set(SOUND_VOLUME, rb->global_settings->volume);
+ rb->adjust_volume(1);
break;
case ACTION_WPS_CONTEXT:
ret = LRC_GOTO_EDITOR;
diff --git a/apps/plugins/mikmod/mikmod.c b/apps/plugins/mikmod/mikmod.c
index 6622b5fdb6..65633c0ad1 100644
--- a/apps/plugins/mikmod/mikmod.c
+++ b/apps/plugins/mikmod/mikmod.c
@@ -710,7 +710,6 @@ static void mm_errorhandler(void)
static int playfile(char* filename)
{
- int vol = 0;
int button;
int retval = PLUGIN_OK;
bool changingpos = false;
@@ -789,13 +788,8 @@ static int playfile(char* filename)
}
break;
}
- vol = rb->global_settings->volume;
- if (vol < rb->sound_max(SOUND_VOLUME))
- {
- vol++;
- rb->sound_set(SOUND_VOLUME, vol);
- rb->global_settings->volume = vol;
- }
+
+ rb->adjust_volume(1);
break;
case ACTION_WPS_VOLDOWN:
@@ -808,13 +802,8 @@ static int playfile(char* filename)
}
break;
}
- vol = rb->global_settings->volume;
- if (vol > rb->sound_min(SOUND_VOLUME))
- {
- vol--;
- rb->sound_set(SOUND_VOLUME, vol);
- rb->global_settings->volume = vol;
- }
+
+ rb->adjust_volume(-1);
break;
case ACTION_WPS_SKIPPREV:
diff --git a/apps/plugins/sdl/SDL_mixer/timidity/playmidi.c b/apps/plugins/sdl/SDL_mixer/timidity/playmidi.c
index 1638732dc5..38f7109b13 100644
--- a/apps/plugins/sdl/SDL_mixer/timidity/playmidi.c
+++ b/apps/plugins/sdl/SDL_mixer/timidity/playmidi.c
@@ -21,6 +21,8 @@
#include "tables.h"
+/* ROCKBOX HACK: avoid a conflict with adjust_volume() in misc.h */
+#define adjust_volume adjust_midi_volume
static int opt_expression_curve = 2;
static int opt_volume_curve = 2;
diff --git a/apps/settings.h b/apps/settings.h
index 1ad1923c73..ca10c45d5f 100644
--- a/apps/settings.h
+++ b/apps/settings.h
@@ -855,6 +855,11 @@ struct user_settings
#endif
int volume_limit; /* maximum volume limit */
+#ifdef HAVE_PERCEPTUAL_VOLUME
+ int volume_adjust_mode;
+ int volume_adjust_norm_steps;
+#endif
+
int surround_enabled;
int surround_balance;
int surround_fx1;
diff --git a/apps/settings_list.c b/apps/settings_list.c
index 60ac4192fa..315f39b21f 100644
--- a/apps/settings_list.c
+++ b/apps/settings_list.c
@@ -40,6 +40,7 @@
#include "powermgmt.h"
#include "kernel.h"
#include "open_plugin.h"
+#include "misc.h"
#ifdef HAVE_REMOTE_LCD
#include "lcd-remote.h"
#endif
@@ -1057,6 +1058,16 @@ const struct settings_list settings[] = {
MAX_FILES_IN_DIR_STEP /* min */, MAX_FILES_IN_DIR_MAX,
MAX_FILES_IN_DIR_STEP,
NULL, NULL, NULL),
+#ifdef HAVE_PERCEPTUAL_VOLUME
+ CHOICE_SETTING(0, volume_adjust_mode, LANG_VOLUME_ADJUST_MODE,
+ VOLUME_ADJUST_DIRECT, "volume adjustment mode",
+ "direct,perceptual", NULL, 2,
+ ID2P(LANG_DIRECT), ID2P(LANG_PERCEPTUAL)),
+ INT_SETTING_NOWRAP(0, volume_adjust_norm_steps, LANG_VOLUME_ADJUST_NORM_STEPS,
+ 50, "perceptual volume step count", UNIT_INT,
+ MIN_NORM_VOLUME_STEPS, MAX_NORM_VOLUME_STEPS, 5,
+ NULL, NULL, NULL),
+#endif
/* use this setting for user code even if there's no exchangable battery
* support enabled */
#if BATTERY_CAPACITY_INC > 0
diff --git a/firmware/export/config.h b/firmware/export/config.h
index 2ec0b7878f..0882cad61c 100644
--- a/firmware/export/config.h
+++ b/firmware/export/config.h
@@ -1319,6 +1319,10 @@ Lyre prototype 1 */
# define HAVE_SCREENDUMP
#endif
+#if !defined(BOOTLOADER) && MEMORYSIZE > 2
+# define HAVE_PERCEPTUAL_VOLUME
+#endif
+
/* null audiohw setting macro for when codec header is included for reasons
other than audio support */
#define AUDIOHW_SETTING(name, us, nd, st, minv, maxv, defv, expr...)
diff --git a/manual/configure_rockbox/system_options.tex b/manual/configure_rockbox/system_options.tex
index ba80a6e6e4..d4a282b445 100755
--- a/manual/configure_rockbox/system_options.tex
+++ b/manual/configure_rockbox/system_options.tex
@@ -137,6 +137,23 @@ This sub menu relates to limits in the Rockbox operating system.
\item LAN party computer $\rightarrow$ \dap $\rightarrow$ human
\end{itemize}
}
+\opt{perceptual_volume}{
+ \subsection{Volume Adjustment Mode}
+ This setting selects the method used to adjust volume with \ButtonVolUp{} and
+ \ButtonVolDown{}. In \setting{Direct} mode each volume step changes the volume
+ by a fixed number of decibels (dB).
+
+ In \setting{Perceptual} mode, the hardware volume range is divided into a
+ number of steps, controlled by the \setting{Number of Volume Steps} option.
+ Each step changes the volume by a variable number of decibels (dB) so the
+ perceived loudness changes by about the same amount at each step. The dB
+ change is smaller at high volumes and larger at low volumes, so a large
+ range of low volumes are effectively compressed into a smaller number of
+ volume steps.
+
+ \setting{Volume Adjustment Mode} does not affect how volume is displayed by
+ themes.
+}
\opt{quickscreen}{
\subsection{Use Shortcuts Menu Instead of Quick Screen} This option
activates the shortcuts menu instead of opening the quick screen when enabled.