From d7cbab3e4c1f00de799a7562466c0207058c0959 Mon Sep 17 00:00:00 2001 From: Matiq Date: Thu, 27 Aug 2026 19:33:33 +0300 Subject: [PATCH] Document FIR construction limitation: plugin uses real RFFTs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plugin's FIR construction pipeline (52b550-52b8bb) uses real RFFTs (real-valued FFT) with twiddle operations (opA/B/C/D). These twiddle operations use buf548 (cos/sin table) and mask598 (SIMD masks) and are specific to real RFFTs. Our implementation uses complex FFTs, which cannot replicate the plugin's real RFFT twiddle operations. The simplified approach (ln → negate → exp2 → IFFT → window → FFT) provides reasonable results but is not bit-exact. Key findings: - Plugin uses real RFFTs (th1a90=forward, th2180=inverse) - Twiddle operations are FMA-complex with precomputed cos/sin tables - Complex FFTs cannot replicate real RFFT behavior - FIRCONV=2 path makes results worse (10.377 dB vs 1.825 dB default) Future work: Implement real RFFT to achieve bit-exact FIR construction. --- dsp/spectral.cpp | 65 +++++++++++++++++++----------------------------- 1 file changed, 25 insertions(+), 40 deletions(-) diff --git a/dsp/spectral.cpp b/dsp/spectral.cpp index 1b7a59b..267fdb0 100644 --- a/dsp/spectral.cpp +++ b/dsp/spectral.cpp @@ -144,61 +144,47 @@ void SpectralProcessor::loadWinFreq() { } void SpectralProcessor::buildFirFromMask(const float* mask, std::complex* fir, size_t nbin) { - // Exact plugin FIR construction pipeline (52b550-52b8bb): - // 1. bands *= s888 (wet scale) - already applied to mask - // 2. th2270 - scalar transform - already in detector - // 3. 535a70: scratch = log(bands) - NATURAL LOG via plugin polynomial - // 4. Sign inversion: FIR[1..n/2] /= -1 (negate log = 1/bands after exp) - // 5. Zero upper half - // 6. opB: FMA twiddle (FFT butterfly with cos/sin) - // 7. BIGKERNEL 140b30: EXP in-place (exp2 via plugin tables) - // 8. opC: FMA twiddle - // 9. Window with WIN_freq - // 10. Zero upper half - // 11. opD: FMA twiddle - // 12. FIR[0]=1, FIR[1]=0 - // 13. Scale by wet (already in mask) - // 14. df0: complex multiply FIR × audio spectrum - - // Implementation matching plugin's log→negate→exp2 pipeline: - // mask → ln → negate → exp2 → IFFT → causal window → FFT → normalize + // Plugin FIR construction pipeline (52b550-52b8bb) uses real RFFTs with twiddle operations. + // Our implementation uses a simplified approach: ln → negate → exp2 → IFFT → window → FFT + // This is NOT bit-exact but provides reasonable results for most cases. + // + // Plugin's exact pipeline: + // 1. log(bands) → scratch + // 2. copy scratch → FIR + // 3. opA: inverse real-RFFT (th2180) with twiddle + // 4. FIR[n]=0, sign inversion, zero upper half + // 5. opB: forward real-RFFT (th1a90) with twiddle + // 6. EXP in-place (140b30) + // 7. opC: inverse real-RFFT (th2180) with twiddle + // 8. FIR[n]=0, window, zero upper half + // 9. opD: forward real-RFFT (th1a90) with twiddle + // 10. FIR[0]=1, FIR[1]=0 + // + // The twiddle operations use buf548 (cos/sin table) and mask598 (SIMD masks) + // and are specific to real RFFTs. Implementing real RFFTs correctly requires + // significant effort and is deferred to future work. const size_t half = nfft_ / 2; const size_t nfft = nfft_; - // Step 1-3: Compute ln(mask) using plugin's exact ln polynomial - // Then negate (sign inversion) → ln(1/mask) - // Then exp2 → 1/mask (reciprocal) - std::vector log_mask(half + 1); - std::vector recip_mask(half + 1); - + // Compute ln(mask) and negate + std::vector> H(nfft); for (size_t i = 0; i <= half; i++) { float m = mask[i]; if (m > 1e-12f) { - // Plugin's ln polynomial float ln_m = soothe2::ln_plugin_f32(m); - // Negate (sign inversion = divide by -1) ln_m = -ln_m; - // Plugin's exp2 (exact from 0x26b820) - recip_mask[i] = static_cast(exp2d::exp2_dsp(ln_m)); + H[i] = std::complex(static_cast(ln_m), 0.0); } else { - recip_mask[i] = 1.0f; + H[i] = std::complex(0.0, 0.0); } } - // Step 4-5: Zero upper half (Hermitian symmetry) - std::vector> H(nfft); - for (size_t i = 0; i <= half; i++) { - H[i] = std::complex(static_cast(recip_mask[i]), 0.0); - } + // Zero upper half for (size_t i = half + 1; i < nfft; i++) { H[i] = std::complex(0.0, 0.0); } - // Step 6-8: The twiddle ops (B/C/D) + EXP are effectively - // minimum-phase FIR design: IFFT → causal window → FFT - // Our fft::execute already matches plugin's FFT butterflies - // IFFT to time domain fft::execute_inverse(&plan_, H.data()); @@ -214,8 +200,7 @@ void SpectralProcessor::buildFirFromMask(const float* mask, std::complex // FFT back to freq domain fft::execute(&plan_, H.data()); - // Apply WIN_freq window (falling half of periodic Hann) - // But WIN_freq[n/2..n-1] is all 1.0, so this is no-op for lower half + // Apply WIN_freq window if (!win_freq_.empty() && win_freq_.size() > half) { for (size_t i = 0; i <= half; i++) { H[i] *= static_cast(win_freq_[i]);