sound, improved and optimized reimplementation of libretro lowpass filter

This commit is contained in:
kub 2020-12-21 23:22:00 +01:00
parent b437951ade
commit 30969671e5
5 changed files with 141 additions and 123 deletions

View file

@ -1,6 +1,7 @@
/* /*
* some code for sample mixing * some code for sample mixing
* (C) notaz, 2006,2007 * (C) notaz, 2006,2007
* (C) kub, 2019,2020 added filtering
* *
* This work is licensed under the terms of MAME license. * This work is licensed under the terms of MAME license.
* See COPYING file in the top-level directory. * See COPYING file in the top-level directory.
@ -13,78 +14,97 @@
/* limitter */ /* limitter */
#define Limit16(val) \ #define Limit16(val) \
val -= val >> 2; /* reduce level to avoid clipping */ \
if ((short)val != val) val = (val < 0 ? MINOUT : MAXOUT) if ((short)val != val) val = (val < 0 ? MINOUT : MAXOUT)
int mix_32_to_16l_level; int mix_32_to_16l_level;
static struct iir2 { // 2-pole IIR static struct iir {
int x[2]; // sample buffer int alpha; // alpha for EMA low pass
int y[2]; // filter intermediates int y[2]; // filter intermediates
int i;
} lfi2, rfi2; } lfi2, rfi2;
// NB ">>" rounds to -infinity, "/" to 0. To compensate the effect possibly use // NB ">>" rounds to -infinity, "/" to 0. To compensate the effect possibly use
// "-(-y>>n)" (round to +infinity) instead of "y>>n" in places. // "-(-y>>n)" (round to +infinity) instead of "y>>n" in places.
// NB uses Q12 fixpoint; samples mustn't have more than 20 bits for this. // NB uses fixpoint; samples mustn't have more than (32-QB) bits. Adding the
// outputs of the sound sources together yields a max. of 18 bits, restricting
// QB to a maximum of 14.
#define QB 12 #define QB 12
// NB alpha for DC filtering shouldn't be smaller than 1/(1<<QB) to avoid loss.
// exponential moving average combined DC filter and lowpass filter
// y0[n] = (x[n]-y0[n-1])*alpha+y0[n-1], y1[n] = (y0[n] - y1[n-1])*(1-1/8192)
static inline int filter_band(struct iir *fi2, int x)
{
// low pass. alpha is Q8 to avoid loss by 32 bit overflow.
// fi2->y[0] += ((x<<(QB-8)) - (fi2->y[0]>>8)) * fi2->alpha;
fi2->y[0] += (x - (fi2->y[0]>>QB)) * fi2->alpha;
// DC filter. for alpha=1-1/8192 cutoff ~1HZ, for 1-1/1024 ~7Hz
fi2->y[1] += (fi2->y[0] - fi2->y[1]) >> QB;
return (fi2->y[0] - fi2->y[1]) >> QB;
}
// exponential moving average filter for DC filtering // exponential moving average filter for DC filtering
// y[n] = (x[n]-y[n-1])*(1/8192) (corner approx. 20Hz, gain 1) // y[n] = (x[n]-y[n-1])*(1-1/8192) (corner approx. 1Hz, gain 1)
static inline int filter_exp(struct iir2 *fi2, int x) static inline int filter_exp(struct iir *fi2, int x)
{ {
int xf = (x<<QB) - fi2->y[0]; fi2->y[1] += ((x << QB) - fi2->y[1]) >> QB;
fi2->y[0] += xf >> 13; return x - (fi2->y[1] >> QB);
xf -= xf >> 2; // level reduction to avoid clipping from overshoot
return xf>>QB;
} }
// unfiltered (for testing) // unfiltered (for testing)
static inline int filter_null(struct iir2 *fi2, int x) static inline int filter_null(struct iir *fi2, int x)
{ {
return x; return x;
} }
#define filter filter_band
#define mix_32_to_16l_stereo_core(dest, src, count, lv, fl) { \ #define mix_32_to_16l_stereo_core(dest, src, count, lv, fl) { \
int l, r; \ int l, r; \
struct iir lf = lfi2, rf = rfi2; \
\ \
for (; count > 0; count--) \ for (; count > 0; count--) \
{ \ { \
l = r = *dest; \ l = r = *dest; \
l += *src++ >> lv; \ l += *src++ >> lv; \
r += *src++ >> lv; \ r += *src++ >> lv; \
l = fl(&lfi2, l); \ l = fl(&lf, l); \
r = fl(&rfi2, r); \ r = fl(&rf, r); \
Limit16(l); \ Limit16(l); \
Limit16(r); \ Limit16(r); \
*dest++ = l; \ *dest++ = l; \
*dest++ = r; \ *dest++ = r; \
} \ } \
lfi2 = lf, rfi2 = rf; \
} }
void mix_32_to_16l_stereo_lvl(short *dest, int *src, int count) void mix_32_to_16l_stereo_lvl(short *dest, int *src, int count)
{ {
mix_32_to_16l_stereo_core(dest, src, count, mix_32_to_16l_level, filter_exp); mix_32_to_16l_stereo_core(dest, src, count, mix_32_to_16l_level, filter);
} }
void mix_32_to_16l_stereo(short *dest, int *src, int count) void mix_32_to_16l_stereo(short *dest, int *src, int count)
{ {
mix_32_to_16l_stereo_core(dest, src, count, 0, filter_exp); mix_32_to_16l_stereo_core(dest, src, count, 0, filter);
} }
void mix_32_to_16_mono(short *dest, int *src, int count) void mix_32_to_16_mono(short *dest, int *src, int count)
{ {
int l; int l;
struct iir lf = lfi2;
for (; count > 0; count--) for (; count > 0; count--)
{ {
l = *dest; l = *dest;
l += *src++; l += *src++;
l = filter_exp(&lfi2, l); l = filter(&lf, l);
Limit16(l); Limit16(l);
*dest++ = l; *dest++ = l;
} }
lfi2 = lf;
} }
@ -118,8 +138,9 @@ void mix_16h_to_32_s2(int *dest_buf, short *mp3_buf, int count)
} }
} }
void mix_reset(void) void mix_reset(int alpha_q16)
{ {
memset(&lfi2, 0, sizeof(lfi2)); memset(&lfi2, 0, sizeof(lfi2));
memset(&rfi2, 0, sizeof(rfi2)); memset(&rfi2, 0, sizeof(rfi2));
lfi2.alpha = rfi2.alpha = (0x10000-alpha_q16) >> 4; // filter alpha, Q12
} }

