ambivalence/Source/DSP/MagnitudeResponseFitter.h

130 lines
6.1 KiB
C
Raw Normal View History

2026-08-15 15:36:28 +02:00
#pragma once
#include "DSPConstants.h"
#include "BiquadFilters.h"
#include "../AlgorithmPresets.h"
#include <array>
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<BiquadCoeffs, ABSO_STAGES> 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<BiquadCoeffs, NUM_BANDS> geqStages; // 10-band GEQ
// visualization
std::array<float, NUM_BANDS> targetDb; // per-band target dB (after clamping)
std::array<float, NUM_BANDS> 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<float, NUM_BANDS>& rt60,
float hfDamping,
float lfAbsorption);
// -------------------------------------------------------------------------
// Stage 2c design function
// -------------------------------------------------------------------------
static DesignResultStage2 designStage2(
int delaySamples,
double sampleRate,
const std::array<float, NUM_BANDS>& 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<float, NUM_BANDS>& rt60) noexcept;
static float getT60AtNyquist(const std::array<float, NUM_BANDS>& rt60, double sampleRate) noexcept;
// -- Stage 2 --
static BiquadCoeffs designSymmetricPeakBiquad(
float fcHz, float gainDB, float Q, double sampleRate) noexcept;
static const std::array<float, NUM_BANDS>& getBandFreqs() noexcept { return BAND_FREQ; }
static const std::array<float, NUM_BANDS>& getBandQs() noexcept;
static float biquadMagnitudeDB(const BiquadCoeffs& c, float fEval, double sampleRate) noexcept;
static void solveLDLT10(
const std::array<std::array<double, NUM_BANDS>, NUM_BANDS>& A,
const std::array<double, NUM_BANDS>& b,
std::array<double, NUM_BANDS>& 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<std::array<double, NUM_BANDS>, NUM_BANDS> cachedB;
static std::array<std::array<double, NUM_BANDS>, NUM_BANDS> cachedBtWB;
static std::array<double, NUM_BANDS> cachedW;
static double cachedSampleRate;
static bool cacheValid;
};
} // namespace FDNReverb