From 84057af59d87803489baddca29ffa1b3ebf87e3d Mon Sep 17 00:00:00 2001 From: Lord-Nightmare Date: Wed, 28 Sep 2022 22:42:11 -0400 Subject: 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] --- src/devices/sound/flt_biquad.cpp | 71 +++++++++++++++++++++++++--------------- src/devices/sound/flt_biquad.h | 10 ++++-- 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/src/devices/sound/flt_biquad.cpp b/src/devices/sound/flt_biquad.cpp index a023c1ff0a5..6ae1bde0109 100644 --- a/src/devices/sound/flt_biquad.cpp +++ b/src/devices/sound/flt_biquad.cpp @@ -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: diff --git a/src/devices/sound/flt_biquad.h b/src/devices/sound/flt_biquad.h index 9ac3f820893..e565aa6da8a 100644 --- a/src/devices/sound/flt_biquad.h +++ b/src/devices/sound/flt_biquad.h @@ -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); -- cgit v1.2.3