From 5b5cd252d025a19bc73edb3bc47e943798dbe334 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Wed, 18 Feb 2026 17:41:36 +0000 Subject: [PATCH] pcm: improve 32-bit software volume scaling Replace the factor calculation from pcm-alsa.c, which is based on signal *power* ratios, with the fp_factor() calculation that is based on amplitude ratios. Because power changes with the square of amplitude, this means 1 dB of power equals 2 dB of amplitude. Rockbox's volume controls are amplitude-based, so the smallest step size for pcm-alsa was effectively 2 dB. The fp_factor() method supports 0.1 dB steps and is a bit more accurate besides, so it's simply superior in all respects (aside from taking a few more CPU cycles to calculate the factor). Change-Id: I34d143c225d8b5e085cde299fc405f83c13314bf --- firmware/pcm_sw_volume.c | 58 ++++++++----------- .../target/hosted/aigo/erosqlinux_codec.c | 6 +- firmware/target/hosted/fiio/fiiolinux_codec.c | 12 ++-- firmware/target/hosted/pcm-alsa.c | 45 +++----------- firmware/target/hosted/pcm-alsa.h | 4 +- .../target/hosted/sonynwz/nwzlinux-codec.c | 2 +- .../erosqnative/audiohw-erosqnative.c | 6 +- 7 files changed, 46 insertions(+), 87 deletions(-) diff --git a/firmware/pcm_sw_volume.c b/firmware/pcm_sw_volume.c index bee1559bb8..61a66d650a 100644 --- a/firmware/pcm_sw_volume.c +++ b/firmware/pcm_sw_volume.c @@ -51,23 +51,29 @@ static typeof (memcpy) *pcm_scaling_fn = NULL; /* take care of some defines for 32-bit software vol */ #if (PCM_NATIVE_BITDEPTH > 16) /* >16-bit */ - # define HAVE_SWVOL_32 -# define PCM_VOL_SAMPLE_SIZE (2 * sizeof (int32_t)) -# define PCM_DBL_BUF_SIZE_T int32_t - -# if !defined(PCM_DC_OFFSET_VALUE) -/* PCM_DC_OFFSET_VALUE is only needed due to hardware quirk on Eros Q */ -# define PCM_DC_OFFSET_VALUE 0 -# endif - +# define PCM_VOL_SAMPLE_SIZE (2 * sizeof (int32_t)) +# define PCM_DBL_BUF_SIZE_T int32_t #else /* 16-BIT */ - -# define PCM_VOL_SAMPLE_SIZE PCM_SAMPLE_SIZE -# define PCM_DBL_BUF_SIZE_T int16_t - +# define PCM_VOL_SAMPLE_SIZE (2 * sizeof (int16_t)) +# define PCM_DBL_BUF_SIZE_T int16_t #endif /* 16-BIT */ +#if !defined(PCM_DC_OFFSET_VALUE) +/* PCM_DC_OFFSET_VALUE is only needed due to hardware quirk on Eros Q */ +# define PCM_DC_OFFSET_VALUE 0 +#endif + +/* + * 16-bit samples are scaled up to an effective bit depth + * of (16+fracbits) bits and must be shifted to produce a + * native bit depth sample. Positive values correspond to + * right shifts (throwing away bits of the scaled result) + * while negative values correspond to left shifts (where + * the scaled result completely fits in the native sample). + */ +#define PCM_SCALE_SHIFT (16 + PCM_SW_VOLUME_FRACBITS - PCM_NATIVE_BITDEPTH) + /*** ** Volume scaling routines ** If unbuffered, called externally by pcm driver @@ -83,10 +89,10 @@ static typeof (memcpy) *pcm_scaling_fn = NULL; /* Scale sample by PCM factor */ static inline int32_t pcm_scale_sample(PCM_F_T f, int32_t s) { -#if defined(HAVE_SWVOL_32) - return (f * s + PCM_DC_OFFSET_VALUE) >> (32 - PCM_NATIVE_BITDEPTH); +#if PCM_SCALE_SHIFT > 0 + return (f * s + PCM_DC_OFFSET_VALUE) >> PCM_SCALE_SHIFT; #else - return (f * s) >> PCM_SW_VOLUME_FRACBITS; + return (f * s + PCM_DC_OFFSET_VALUE) << (-PCM_SCALE_SHIFT); #endif } @@ -347,28 +353,10 @@ static uint32_t pcm_centibels_to_factor(int volume) { if (volume == PCM_MUTE_LEVEL) return 0; /* mute */ -#if defined(HAVE_SWVOL_32) - /* - * 32-bit software volume taken from pcm-alsa.c - */ - volume += 48; /* -42dB .. 0dB => 5dB .. 48dB */ - /* NOTE if vol_dB = 5 then vol_shift = 1 but r = 1 so we do vol_shift - 1 >= 0 - * otherwise vol_dB >= 0 implies vol_shift >= 2 so vol_shift - 2 >= 0 */ - int vol_shift = volume / 3; - int r = volume % 3; - int32_t dig_vol_mult; - if(r == 0) - dig_vol_mult = 1 << vol_shift; - else if(r == 1) - dig_vol_mult = 1 << vol_shift | 1 << (vol_shift - 2); - else - dig_vol_mult = 1 << vol_shift | 1 << (vol_shift - 1); - return dig_vol_mult; -#else /* standard software volume */ + /* Centibels -> fixedpoint */ return (uint32_t)fp_factor(fp_div(volume, 10, PCM_SW_VOLUME_FRACBITS), PCM_SW_VOLUME_FRACBITS); -#endif /* HAVE_SWVOL_32 */ } diff --git a/firmware/target/hosted/aigo/erosqlinux_codec.c b/firmware/target/hosted/aigo/erosqlinux_codec.c index 7cb1ef5f15..274d77ddb2 100644 --- a/firmware/target/hosted/aigo/erosqlinux_codec.c +++ b/firmware/target/hosted/aigo/erosqlinux_codec.c @@ -204,7 +204,7 @@ static void audiohw_set_volume_v1(int vol_l, int vol_r) int sw_volume_l = l <= min_pcm ? min_pcm : MIN(l, max_pcm); int sw_volume_r = r <= min_pcm ? min_pcm : MIN(r, max_pcm); - pcm_set_mixer_volume(sw_volume_l / 20, sw_volume_r / 20); + pcm_set_mixer_volume(sw_volume_l, sw_volume_r); } static void audiohw_set_volume_v2(int vol_l, int vol_r) @@ -236,7 +236,7 @@ static void audiohw_set_volume_v2(int vol_l, int vol_r) alsa_controls_set_ints("Right Playback Volume", 1, &r); /* Dial back PCM mixer to avoid compression */ - pcm_set_mixer_volume(global_settings.volume_limit / 2, global_settings.volume_limit / 2); + pcm_set_mixer_volume(global_settings.volume_limit, global_settings.volume_limit); } void audiohw_set_volume(int vol_l, int vol_r) @@ -274,7 +274,7 @@ void audiohw_set_lineout_volume(int vol_l, int vol_r) } else { int sw_volume_l = l <= min_pcm ? min_pcm : MIN(l, max_pcm); int sw_volume_r = r <= min_pcm ? min_pcm : MIN(r, max_pcm); - pcm_set_mixer_volume(sw_volume_l / 20, sw_volume_r / 20); + pcm_set_mixer_volume(sw_volume_l, sw_volume_r); } } } diff --git a/firmware/target/hosted/fiio/fiiolinux_codec.c b/firmware/target/hosted/fiio/fiiolinux_codec.c index 99cff3f5e4..397e58286f 100644 --- a/firmware/target/hosted/fiio/fiiolinux_codec.c +++ b/firmware/target/hosted/fiio/fiiolinux_codec.c @@ -95,21 +95,21 @@ void audiohw_set_volume(int vol_l, int vol_r) if (ak_hw < 0) return; - vol[0] = vol_l / 20; - vol[1] = vol_r / 20; + vol[0] = vol_l; + vol[1] = vol_r; for (int i = 0; i < 2; i++) { - if (vol[i] > -56) + if (vol[i] > -1120) { - if (vol[i] < -12) + if (vol[i] < -240) { vol_hw[i] = 1; - vol_sw[i] = vol[i] + 12; + vol_sw[i] = vol[i] + 240; } else { - vol_hw[i] = 25 - (-vol[i] * 2); + vol_hw[i] = 25 - (-vol[i] / 10); vol_sw[i] = 0; } } diff --git a/firmware/target/hosted/pcm-alsa.c b/firmware/target/hosted/pcm-alsa.c index f6347cea73..9286369a25 100644 --- a/firmware/target/hosted/pcm-alsa.c +++ b/firmware/target/hosted/pcm-alsa.c @@ -50,6 +50,7 @@ #include "pcm_sampr.h" #include "audiohw.h" #include "pcm-alsa.h" +#include "fixedpoint.h" #include "logf.h" @@ -267,46 +268,14 @@ error: } #if defined(HAVE_ALSA_32BIT) -/* Digital volume explanation: - * with very good approximation (<0.1dB) the convertion from dB to multiplicative - * factor, for dB>=0, is 2^(dB/3). We can then notice that if we write dB=3*k+r - * then this is 2^k*2^(r/3) so we only need to look at r=0,1,2. For r=0 this is - * 1, for r=1 we have 2^(1/3)~=1.25 so we approximate by 1+1/4, and 2^(2/3)~=1.5 - * so we approximate by 1+1/2. To go from negative to nonnegative we notice that - * 48 dB => 63095 factor ~= 2^16 so we virtually pre-multiply everything by 2^(-16) - * and add 48dB to the input volume. We cannot go lower -43dB because several - * values between -48dB and -43dB would require a fractional multiplier, which is - * stupid to implement for such very low volume. */ -static int dig_vol_mult_l = 2 << 16; /* multiplicative factor to apply to each sample */ -static int dig_vol_mult_r = 2 << 16; /* multiplicative factor to apply to each sample */ +/* Multiplicative factors applied to each sample */ +static int32_t dig_vol_mult_l = 0; +static int32_t dig_vol_mult_r = 0; + void pcm_set_mixer_volume(int vol_db_l, int vol_db_r) { - if(vol_db_l > 0 || vol_db_r > 0 || vol_db_l < -43 || vol_db_r < -43) - panicf("invalid pcm alsa volume %d %d", vol_db_l, vol_db_r); - if(format != SND_PCM_FORMAT_S32_LE) - panicf("this function assumes 32-bit sample size"); - vol_db_l += 48; /* -42dB .. 0dB => 5dB .. 48dB */ - vol_db_r += 48; /* -42dB .. 0dB => 5dB .. 48dB */ - /* NOTE if vol_dB = 5 then vol_shift = 1 but r = 1 so we do vol_shift - 1 >= 0 - * otherwise vol_dB >= 0 implies vol_shift >= 2 so vol_shift - 2 >= 0 */ - int vol_shift_l = vol_db_l / 3; - int vol_shift_r = vol_db_r / 3; - int r_l = vol_db_l % 3; - int r_r = vol_db_r % 3; - if(r_l == 0) - dig_vol_mult_l = 1 << vol_shift_l; - else if(r_l == 1) - dig_vol_mult_l = 1 << vol_shift_l | 1 << (vol_shift_l - 2); - else - dig_vol_mult_l = 1 << vol_shift_l | 1 << (vol_shift_l - 1); - logf("l: %d dB -> factor = %d", vol_db_l - 48, dig_vol_mult_l); - if(r_r == 0) - dig_vol_mult_r = 1 << vol_shift_r; - else if(r_r == 1) - dig_vol_mult_r = 1 << vol_shift_r | 1 << (vol_shift_r - 2); - else - dig_vol_mult_r = 1 << vol_shift_r | 1 << (vol_shift_r - 1); - logf("r: %d dB -> factor = %d", vol_db_r - 48, dig_vol_mult_r); + dig_vol_mult_l = fp_factor(fp_div(vol_db_l, 10, 16), 16); + dig_vol_mult_r = fp_factor(fp_div(vol_db_r, 10, 16), 16); } #endif diff --git a/firmware/target/hosted/pcm-alsa.h b/firmware/target/hosted/pcm-alsa.h index f35be60074..3fa6ffe9db 100644 --- a/firmware/target/hosted/pcm-alsa.h +++ b/firmware/target/hosted/pcm-alsa.h @@ -21,10 +21,12 @@ #define __PCM_ALSA_RB_H__ #include +#include #if defined(HAVE_ALSA_32BIT) + /* Set the PCM volume in dB: each sample with have this volume applied digitally - * before being sent to ALSA. Volume must satisfy -43 <= dB <= 0 */ + * before being sent to ALSA. Effective range -79 dB to 0 dB */ void pcm_set_mixer_volume(int vol_db_l, int vol_db_r); #endif diff --git a/firmware/target/hosted/sonynwz/nwzlinux-codec.c b/firmware/target/hosted/sonynwz/nwzlinux-codec.c index 5085befb20..52ff9871bc 100644 --- a/firmware/target/hosted/sonynwz/nwzlinux-codec.c +++ b/firmware/target/hosted/sonynwz/nwzlinux-codec.c @@ -415,7 +415,7 @@ void audiohw_set_volume(int vol_l, int vol_r) printf(" set driver volume %d (%d dB)\n", drv_vol, curve->level[drv_vol] / 10); nwz_set_driver_vol(drv_vol); printf(" set digital volume %d dB\n", vol / 10); - pcm_set_mixer_volume(vol / 10, vol / 10); + pcm_set_mixer_volume(vol, vol); } void audiohw_close(void) diff --git a/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c b/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c index 353badaef8..3fc774cfdd 100644 --- a/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c +++ b/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c @@ -200,8 +200,8 @@ void audiohw_set_volume(int vol_l, int vol_r) } else /* PCM5102A */ { - l = l <= PCM5102A_VOLUME_MIN ? PCM_MUTE_LEVEL : (l / 20); - r = r <= PCM5102A_VOLUME_MIN ? PCM_MUTE_LEVEL : (r / 20); + l = l <= PCM5102A_VOLUME_MIN ? PCM_MUTE_LEVEL : l; + r = r <= PCM5102A_VOLUME_MIN ? PCM_MUTE_LEVEL : r; pcm_set_master_volume(l, r); } @@ -214,4 +214,4 @@ void audiohw_set_filter_roll_off(int value) es9018k2m_set_filter_roll_off(value); } } -#endif /* !defined(BOOTLOADER) */ \ No newline at end of file +#endif /* !defined(BOOTLOADER) */