Skip to content

Commit

Permalink
Smooth linear filter parameter change (#1432)
Browse files Browse the repository at this point in the history
  • Loading branch information
derselbst authored Dec 1, 2024
1 parent aed6ba3 commit 9a19ab2
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 73 deletions.
187 changes: 130 additions & 57 deletions src/rvoice/fluid_iir_filter.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
#include "fluid_sys.h"
#include "fluid_conv.h"


static FLUID_INLINE void
fluid_iir_filter_calculate_coefficients(fluid_iir_filter_t *iir_filter, fluid_real_t output_rate,
fluid_real_t *a1_out, fluid_real_t *a2_out,
fluid_real_t *b02_out, fluid_real_t *b1_out);


/**
* Applies a low- or high-pass filter with variable cutoff frequency and quality factor
* for a given biquad transfer function:
Expand All @@ -48,9 +55,9 @@
*/
void
fluid_iir_filter_apply(fluid_iir_filter_t *iir_filter,
fluid_real_t *dsp_buf, int count)
fluid_real_t *dsp_buf, int count, fluid_real_t output_rate)
{
if(iir_filter->type == FLUID_IIR_DISABLED || iir_filter->q_lin == 0)
if(iir_filter->type == FLUID_IIR_DISABLED || FLUID_FABS(iir_filter->last_q) <= 0.001)
{
return;
}
Expand All @@ -65,6 +72,9 @@ fluid_iir_filter_apply(fluid_iir_filter_t *iir_filter,
fluid_real_t dsp_a2 = iir_filter->a2;
fluid_real_t dsp_b02 = iir_filter->b02;
fluid_real_t dsp_b1 = iir_filter->b1;

int fres_incr_count = iir_filter->fres_incr_count;
int q_incr_count = iir_filter->q_incr_count;

fluid_real_t dsp_centernode;
int dsp_i;
Expand Down Expand Up @@ -94,6 +104,24 @@ fluid_iir_filter_apply(fluid_iir_filter_t *iir_filter,
// dsp_buf[dsp_i] = dsp_b02 * dsp_input + dsp_hist1;
// dsp_hist1 = dsp_b1 * dsp_input - dsp_a1 * dsp_buf[dsp_i] + dsp_hist2;
// dsp_hist2 = dsp_b02 * dsp_input - dsp_a2 * dsp_buf[dsp_i];

if(fres_incr_count > 0 || q_incr_count > 0)
{
if(fres_incr_count > 0)
{
--fres_incr_count;
iir_filter->last_fres += iir_filter->fres_incr;
}
if(q_incr_count > 0)
{
--q_incr_count;
iir_filter->last_q += iir_filter->q_incr;
}

LOG_FILTER("last_fres: %.2f Hz | target_fres: %.2f Hz |---| last_q: %.4f | target_q: %.4f", iir_filter->last_fres, iir_filter->target_fres, iir_filter->last_q, iir_filter->target_q);

fluid_iir_filter_calculate_coefficients(iir_filter, output_rate, &dsp_a1, &dsp_a2, &dsp_b02, &dsp_b1);
}
}

iir_filter->hist1 = dsp_hist1;
Expand All @@ -102,12 +130,14 @@ fluid_iir_filter_apply(fluid_iir_filter_t *iir_filter,
iir_filter->a2 = dsp_a2;
iir_filter->b02 = dsp_b02;
iir_filter->b1 = dsp_b1;

iir_filter->fres_incr_count = fres_incr_count;
iir_filter->q_incr_count = q_incr_count;

fluid_check_fpe("voice_filter");
}
}


DECLARE_FLUID_RVOICE_FUNCTION(fluid_iir_filter_init)
{
fluid_iir_filter_t *iir_filter = obj;
Expand All @@ -129,7 +159,7 @@ fluid_iir_filter_reset(fluid_iir_filter_t *iir_filter)
iir_filter->hist1 = 0;
iir_filter->hist2 = 0;
iir_filter->last_fres = -1.;
iir_filter->q_lin = 0;
iir_filter->last_q = 0;
iir_filter->filter_startup = 1;
}

Expand All @@ -139,7 +169,8 @@ DECLARE_FLUID_RVOICE_FUNCTION(fluid_iir_filter_set_fres)
fluid_real_t fres = param[0].real;

iir_filter->fres = fres;
iir_filter->last_fres = -1.;

LOG_FILTER("fluid_iir_filter_set_fres: fres= %f [acents]",fres);
}

static fluid_real_t fluid_iir_filter_q_from_dB(fluid_real_t q_dB)
Expand Down Expand Up @@ -178,59 +209,56 @@ DECLARE_FLUID_RVOICE_FUNCTION(fluid_iir_filter_set_q)
fluid_iir_filter_t *iir_filter = obj;
fluid_real_t q = param[0].real;
int flags = iir_filter->flags;