View file

@ -8,4 +8,4 @@ void mix_32_to_16_mono(short *dest, int *src, int count);
extern int mix_32_to_16l_level; extern int mix_32_to_16l_level;
void mix_32_to_16l_stereo_lvl(short *dest, int *src, int count); void mix_32_to_16l_stereo_lvl(short *dest, int *src, int count);
void mix_reset(void); void mix_reset(int alpha_q16);

View file

@ -154,34 +154,46 @@ m16_32_s2_no_unal2:
@ limit @ limit
@ reg=int_sample, lr=1, r3=tmp, kills flags @ reg=int_sample, r12=1, r8=tmp, kills flags
.macro Limit reg .macro Limit reg
add r3, lr, \reg, asr #15 sub \reg, \reg, \reg, asr #2 @ reduce audio lvl some to avoid clipping
bics r3, r3, #1 @ in non-overflow conditions r3 is 0 or 1 add r8, r12, \reg, asr #15
bics r8, r8, #1 @ in non-overflow conditions r8 is 0 or 1
movne \reg, #0x8000 movne \reg, #0x8000
subpl \reg, \reg, #1 subpl \reg, \reg, #1
.endm .endm
@ limit and shift up by 16 @ limit and shift up by 16
@ reg=int_sample, lr=1, r3=tmp, kills flags @ reg=int_sample, r12=1, r8=tmp, kills flags
.macro Limitsh reg .macro Limitsh reg
add r3, lr, \reg, asr #15 sub \reg, \reg, \reg, asr #2 @ reduce audio lvl some to avoid clipping
bics r3, r3, #1 @ in non-overflow conditions r3 is 0 or 1 add r8, r12,\reg, asr #15
bics r8, r8, #1 @ in non-overflow conditions r8 is 0 or 1
moveq \reg, \reg, lsl #16 moveq \reg, \reg, lsl #16
movne \reg, #0x80000000 movne \reg, #0x80000000
subpl \reg, \reg, #0x00010000 subpl \reg, \reg, #0x00010000
.endm .endm
@ filter out DC offset @ filter out DC offset
@ in=int_sample (max 20 bit), y=filter memory, r3=tmp @ in=int_sample (max 20 bit), y=filter memory, r8=tmp
.macro DCfilt in y .macro DCfilt in y
rsb r3, \y, \in, lsl #12 @ fixpoint 20.12 rsb r8, \y, \in, lsl #12 @ fixpoint 20.12
add \y, \y, r3, asr #13 add \y, \y, r8, asr #12 @ alpha = 1-1/4094
sub r3, r3, r3, asr #2 @ reduce audio lvl some sub \in, \in, \y, asr #12
asr \in, r3, #12
.endm .endm
@ lowpass filter
@ in=int_sample (max 20 bit), y=filter memory, r12=alpha(Q8), r8=tmp
.macro LPfilt in y
@ asr r8, \y, #8
@ rsb r8, r8, \in, lsl #4 @ fixpoint 20.12
sub r8, \in, \y, asr #12 @ fixpoint 20.12
mla \y, r8, r12, \y
asr \in, \y, #12
.endm
@ mix 32bit audio (with 16bits really used, upper bits indicate overflow) with normal 16 bit audio with left channel only @ mix 32bit audio (with 16bits really used, upper bits indicate overflow) with normal 16 bit audio with left channel only
@ warning: this function assumes dest is word aligned @ warning: this function assumes dest is word aligned
.global mix_32_to_16l_stereo @ short *dest, int *src, int count .global mix_32_to_16l_stereo @ short *dest, int *src, int count
@ -193,9 +205,10 @@ mix_32_to_16l_stereo:
subs r2, r2, #4 subs r2, r2, #4
bmi m32_16l_st_end bmi m32_16l_st_end
mov lr, #1
ldr r12, =filter ldr r12, =filter
ldmia r12, {r10-r11} ldr r8, [r12], #4
ldmia r12, {r3,r10-r11,lr}
str r8, [sp, #-4]!
m32_16l_st_loop: m32_16l_st_loop:
ldmia r0, {r8,r12} ldmia r0, {r8,r12}
@ -206,10 +219,16 @@ m32_16l_st_loop:
add r5, r5, r8, asr #16 add r5, r5, r8, asr #16
add r6, r6, r12,asr #16 add r6, r6, r12,asr #16
add r7, r7, r12,asr #16 add r7, r7, r12,asr #16
ldr r12,[sp]
LPfilt r4, r3
LPfilt r5, lr
LPfilt r6, r3
LPfilt r7, lr
DCfilt r4, r10 DCfilt r4, r10
DCfilt r5, r11 DCfilt r5, r11
DCfilt r6, r10 DCfilt r6, r10
DCfilt r7, r11 DCfilt r7, r11
mov r12,#1
Limitsh r4 Limitsh r4
Limitsh r5 Limitsh r5
Limitsh r6 Limitsh r6
@ -228,8 +247,12 @@ m32_16l_st_end:
ldmia r1!,{r4,r5} ldmia r1!,{r4,r5}
add r4, r4, r6 add r4, r4, r6
add r5, r5, r6 add r5, r5, r6
ldr r12,[sp]
LPfilt r4, r3
LPfilt r5, lr
DCfilt r4, r10 DCfilt r4, r10
DCfilt r5, r11 DCfilt r5, r11
mov r12,#1
Limitsh r4 Limitsh r4
Limitsh r5 Limitsh r5
orr r4, r5, r4, lsr #16 orr r4, r5, r4, lsr #16
@ -237,7 +260,9 @@ m32_16l_st_end:
m32_16l_st_no_unal2: m32_16l_st_no_unal2:
ldr r12, =filter ldr r12, =filter
stmia r12, {r10-r11} add r12,r12, #4
stmia r12, {r3,r10-r11,lr}
add sp, sp, #4
ldmfd sp!, {r4-r8,r10-r11,lr} ldmfd sp!, {r4-r8,r10-r11,lr}
bx lr bx lr
@ -248,9 +273,10 @@ m32_16l_st_no_unal2:
mix_32_to_16_mono: mix_32_to_16_mono:
stmfd sp!, {r4-r8,r10-r11,lr} stmfd sp!, {r4-r8,r10-r11,lr}
mov lr, #1
ldr r12, =filter ldr r12, =filter
ldr r10, [r12] ldr r8, [r12], #4
ldmia r12, {r10-r11}
str r8, [sp, #-4]!
@ check if dest is word aligned @ check if dest is word aligned
tst r0, #2 tst r0, #2
@ -259,6 +285,10 @@ mix_32_to_16_mono:
ldr r4, [r1], #4 ldr r4, [r1], #4
sub r2, r2, #1 sub r2, r2, #1
add r4, r4, r5 add r4, r4, r5
ldr r12,[sp]
LPfilt r4, r11
DCfilt r4, r10
mov r12,#1
Limit r4 Limit r4
strh r4, [r0], #2 strh r4, [r0], #2
@ -275,10 +305,16 @@ m32_16_mo_loop:
add r7, r7, r12,asr #16 add r7, r7, r12,asr #16
mov r12,r12,lsl #16 mov r12,r12,lsl #16
add r6, r6, r12,asr #16 add r6, r6, r12,asr #16
ldr r12,[sp]
LPfilt r4, r11
LPfilt r5, r11
LPfilt r6, r11
LPfilt r7, r11
DCfilt r4, r10 DCfilt r4, r10
DCfilt r5, r10 DCfilt r5, r10
DCfilt r6, r10 DCfilt r6, r10
DCfilt r7, r10 DCfilt r7, r10
mov r12,#1
Limitsh r4 Limitsh r4
Limitsh r5 Limitsh r5
Limitsh r6 Limitsh r6
@ -298,8 +334,12 @@ m32_16_mo_end:
add r5, r5, r6, asr #16 add r5, r5, r6, asr #16
mov r6, r6, lsl #16 mov r6, r6, lsl #16
add r4, r4, r6, asr #16 add r4, r4, r6, asr #16
ldr r12,[sp]
LPfilt r4, r11
LPfilt r5, r11
DCfilt r4, r10 DCfilt r4, r10
DCfilt r5, r10 DCfilt r5, r10
mov r12,#1
Limitsh r4 Limitsh r4
Limitsh r5 Limitsh r5
orr r4, r5, r4, lsr #16 orr r4, r5, r4, lsr #16
@ -311,13 +351,18 @@ m32_16_mo_no_unal2:
ldrsh r5, [r0] ldrsh r5, [r0]
ldr r4, [r1], #4 ldr r4, [r1], #4
add r4, r4, r5 add r4, r4, r5
ldr r12,[sp]
LPfilt r4, r11
DCfilt r4, r10 DCfilt r4, r10
mov r12,#1
Limit r4 Limit r4
strh r4, [r0], #2 strh r4, [r0], #2
m32_16_mo_no_unal: m32_16_mo_no_unal:
ldr r12, =filter ldr r12, =filter
str r10, [r12] add r12,r12, #4
stmia r12, {r10-r11}
add sp, sp, #4
ldmfd sp!, {r4-r8,r10-r11,lr} ldmfd sp!, {r4-r8,r10-r11,lr}
bx lr bx lr
@ -344,7 +389,9 @@ mix_32_to_16l_stereo_lvl:
mov lr, #1 mov lr, #1
ldr r9, [r9] ldr r9, [r9]
ldr r12, =filter ldr r12, =filter
ldm r12, {r10-r11} ldr r8, [r12], #4
ldmia r12, {r3,r10-r11,lr}
str r8, [sp, #-4]!
mov r2, r2, lsl #1 mov r2, r2, lsl #1
subs r2, r2, #4 subs r2, r2, #4
@ -363,10 +410,16 @@ m32_16l_st_l_loop:
mov r5, r5, asr r9 mov r5, r5, asr r9
mov r6, r6, asr r9 mov r6, r6, asr r9
mov r7, r7, asr r9 mov r7, r7, asr r9
ldr r12,[sp]
LPfilt r4, r3
LPfilt r5, lr
LPfilt r6, r3
LPfilt r7, lr
DCfilt r4, r10 DCfilt r4, r10
DCfilt r5, r11 DCfilt r5, r11
DCfilt r6, r10 DCfilt r6, r10
DCfilt r7, r11 DCfilt r7, r11
mov r12,#1
Limitsh r4 Limitsh r4
Limitsh r5 Limitsh r5
Limitsh r6 Limitsh r6
@ -387,8 +440,12 @@ m32_16l_st_l_end:
add r5, r5, r6 add r5, r5, r6
mov r4, r4, asr r9 mov r4, r4, asr r9
mov r5, r5, asr r9 mov r5, r5, asr r9
ldr r12,[sp]
LPfilt r4, r3
LPfilt r5, lr
DCfilt r4, r10 DCfilt r4, r10
DCfilt r5, r11 DCfilt r5, r11
mov r12,#1
Limitsh r4 Limitsh r4
Limitsh r5 Limitsh r5
orr r4, r5, r4, lsr #16 orr r4, r5, r4, lsr #16
@ -396,22 +453,32 @@ m32_16l_st_l_end:
m32_16l_st_l_no_unal2: m32_16l_st_l_no_unal2:
ldr r12, =filter ldr r12, =filter
stmia r12, {r10-r11} add r12,r12, #4
stmia r12, {r3,r10-r11,lr}
add sp, sp, #4
ldmfd sp!, {r4-r11,lr} ldmfd sp!, {r4-r11,lr}
bx lr bx lr
#endif /* __GP2X__ */ #endif /* __GP2X__ */
.global mix_reset @ void .global mix_reset @ int alpha_q16
mix_reset: mix_reset:
ldr r0, =filter ldr r2, =filter
rsb r0, r0, #0x10000
@ asr r0, r0, #8
asr r0, r0, #4
str r0, [r2], #4
mov r1, #0 mov r1, #0
str r1, [r0] str r1, [r2], #4
str r1, [r0, #4] str r1, [r2], #4
str r1, [r2], #4
str r1, [r2], #4
bx lr bx lr
.data .data
filter: filter:
.ds 8 .ds 4 @ alpha_q8
.ds 8 @ filter history for left channel
.ds 8 @ filter history for right channel
@ vim:filetype=armasm @ vim:filetype=armasm

View file

@ -26,73 +26,6 @@ short cdda_out_buffer[2*1152];
// sn76496 // sn76496
extern int *sn76496_regs; extern int *sn76496_regs;
// Low pass filter 'previous' samples
static int32_t lpf_lp;
static int32_t lpf_rp;
static void low_pass_filter_stereo(int *buf32, int length)
{
int samples = length;
int *out32 = buf32;
// Restore previous samples
int32_t lpf_l = lpf_lp;
int32_t lpf_r = lpf_rp;
// Single-pole low-pass filter (6 dB/octave)
int32_t factor_a = PicoIn.sndFilterRange;
int32_t factor_b = 0x10000 - factor_a;
do
{
// Apply low-pass filter
lpf_l = (lpf_l * factor_a) + (out32[0] * factor_b);
lpf_r = (lpf_r * factor_a) + (out32[1] * factor_b);
// 16.16 fixed point
lpf_l >>= 16;
lpf_r >>= 16;
// Update sound buffer
*out32++ = lpf_l;
*out32++ = lpf_r;
}
while (--samples);
// Save last samples for next frame
lpf_lp = lpf_l;
lpf_rp = lpf_r;
}
static void low_pass_filter_mono(int *buf32, int length)
{
int samples = length;
int *out32 = buf32;
// Restore previous sample
int32_t lpf_l = lpf_lp;
// Single-pole low-pass filter (6 dB/octave)
int32_t factor_a = PicoIn.sndFilterRange;
int32_t factor_b = 0x10000 - factor_a;
do
{
// Apply low-pass filter
lpf_l = (lpf_l * factor_a) + (out32[0] * factor_b);
// 16.16 fixed point
lpf_l >>= 16;
// Update sound buffer
*out32++ = lpf_l;
}
while (--samples);
// Save last sample for next frame
lpf_lp = lpf_l;
}
void (*low_pass_filter)(int *buf32, int length) = low_pass_filter_stereo;
// ym2413 // ym2413
#define YM2413_CLK 3579545 #define YM2413_CLK 3579545
OPLL old_opll; OPLL old_opll;
@ -119,11 +52,7 @@ PICO_INTERNAL void PsndReset(void)
PsndRerate(0); PsndRerate(0);
timers_reset(); timers_reset();
// Reset low pass filter mix_reset(PicoIn.sndFilter ? PicoIn.sndFilterRange : 0);
lpf_lp = 0;
lpf_rp = 0;
mix_reset();
} }
@ -179,9 +108,6 @@ void PsndRerate(int preserve_state)
// set mixer // set mixer
PsndMix_32_to_16l = (PicoIn.opt & POPT_EN_STEREO) ? mix_32_to_16l_stereo : mix_32_to_16_mono; PsndMix_32_to_16l = (PicoIn.opt & POPT_EN_STEREO) ? mix_32_to_16l_stereo : mix_32_to_16_mono;
// set low pass filter
low_pass_filter = (PicoIn.opt & POPT_EN_STEREO) ? low_pass_filter_stereo : low_pass_filter_mono;
if (PicoIn.AHW & PAHW_PICO) if (PicoIn.AHW & PAHW_PICO)
PicoReratePico(); PicoReratePico();
} }
@ -463,11 +389,6 @@ static int PsndRender(int offset, int length)
if ((PicoIn.AHW & PAHW_32X) && (PicoIn.opt & POPT_EN_PWM)) if ((PicoIn.AHW & PAHW_32X) && (PicoIn.opt & POPT_EN_PWM))
p32x_pwm_update(buf32, length-offset, stereo); p32x_pwm_update(buf32, length-offset, stereo);
// Apply low pass filter, if required
if (PicoIn.sndFilter == 1) {
low_pass_filter(buf32, length);
}
// convert + limit to normal 16bit output // convert + limit to normal 16bit output
PsndMix_32_to_16l(PicoIn.sndOut+(offset<<stereo), buf32, length-offset); PsndMix_32_to_16l(PicoIn.sndOut+(offset<<stereo), buf32, length-offset);

