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
This commit is contained in:
Aidan MacDonald 2022-11-22 04:10:35 +00:00
parent 15c4447b66
commit 5b27e2255a
17 changed files with 240 additions and 35 deletions

View file

@ -296,3 +296,7 @@ hibylinux
(CONFIG_KEYPAD == IRIVER_H10_PAD)
clear_settings_on_hold
#endif
#if defined(HAVE_PERCEPTUAL_VOLUME)
perceptual_volume
#endif

View file

@ -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:

View file

@ -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);
}

View file

@ -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)

View file

@ -15163,7 +15163,7 @@
</phrase>
<phrase>
id: LANG_DIRECT
desc: in the pictureflow settings
desc: in the pictureflow settings, also a volume adjustment mode
user: core
<source>
*: "Direct"
@ -16559,3 +16559,54 @@
*: "Play Last Shuffled"
</voice>
</phrase>
<phrase>
id: LANG_VOLUME_ADJUST_MODE
desc: in system settings
user: core
<source>
*: none
perceptual_volume: "Volume Adjustment Mode"
</source>
<dest>
*: none
perceptual_volume: "Volume Adjustment Mode"
</dest>
<voice>
*: none
perceptual_volume: "Volume Adjustment Mode"
</voice>
</phrase>
<phrase>
id: LANG_VOLUME_ADJUST_NORM_STEPS
desc: in system settings
user: core
<source>
*: none
perceptual_volume: "Number of Volume Steps"
</source>
<dest>
*: none
perceptual_volume: "Number of Volume Steps"
</dest>
<voice>
*: none
perceptual_volume: "Number of Volume Steps"
</voice>
</phrase>
<phrase>
id: LANG_PERCEPTUAL
desc: in system settings -> volume adjustment mode
user: core
<source>
*: none
perceptual_volume: "Perceptual"
</source>
<dest>
*: none
perceptual_volume: "Perceptual"
</dest>
<voice>
*: none
perceptual_volume: "Perceptual"
</voice>
</phrase>

View file

@ -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

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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 */

View file

@ -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;

View file

@ -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:

View file

@ -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;

View file

@ -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;

View file

@ -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

View file

@ -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...)

View file

@ -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.