#pragma once #include "DSPConstants.h" #include "BiquadFilters.h" #include "../AlgorithmPresets.h" #include namespace FDNReverb { // ----------------------------------------------------------------------------- // MagnitudeResponseFitter // ----------------------------------------------------------------------------- // designs the 10-band RT60 absorption filters for the FDN. // // design modes : // Stage 1 (Jot first-order orthogonalizing): // Jot-Chaigne (AES Preprint 3030, 1991) first-order orthogonalizing filters. // matched at DC and Nyquist with 2 design points. // // Stage 2c (Valimaki-Liski cumulative GEQ): // Valimaki & Liski (IEEE SPL 2017) Interaction Matrix + WLS // exact fit across the 10 bands. // // safety guarantee : // - targets are clamped to 0 dB or below -> loop gain <= 1 is guaranteed // - band 0 midGain and b0/b1/b2 are absorbed into the applied filter // - LF/HF corrections are independent GEQ targets in dB // // important : // - per-band decay in dB: -60*m / (fs*T60) // avoids the "2 kHz T60 assumption" of Schlecht-Habets (DAFx-17) // - design runs offline (message thread); the resulting Biquad coefficients // are used on the audio thread // ----------------------------------------------------------------------------- class MagnitudeResponseFitter { public: enum class DesignMode { Stage1_Jot1stOrder, // Jot first-order orthogonalizing (2 pts: DC/Nyquist) Stage2_BiquadGEQ // Valimaki-Liski cumulative GEQ (exact at 10 bands) }; // ------------------------------------------------------------------------- // Stage 1 design result (existing) // ------------------------------------------------------------------------- // ABSO_STAGES = 3 Biquads: // coeffs[0] = gain (Jot first-order orthogonalizing filter, Biquad form) // coeffs[1] = low-band correction (Low Shelf, LF Absorption) // coeffs[2] = high-band correction (High Shelf, HF Damping) struct DesignResult { std::array coeffs; float dcGain{ 1.0f }; float nyquistGain{ 1.0f }; float pole{ 0.0f }; }; // ------------------------------------------------------------------------- // Stage 2c design result // ------------------------------------------------------------------------- // 10-band GEQ: // geqStages[0] = band 0 (31.25 Hz), midGain absorbed into the coefficient // geqStages[1..9] = bands 1-9 (62.5 Hz - 16 kHz), GEQ // // filter chain: geqStages[0] -> geqStages[1] -> ... -> geqStages[9] // no separate midGain stage is needed (absorbed into band 0). struct DesignResultStage2 { std::array geqStages; // 10-band GEQ // visualization std::array targetDb; // per-band target dB (after clamping) std::array commandDb; // WLS-solved command dB float midGainAbsorbed{ 1.0f }; // midGain absorbed into band 0 }; // ------------------------------------------------------------------------- // Stage 1 design function (existing) // ------------------------------------------------------------------------- static DesignResult design( int delaySamples, double sampleRate, const std::array& rt60, float hfDamping, float lfAbsorption); // ------------------------------------------------------------------------- // Stage 2c design function // ------------------------------------------------------------------------- static DesignResultStage2 designStage2( int delaySamples, double sampleRate, const std::array& rt60, float hfDamping, float lfAbsorption); // ------------------------------------------------------------------------- // precompute the interaction matrix once (per sample rate) // ------------------------------------------------------------------------- static void precomputeInteractionMatrix(double sampleRate); static double getCachedSampleRate() noexcept { return cachedSampleRate; } private: // -- Stage 1 -- static float t60ToLoopGain(float t60Seconds, int delaySamples, double sampleRate) noexcept; static float computeJotPole(float gDC, float alphaRatio) noexcept; static BiquadCoeffs orthogonalizedFirstOrderToBiquad(float gain, float pole) noexcept; static float getT60AtDC(const std::array& rt60) noexcept; static float getT60AtNyquist(const std::array& rt60, double sampleRate) noexcept; // -- Stage 2 -- static BiquadCoeffs designSymmetricPeakBiquad( float fcHz, float gainDB, float Q, double sampleRate) noexcept; static const std::array& getBandFreqs() noexcept { return BAND_FREQ; } static const std::array& getBandQs() noexcept; static float biquadMagnitudeDB(const BiquadCoeffs& c, float fEval, double sampleRate) noexcept; static void solveLDLT10( const std::array, NUM_BANDS>& A, const std::array& b, std::array& x) noexcept; // absorb the entire DC gain of the Biquad (b0, b1, b2) into a gain // so an independent DC gain can be applied to the filter mathematically static BiquadCoeffs absorbGainIntoBiquad(const BiquadCoeffs& c, float linearGain) noexcept; // -- Stage 2 static -- static std::array, NUM_BANDS> cachedB; static std::array, NUM_BANDS> cachedBtWB; static std::array cachedW; static double cachedSampleRate; static bool cacheValid; }; } // namespace FDNReverb