LOG_FILTER("fluid_iir_filter_set_q: Q= %f [dB]",q);

if(flags & FLUID_IIR_Q_ZERO_OFF && q <= 0.0)
{
q = 0;
}
else if(flags & FLUID_IIR_Q_LINEAR)
{
/* q is linear (only for user-defined filter)
* increase to avoid Q being somewhere between zero and one,
* which results in some strange amplified lowpass signal
*/
q++;
/* q is linear (only for user-defined filter) */
}
else
{
q = fluid_iir_filter_q_from_dB(q);
}

iir_filter->q_lin = q;
iir_filter->filter_gain = 1.0;

if(!(flags & FLUID_IIR_NO_GAIN_AMP))
LOG_FILTER("fluid_iir_filter_set_q: Q= %f [linear]",q);

if(iir_filter->filter_startup)
{
/* SF 2.01 page 59:
*
* The SoundFont specs ask for a gain reduction equal to half the
* height of the resonance peak (Q). For example, for a 10 dB
* resonance peak, the gain is reduced by 5 dB. This is done by
* multiplying the total gain with sqrt(1/Q). `Sqrt' divides dB
* by 2 (100 lin = 40 dB, 10 lin = 20 dB, 3.16 lin = 10 dB etc)
* The gain is later factored into the 'b' coefficients
* (numerator of the filter equation). This gain factor depends
* only on Q, so this is the right place to calculate it.
*/
iir_filter->filter_gain /= FLUID_SQRT(q);
iir_filter->last_q = q;
}

/* The synthesis loop will have to recalculate the filter coefficients. */
iir_filter->last_fres = -1.;
else
{
static const fluid_real_t q_incr_count = FLUID_BUFSIZE;
iir_filter->q_incr = (q - iir_filter->last_q) / (q_incr_count);
iir_filter->q_incr_count = q_incr_count;
}
#ifdef DBG_FILTER
iir_filter->target_q = q;
#endif
}