View file

@ -70,6 +70,7 @@ int _newlib_vm_size_user = 1 << TARGET_SIZE_2;
#include <pico/pico_int.h> #include <pico/pico_int.h>
#include <pico/state.h> #include <pico/state.h>
#include <pico/patch.h> #include <pico/patch.h>
#include <pico/sound/mix.h>
#include "../common/input_pico.h" #include "../common/input_pico.h"
#include "../common/version.h" #include "../common/version.h"
#include <libretro.h> #include <libretro.h>
@ -1434,6 +1435,8 @@ static void update_variables(bool first_run)
unsigned old_frameskip_type; unsigned old_frameskip_type;
int old_vout_format; int old_vout_format;
double new_sound_rate; double new_sound_rate;
unsigned short old_snd_filter;
int32_t old_snd_filter_range;
var.value = NULL; var.value = NULL;
var.key = "picodrive_input1"; var.key = "picodrive_input1";
@ -1539,6 +1542,7 @@ static void update_variables(bool first_run)
PicoIn.opt &= ~POPT_EN_DRC; PicoIn.opt &= ~POPT_EN_DRC;
#endif #endif
old_snd_filter = PicoIn.sndFilter;
var.value = NULL; var.value = NULL;
var.key = "picodrive_audio_filter"; var.key = "picodrive_audio_filter";
PicoIn.sndFilter = 0; PicoIn.sndFilter = 0;
@ -1547,6 +1551,7 @@ static void update_variables(bool first_run)
PicoIn.sndFilter = 1; PicoIn.sndFilter = 1;
} }
old_snd_filter_range = PicoIn.sndFilterRange;
var.value = NULL; var.value = NULL;
var.key = "picodrive_lowpass_range"; var.key = "picodrive_lowpass_range";
PicoIn.sndFilterRange = (60 * 65536) / 100; PicoIn.sndFilterRange = (60 * 65536) / 100;
@ -1554,6 +1559,10 @@ static void update_variables(bool first_run)
PicoIn.sndFilterRange = (atoi(var.value) * 65536) / 100; PicoIn.sndFilterRange = (atoi(var.value) * 65536) / 100;
} }
if (old_snd_filter != PicoIn.sndFilter || old_snd_filter_range != PicoIn.sndFilterRange) {
mix_reset(PicoIn.sndFilter ? PicoIn.sndFilterRange : 0);
}
old_frameskip_type = frameskip_type; old_frameskip_type = frameskip_type;
frameskip_type = 0; frameskip_type = 0;
var.key = "picodrive_frameskip"; var.key = "picodrive_frameskip";