From 5b27e2255a8bbb8645f089b8e721343bba5bd396 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Tue, 22 Nov 2022 04:10:35 +0000 Subject: [PATCH] Add perceptual volume adjustment The perceived loudness change of a change in volume depends on the listening volume: at high volumes a 1 dB increment is noticeable, but at low volumes a larger increment is needed to get a comparable change in loudness. Perceptual volume adjustment accounts for this fact, and divides the hardware volume range into a number of steps. Each step changes the dB volume by a variable amount, with most of the steps concentrated at higher volumes. This makes it possible to sweep over the entire hardware volume range quickly, without losing the ability to finely adjust the volume at normal listening levels. Use "Volume Adjustment Mode" in the system settings menu to select perceptual volume mode. The number of steps used is controlled by "Number of Volume Steps". (Number of steps has no effect in direct adjustment mode.) It's still possible to set a specific dB volume level from the sound settings menu when perceptual volume is enabled, and perceptual volume does not affect the volume displayed by themes. Change-Id: I6f91fd3f7c5e2d323a914e47b5653033e92b4b3b --- apps/features.txt | 4 + apps/gui/list.c | 6 +- apps/gui/quickscreen.c | 6 +- apps/gui/wps.c | 4 +- apps/lang/english.lang | 53 ++++++++- apps/menus/settings_menu.c | 9 ++ apps/misc.c | 107 ++++++++++++++++++ apps/misc.h | 14 +++ apps/plugin.c | 1 + apps/plugin.h | 3 +- apps/plugins/lrcplayer.c | 10 +- apps/plugins/mikmod/mikmod.c | 19 +--- .../plugins/sdl/SDL_mixer/timidity/playmidi.c | 2 + apps/settings.h | 5 + apps/settings_list.c | 11 ++ firmware/export/config.h | 4 + manual/configure_rockbox/system_options.tex | 17 +++ 17 files changed, 240 insertions(+), 35 deletions(-) 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.