static FLUID_INLINE void
fluid_iir_filter_calculate_coefficients(fluid_iir_filter_t *iir_filter,
int transition_samples,
fluid_real_t output_rate)
fluid_real_t output_rate,
fluid_real_t *a1_out, fluid_real_t *a2_out,
fluid_real_t *b02_out, fluid_real_t *b1_out)
{
/* FLUID_IIR_Q_LINEAR may switch the filter off by setting Q==0 */
if(iir_filter->q_lin == 0)
// FLUID_IIR_Q_LINEAR may switch the filter off by setting Q==0
// Due to the linear smoothing, last_q may not exactly become zero.
if(FLUID_FABS(iir_filter->last_q) <= 0.001)
{
return;
}
else
{
int flags = iir_filter->flags;
fluid_real_t filter_gain = 1.0f;

/*
* Those equations from Robert Bristow-Johnson's `Cookbook
* formulae for audio EQ biquad filter coefficients', obtained
Expand All @@ -244,28 +272,43 @@ fluid_iir_filter_calculate_coefficients(fluid_iir_filter_t *iir_filter,
(iir_filter->last_fres / output_rate);
fluid_real_t sin_coeff = FLUID_SIN(omega);
fluid_real_t cos_coeff = FLUID_COS(omega);
fluid_real_t alpha_coeff = sin_coeff / (2.0f * iir_filter->q_lin);
fluid_real_t alpha_coeff = sin_coeff / (2.0f * iir_filter->last_q);
fluid_real_t a0_inv = 1.0f / (1.0f + alpha_coeff);

/* Calculate the filter coefficients. All coefficients are
* normalized by a0. Think of `a1' as `a1/a0'.
*
* Here a couple of multiplications are saved by reusing common expressions.
* The original equations should be:
* iir_filter->b0=(1.-cos_coeff)*a0_inv*0.5*iir_filter->filter_gain;
* iir_filter->b1=(1.-cos_coeff)*a0_inv*iir_filter->filter_gain;
* iir_filter->b2=(1.-cos_coeff)*a0_inv*0.5*iir_filter->filter_gain; */
* iir_filter->b0=(1.-cos_coeff)*a0_inv*0.5*filter_gain;
* iir_filter->b1=(1.-cos_coeff)*a0_inv*filter_gain;
* iir_filter->b2=(1.-cos_coeff)*a0_inv*0.5*filter_gain; */

/* "a" coeffs are same for all 3 available filter types */
fluid_real_t a1_temp = -2.0f * cos_coeff * a0_inv;
fluid_real_t a2_temp = (1.0f - alpha_coeff) * a0_inv;

fluid_real_t b02_temp, b1_temp;

if(!(flags & FLUID_IIR_NO_GAIN_AMP))
{
/* SF 2.01 page 59:
*
* The SoundFont specs ask for a gain reduction equal to half the
* height of the resonance peak (Q). For example, for a 10 dB
* resonance peak, the gain is reduced by 5 dB. This is done by
* multiplying the total gain with sqrt(1/Q). `Sqrt' divides dB
* by 2 (100 lin = 40 dB, 10 lin = 20 dB, 3.16 lin = 10 dB etc)
* The gain is later factored into the 'b' coefficients
* (numerator of the filter equation). This gain factor depends
* only on Q, so this is the right place to calculate it.
*/
filter_gain /= FLUID_SQRT(iir_filter->last_q);
}

switch(iir_filter->type)
{
case FLUID_IIR_HIGHPASS:
b1_temp = (1.0f + cos_coeff) * a0_inv * iir_filter->filter_gain;
b1_temp = (1.0f + cos_coeff) * a0_inv * filter_gain;

/* both b0 -and- b2 */
b02_temp = b1_temp * 0.5f;
Expand All @@ -274,7 +317,7 @@ fluid_iir_filter_calculate_coefficients(fluid_iir_filter_t *iir_filter,
break;

case FLUID_IIR_LOWPASS:
b1_temp = (1.0f - cos_coeff) * a0_inv * iir_filter->filter_gain;
b1_temp = (1.0f - cos_coeff) * a0_inv * filter_gain;

/* both b0 -and- b2 */
b02_temp = b1_temp * 0.5f;
Expand All @@ -285,11 +328,10 @@ fluid_iir_filter_calculate_coefficients(fluid_iir_filter_t *iir_filter,
return;
}

iir_filter->a1 = a1_temp;
iir_filter->a2 = a2_temp;
iir_filter->b02 = b02_temp;
iir_filter->b1 = b1_temp;
iir_filter->filter_startup = 0;
*a1_out = a1_temp;
*a2_out = a2_temp;
*b02_out = b02_temp;
*b1_out = b1_temp;

fluid_check_fpe("voice_write filter calculation");
}
Expand All @@ -300,7 +342,13 @@ void fluid_iir_filter_calc(fluid_iir_filter_t *iir_filter,
fluid_real_t output_rate,
fluid_real_t fres_mod)
{
fluid_real_t fres;
unsigned int calc_coeff_flag = FALSE;
fluid_real_t fres, fres_diff;

if(iir_filter->type == FLUID_IIR_DISABLED)
{
return;
}

/* calculate the frequency of the resonant filter in Hz */
fres = fluid_ct2hz(iir_filter->fres + fres_mod);
Expand All @@ -324,23 +372,48 @@ void fluid_iir_filter_calc(fluid_iir_filter_t *iir_filter,
fres = 5.f;
}

// FLUID_LOG(FLUID_INFO, "%f + %f cents = %f cents = %f Hz | Q: %f", iir_filter->fres, fres_mod, iir_filter->fres + fres_mod, fres, iir_filter->q_lin);
LOG_FILTER("%f + %f = %f cents = %f Hz | Q: %f", iir_filter->fres, fres_mod, iir_filter->fres + fres_mod, fres, iir_filter->last_q);

/* if filter enabled and there is a significant frequency change.. */
if(iir_filter->type != FLUID_IIR_DISABLED && FLUID_FABS(fres - iir_filter->last_fres) > 0.01f)
fres_diff = fres - iir_filter->last_fres;
if(iir_filter->filter_startup)
{
/* The filter coefficients have to be recalculated (filter
* parameters have changed). Recalculation for various reasons is
* forced by setting last_fres to -1. The flag filter_startup
* indicates, that the DSP loop runs for the first time, in this
* case, the filter is set directly, instead of smoothly fading
* between old and new settings. */
// The filer was just starting up, make sure to calculate initial coefficients for the initial Q value, even though the fres may not have changed
calc_coeff_flag = TRUE;

iir_filter->fres_incr_count = 0;
iir_filter->last_fres = fres;
fluid_iir_filter_calculate_coefficients(iir_filter, FLUID_BUFSIZE,
output_rate);
iir_filter->filter_startup = 0;
}
else if(FLUID_FABS(fres_diff) > 0.01f)
{
fluid_real_t fres_incr_count = FLUID_BUFSIZE;
fluid_real_t num_buffers = iir_filter->last_q;
fluid_clip(num_buffers, 1, 5);
// For high values of Q, the phase gets really steep. To prevent clicks when quickly modulating fres in this case, we need to smooth out "slower".
// This is done by simply using Q times FLUID_BUFSIZE samples for the interpolation to complete, capped at 5.
// 5 was chosen because the phase doesn't really get any steeper when continuing to increase Q.
fres_incr_count *= num_buffers;
iir_filter->fres_incr = fres_diff / (fres_incr_count);
iir_filter->fres_incr_count = fres_incr_count;
#ifdef DBG_FILTER
iir_filter->target_fres = fres;
#endif

// The filter coefficients have to be recalculated (filter cutoff has changed).
calc_coeff_flag = TRUE;
}
else
{
// We do not account for any change of Q here - if it was changed q_incro_count will be non-zero and recalculating the coeffs
// will be taken care of in fluid_iir_filter_apply().
}

if(calc_coeff_flag)
{
fluid_iir_filter_calculate_coefficients(iir_filter, output_rate, &iir_filter->a1, &iir_filter->a2, &iir_filter->b02, &iir_filter->b1);
}

fluid_check_fpe("voice_write DSP coefficients");

}

31 changes: 22 additions & 9 deletions src/rvoice/fluid_iir_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,22 @@

#include "fluidsynth_priv.h"

// Uncomment to get debug logging for filter parameters
// #define DBG_FILTER
#ifdef DBG_FILTER
#define LOG_FILTER(...) FLUID_LOG(FLUID_DBG, __VA_ARGS__)
#else
#define LOG_FILTER(...)
#endif

typedef struct _fluid_iir_filter_t fluid_iir_filter_t;

DECLARE_FLUID_RVOICE_FUNCTION(fluid_iir_filter_init);
DECLARE_FLUID_RVOICE_FUNCTION(fluid_iir_filter_set_fres);
DECLARE_FLUID_RVOICE_FUNCTION(fluid_iir_filter_set_q);

void fluid_iir_filter_apply(fluid_iir_filter_t *iir_filter,
fluid_real_t *dsp_buf, int dsp_buf_count);
fluid_real_t *dsp_buf, int dsp_buf_count, fluid_real_t output_rate);

void fluid_iir_filter_reset(fluid_iir_filter_t *iir_filter);

Expand All @@ -54,15 +62,20 @@ struct _fluid_iir_filter_t
fluid_real_t a2; /* a1 / a0 */

fluid_real_t hist1, hist2; /* Sample history for the IIR filter */
int filter_startup; /* Flag: If set, the filter will be set directly.
Else it changes smoothly. */
int filter_startup; /* Flag: If set, the filter parameters will be set directly. Else it changes smoothly. */

fluid_real_t fres; /* the resonance frequency, in cents (not absolute cents) */
fluid_real_t last_fres; /* Current resonance frequency of the IIR filter */
/* Serves as a flag: A deviation between fres and last_fres */
/* indicates, that the filter has to be recalculated. */
fluid_real_t q_lin; /* the q-factor on a linear scale */
fluid_real_t filter_gain; /* Gain correction factor, depends on q */
fluid_real_t fres; /* The desired resonance frequency, in absolute cents, this filter is currently set to */
fluid_real_t last_fres; /* The filter's current (smoothed out) resonance frequency in Hz, which will converge towards its target fres once fres_incr_count has become zero */
fluid_real_t fres_incr; /* The linear increment of fres each sample */
int fres_incr_count; /* The number of samples left for the smoothed last_fres adjustment to complete */

fluid_real_t last_q; /* The filter's current (smoothed) Q-factor (or "bandwidth", or "resonance-friendlyness") on a linear scale. Just like fres, this will converge towards its target Q once q_incr_count has become zero. */
fluid_real_t q_incr; /* The linear increment of q each sample */
int q_incr_count; /* The number of samples left for the smoothed Q adjustment to complete */
#ifdef DBG_FILTER
fluid_real_t target_fres; /* The filter's target fres, that last_fres should converge towards - for debugging only */
fluid_real_t target_q; /* The filter's target Q - for debugging only */
#endif
};

#endif
Expand Down
4 changes: 2 additions & 2 deletions src/rvoice/fluid_rvoice.c
Original file line number Diff line number Diff line change
Expand Up @@ -488,11 +488,11 @@ fluid_rvoice_write(fluid_rvoice_t *voice, fluid_real_t *dsp_buf)
fluid_lfo_get_val(&voice->envlfo.modlfo) * voice->envlfo.modlfo_to_fc +
modenv_val * voice->envlfo.modenv_to_fc);

fluid_iir_filter_apply(&voice->resonant_filter, dsp_buf, count);
fluid_iir_filter_apply(&voice->resonant_filter, dsp_buf, count, voice->dsp.output_rate);

/* additional custom filter - only uses the fixed modulator, no lfos... */
fluid_iir_filter_calc(&voice->resonant_custom_filter, voice->dsp.output_rate, 0);
fluid_iir_filter_apply(&voice->resonant_custom_filter, dsp_buf, count);
fluid_iir_filter_apply(&voice->resonant_custom_filter, dsp_buf, count, voice->dsp.output_rate);

return count;
}
Expand Down
Loading

0 comments on commit 9a19ab2

Please sign in to comment.