Add the ability to define a biquad filter using raw parameters, instead of only by using component values or by type/cutoff/bandwidth/gain. Fix some of the issues with the Multiple Feedback Bandpass filter. [Lord Nightmare]

This commit is contained in:
Lord-Nightmare 2022-09-28 22:42:11 -04:00
parent c11d1b7844
commit 84057af59d
2 changed files with 52 additions and 29 deletions

View File

@ -14,10 +14,11 @@
* It uses the Q factor directly in the filter definitions, rather than the damping factor (1/Q)
* It implements every common type of digital biquad filter which I could find documentation for.
* The filter is Direct-form II instead of Direct-form I, which results in shorter compiled code.
* Optional direct control of the 5 normalized biquad parameters for a custom/raw parameter filter.
Possibly useful features which aren't implemented because nothing uses them yet:
* More Sallen-Key filter variations (band-pass, high-pass)
* Direct control of the 5 normalized biquad parameters for a custom/raw parameter filter.
*/
#include "emu.h"
#include "flt_biquad.h"
@ -84,6 +85,17 @@ filter_biquad_device& filter_biquad_device::setup(filter_biquad_device::biquad_p
m_gain = p.gain;
return *this;
}
filter_biquad_device& filter_biquad_device::setup_raw(double a1, double a2, double b0, double b1, double b2)
{
m_type = biquad_type::RAWPARAMS;
m_a1 = a1;
m_a2 = a2;
m_b0 = b0;
m_b1 = b1;
m_b2 = b2;
return *this;
}
// modify an existing instance with new filter parameters
void filter_biquad_device::modify(biquad_type type, double fc, double q, double gain)
@ -104,14 +116,30 @@ void filter_biquad_device::modify(filter_biquad_device::biquad_params p)
m_gain = p.gain;
recalc();
}
void filter_biquad_device::modify_raw(double a1, double a2, double b0, double b1, double b2)
{
m_stream->update();
m_type = biquad_type::RAWPARAMS;
m_a1 = a1;
m_a2 = a2;
m_b0 = b0;
m_b1 = b1;
m_b2 = b2;
recalc();
}
//-------------------------------------------------
// Filter setup helpers for various filter models
//-------------------------------------------------
// NOTE: if a resistor doesn't exist, pass a value of RES_M(999.99) or the like, i.e. an 'infinite resistor'
// NOTE: if a resistor is a direct short, set its resistance to RES_R(0.001)
// Sallen-Key filters
/* Setup a biquad filter structure based on a single op-amp Sallen-Key low-pass filter circuit.
* This is sometimes, incorrectly, called a "Butterworth" filter structure.
*
* .----------------------------.
* | |
@ -154,9 +182,6 @@ filter_biquad_device::biquad_params filter_biquad_device::opamp_sk_lowpass_calc(
{
fatalerror("filter_biquad_device::opamp_sk_lowpass_calc() - no parameters can be 0; parameters were: r1: %f, r2: %f, r3: %f, r4: %f, c1: %f, c2: %f", r1, r2, r3, r4, c1, c2); /* Filter can not be setup. Undefined results. */
}
// NOTE: if R3 doesn't exist (no link to ground), pass a value of RES_M(999.99) or the like, i.e. an 'infinite resistor'
// NOTE: if R4 is a direct short, set its resistance to RES_R(0.001)
// NOTE: if R3 doesn't exist AND R4 is a direct short, follow both rules above.
r.type = biquad_type::LOWPASS;
r.gain = 1.0 + (r4 / r3); // == (r3 + r4) / r3
r.fc = 1.0 / (2 * M_PI * sqrt(r1 * r2 * c1 * c2));
@ -179,6 +204,7 @@ filter_biquad_device::biquad_params filter_biquad_device::opamp_sk_lowpass_calc(
* a dead short, and c1 omitted. set both c1 and r2 to 0 in this case.
* NOTE3: a variant of NOTE2 has only the c1 capacitor left off, and r2 present. if so,
* set c1 to 0 and r2 to its expected value.
* TODO: make this compatible with the RES_M(999.99) and RES_R(0.001) rules!
*
* .--------+---------.
* | | |
@ -215,7 +241,7 @@ filter_biquad_device::biquad_params filter_biquad_device::opamp_mfb_lowpass_calc
}
r.gain = -r3 / r1;
r.q = (M_SQRT2 / 2.0);
if (c1 == 0) // if both R2 and C1 are 0, it is the 'proper' first order case. There do exist some unusual filters where R2 is not 0, though. In both cases this yields a single-pole filter with limited configurable gain, and a Q of ~0.707. R2 being zero makes the (r1 * r3) numerator term cancel out to 1.0.
if (c1 == 0) // if both R2 and C1 are 0, it is the 'proper' first order case. If C1 is 0 (Williams...) the filter is 1st order. There do exist some unusual filters where R2 is not 0, though. In both cases this yields a single-pole filter with limited configurable gain, and a Q of ~0.707. R2 being zero makes the (r1 * r3) numerator term cancel out to 1.0.
{
r.fc = (r1 * r3) / (2 * M_PI * ((r1 * r2) + (r1 * r3) + (r2 * r3)) * r3 * c2);
r.type = biquad_type::LOWPASS1P;
@ -234,14 +260,14 @@ filter_biquad_device::biquad_params filter_biquad_device::opamp_mfb_lowpass_calc
/* Setup a biquad filter structure based on a single op-amp Multiple-Feedback band-pass filter circuit.
* This is sometimes called a "modified Deliyannis" or "Deliyannis-friend" filter circuit,
* or an "Infinite Gain Multiple-Feedback [band-pass] Filter", or "IGMF".
* or an "Infinite Gain Multiple-Feedback [band-pass] Filter" aka "IGMF".
* NOTE: vRef is not definable when setting up the filter, and is assumed to be grounded.
* If the analog effects caused by vRef are important to the operation of the specific filter
* in question, a netlist implementation may work better under those circumstances.
* TODO: There is a documented modification to this filter which adds a resistor ladder between
* ground and the op-amp output, with the 'rung' of the ladder connecting to the + input of
* the op-amp, and this allows more control of the filter.
* NOTE2: If r2 is not used, then set it to 0 ohms, the code will switch to an Infinite Gain MFB Bandpass
* NOTE2: If r2 is not used, then set it to RES_M(999.99), the code will effectively be an Infinite Gain MFB Bandpass.
*
* .--------+---------.
* | | |
@ -259,27 +285,18 @@ filter_biquad_device::biquad_params filter_biquad_device::opamp_mfb_lowpass_calc
*/
filter_biquad_device& filter_biquad_device::opamp_mfb_bandpass_setup(double r1, double r2, double r3, double c1, double c2)
{
if ((r1 == 0) || (r3 == 0) || (c1 == 0) || (c2 == 0))
if ((r1 == 0) || (r2 == 0) || (r3 == 0) || (c1 == 0) || (c2 == 0))
{
fatalerror("filter_biquad_device::opamp_mfb_bandpass_setup() - only r2 can be 0; parameters were: r1: %f, r2: %f, r3: %f, c1: %f, c2: %f", r1, r2, r3, c1, c2); /* Filter can not be setup. Undefined results. */
fatalerror("filter_biquad_device::opamp_mfb_bandpass_setup() - no parameters can be 0; parameters were: r1: %f, r2: %f, r3: %f, c1: %f, c2: %f", r1, r2, r3, c1, c2); /* Filter can not be setup. Undefined results. */
}
double r_in, gain;
double const r_in = 1.0 / (1.0/r1 + 1.0/r2); // TODO: verify
// gain = (r2 / (r1 + r2)) * (-r3 / r_in * c2 / (c1 + c2)); // ??? wrong?
double const gain = -r3 / (2.0 * r1);
// q = sqrt(r3 / r_in * c1 * c2) / (c1 + c2); // ??? wrong?
double const q = 0.5 * sqrt(r3 / r1);
if (r2 == 0)
{
gain = 1;
r_in = r1;
}
else
{
gain = r2 / (r1 + r2);
r_in = 1.0 / (1.0/r1 + 1.0/r2);
}
double const fc = 1.0 / (2 * M_PI * sqrt(r_in * r3 * c1 * c2)); // technically this is the center frequency of the bandpass
double const q = sqrt(r3 / r_in * c1 * c2) / (c1 + c2);
gain *= -r3 / r_in * c2 / (c1 + c2);
double const fc = 1.0 / (sqrt(r_in * r3 * c1 * c2)); // technically this is the center frequency of the bandpass
#ifdef FLT_BIQUAD_DEBUG_SETUP
logerror("filter_biquad_device::opamp_mfb_bandpass_setup() yields: fc = %f, Q = %f, gain = %f\n", fc, q, gain);
#endif
@ -434,6 +451,9 @@ void filter_biquad_device::sound_stream_update(sound_stream &stream, std::vector
*/
void filter_biquad_device::recalc()
{
if (m_type == biquad_type::RAWPARAMS)
return; // if we're dealing with raw parameters, just return, don't touch anything.
double const MGain = fabs(m_gain); // absolute multiplicative gain
double const DBGain = log10(MGain) * 20.0; // gain in dB
double const AMGain = pow(10, fabs(DBGain) / 20.0); // multiplicative gain of absolute DB
@ -442,9 +462,6 @@ void filter_biquad_device::recalc()
double const KoverQ = K / m_q;
double normal = 1.0 / (1.0 + KoverQ + Ksquared);
m_a1 = 2.0 * (Ksquared - 1.0) * normal;
m_a2 = (1.0 - KoverQ + Ksquared) * normal;
switch (m_type)
{
case biquad_type::LOWPASS1P:

View File

@ -24,7 +24,8 @@ public:
NOTCH,
PEAK,
LOWSHELF,
HIGHSHELF
HIGHSHELF,
RAWPARAMS
};
struct biquad_params
@ -44,7 +45,12 @@ public:
void modify(biquad_type type, double fc, double q, double gain);
void modify(biquad_params p);
// helper setup functions to create common filters representable by biquad filters:
// set up the filter with raw biquad coefficients
filter_biquad_device& setup_raw(double a1, double a2, double b0, double b1, double b2);
void modify_raw(double a1, double a2, double b0, double b1, double b2);
// Helper setup functions to create common filters representable by biquad filters:
// (and, as needed, modify/update/recalc helpers)
// Sallen-Key low-pass
filter_biquad_device& opamp_sk_lowpass_setup(double r1, double r2, double r3, double r4, double c1, double c2);