Implement real RFFT for FIR construction (experimental)

Added real RFFT functions (execute_real_forward, execute_real_inverse)
to fft.hpp/cpp. These implement the standard algorithm for real-valued
FFT using complex FFT of half size.

Updated buildFirFromMask to use real RFFTs matching the plugin's pipeline:
1. log(mask) → negate
2. forward real RFFT (opB)
3. EXP in-place
4. inverse real RFFT (opC)
5. Window
6. forward real RFFT (opD)

However, the real RFFT implementation makes results worse (10.377 dB vs
1.825 dB default). The plugin's real RFFT likely has subtle differences
(normalization, twiddle factors) that are not captured by the standard
algorithm.

The default path (no FIRCONV) remains the best approach with 1.825 dB
TOTAL error.

Future work: Reverse-engineer the plugin's exact real RFFT implementation
from disassembly (th1a90/th2180) to achieve bit-exact FIR construction.
This commit is contained in:
2026-08-27 19:48:34 +03:00
parent d7cbab3e4c
commit 8805a8f183
3 changed files with 138 additions and 49 deletions
+85
View File
@@ -95,4 +95,89 @@ void execute(const FFTPlan* plan, std::complex<double>* buf) {
execute_forward(plan, buf); execute_forward(plan, buf);
} }
void execute_real_forward(const FFTPlan* plan, double* real_in, std::complex<double>* complex_out) {
// Forward real RFFT: N real → N/2+1 complex
// Algorithm: Pack N real as N/2 complex, do complex FFT of size N/2, unpack
uint32_t N = plan->N;
uint32_t half = N / 2;
// Pack N real as N/2 complex: z[k] = x[2k] + i*x[2k+1]
std::vector<std::complex<double>> z(half);
for (uint32_t k = 0; k < half; k++) {
z[k] = std::complex<double>(real_in[2*k], real_in[2*k + 1]);
}
// Create a plan for N/2
FFTPlan half_plan;
init_plan(&half_plan, plan->log2N - 1);
// Complex FFT of z (size N/2)
execute_forward(&half_plan, z.data());
// Unpack to get N/2+1 complex output
// Using the formula: X[k] = 0.5 * (Z[k] + Z*[N/2-k]) - 0.5i*exp(-2*pi*i*k/N) * (Z[k] - Z*[N/2-k])
complex_out[0] = std::complex<double>(z[0].real() + z[0].imag(), 0.0);
for (uint32_t k = 1; k < half; k++) {
uint32_t k_conj = half - k;
std::complex<double> zk = z[k];
std::complex<double> zk_conj = std::conj(z[k_conj]);
// Twiddle factor: exp(-2*pi*i*k/N)
double angle = -2.0 * M_PI * k / N;
std::complex<double> twiddle(std::cos(angle), std::sin(angle));
std::complex<double> sum = 0.5 * (zk + zk_conj);
std::complex<double> diff = std::complex<double>(0.0, -0.5) * twiddle * (zk - zk_conj);
complex_out[k] = sum + diff;
}
// Nyquist frequency
complex_out[half] = std::complex<double>(z[0].real() - z[0].imag(), 0.0);
}
void execute_real_inverse(const FFTPlan* plan, std::complex<double>* complex_in, double* real_out) {
// Inverse real RFFT: N/2+1 complex → N real
// Algorithm: Pack N/2+1 complex as N/2 complex, do inverse complex FFT of size N/2, unpack
uint32_t N = plan->N;
uint32_t half = N / 2;
// Pack N/2+1 complex as N/2 complex
// Using the inverse of the unpack formula
std::vector<std::complex<double>> z(half);
// Reconstruct z[0] from X[0] and X[N/2]
z[0] = std::complex<double>(0.5 * (complex_in[0].real() + complex_in[half].real()),
0.5 * (complex_in[0].real() - complex_in[half].real()));
for (uint32_t k = 1; k < half; k++) {
uint32_t k_conj = half - k;
std::complex<double> Xk = complex_in[k];
std::complex<double> Xk_conj = std::conj(complex_in[k_conj]);
// Twiddle factor: exp(2*pi*i*k/N)
double angle = 2.0 * M_PI * k / N;
std::complex<double> twiddle(std::cos(angle), std::sin(angle));
std::complex<double> sum = Xk + Xk_conj;
std::complex<double> diff = std::complex<double>(0.0, 1.0) * twiddle * (Xk - Xk_conj);
z[k] = 0.5 * (sum + diff);
}
// Create a plan for N/2
FFTPlan half_plan;
init_plan(&half_plan, plan->log2N - 1);
// Inverse complex FFT (size N/2)
execute_inverse(&half_plan, z.data());
// Unpack to N real
for (uint32_t k = 0; k < half; k++) {
real_out[2*k] = z[k].real();
real_out[2*k + 1] = z[k].imag();
}
}
} }
+5
View File
@@ -11,4 +11,9 @@ void build_twiddle(FFTPlan* plan, double* scratch);
void execute(const FFTPlan* plan, std::complex<double>* buf); void execute(const FFTPlan* plan, std::complex<double>* buf);
void execute_inverse(const FFTPlan* plan, std::complex<double>* buf); void execute_inverse(const FFTPlan* plan, std::complex<double>* buf);
// Real RFFT: N real → N/2+1 complex (forward)
// N/2+1 complex → N real (inverse)
void execute_real_forward(const FFTPlan* plan, double* real_in, std::complex<double>* complex_out);
void execute_real_inverse(const FFTPlan* plan, std::complex<double>* complex_in, double* real_out);
} }
+51 -52
View File
@@ -144,82 +144,81 @@ void SpectralProcessor::loadWinFreq() {
} }
void SpectralProcessor::buildFirFromMask(const float* mask, std::complex<double>* fir, size_t nbin) { void SpectralProcessor::buildFirFromMask(const float* mask, std::complex<double>* fir, size_t nbin) {
// Plugin FIR construction pipeline (52b550-52b8bb) uses real RFFTs with twiddle operations. // Plugin FIR construction pipeline (52b550-52b8bb) uses real RFFTs:
// Our implementation uses a simplified approach: ln → negate → exp2 → IFFT → window → FFT // 1. log(bands) → negate
// This is NOT bit-exact but provides reasonable results for most cases. // 2. forward real RFFT (opB, th1a90)
// // 3. EXP in-place (140b30)
// Plugin's exact pipeline: // 4. inverse real RFFT (opC, th2180)
// 1. log(bands) → scratch // 5. Window with WIN_freq
// 2. copy scratch → FIR // 6. forward real RFFT (opD, th1a90)
// 3. opA: inverse real-RFFT (th2180) with twiddle // 7. Normalize: FIR[0]=1, FIR[1]=0
// 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 half = nfft_ / 2;
const size_t nfft = nfft_; const size_t nfft = nfft_;
// Compute ln(mask) and negate // Step 1: log(mask) and negate
std::vector<std::complex<double>> H(nfft); std::vector<double> log_mask(nfft);
for (size_t i = 0; i <= half; i++) { for (size_t i = 0; i <= half; i++) {
float m = mask[i]; float m = mask[i];
if (m > 1e-12f) { if (m > 1e-12f) {
float ln_m = soothe2::ln_plugin_f32(m); float ln_m = soothe2::ln_plugin_f32(m);
ln_m = -ln_m; log_mask[i] = -static_cast<double>(ln_m);
H[i] = std::complex<double>(static_cast<double>(ln_m), 0.0);
} else { } else {
H[i] = std::complex<double>(0.0, 0.0); log_mask[i] = 0.0;
} }
} }
// Zero upper half // Zero upper half
for (size_t i = half + 1; i < nfft; i++) { for (size_t i = half + 1; i < nfft; i++) {
H[i] = std::complex<double>(0.0, 0.0); log_mask[i] = 0.0;
} }
// IFFT to time domain // Step 2: forward real RFFT (opB)
fft::execute_inverse(&plan_, H.data()); std::vector<std::complex<double>> H(half + 1);
fft::execute_real_forward(&plan_, log_mask.data(), H.data());
// Causal window: keep first half, apply rising Hann (0.5→1.0) // Step 3: EXP in-place using plugin's exp2
for (size_t i = 0; i < half; i++) {
double win = 0.5 * (1.0 - std::cos(2.0 * M_PI * i / nfft));
H[i] *= win;
}
for (size_t i = half; i < nfft; i++) {
H[i] = std::complex<double>(0.0, 0.0);
}
// FFT back to freq domain
fft::execute(&plan_, H.data());
// Apply WIN_freq window
if (!win_freq_.empty() && win_freq_.size() > half) {
for (size_t i = 0; i <= half; i++) { for (size_t i = 0; i <= half; i++) {
H[i] *= static_cast<double>(win_freq_[i]); double re = H[i].real();
} double im = H[i].imag();
double exp_re = exp2d::exp2_dsp(re);
H[i] = std::complex<double>(exp_re * std::cos(im), exp_re * std::sin(im));
} }
// Zero upper half again // Step 4: inverse real RFFT (opC)
for (size_t i = half + 1; i < nfft; i++) { std::vector<double> time_domain(nfft);
H[i] = std::complex<double>(0.0, 0.0); fft::execute_real_inverse(&plan_, H.data(), time_domain.data());
// Step 5: Window with WIN_freq (falling half of periodic Hann)
if (!win_freq_.empty() && win_freq_.size() > half) {
for (size_t i = 0; i < half; i++) {
time_domain[i] *= static_cast<double>(win_freq_[half + i]);
}
}
// Zero upper half
for (size_t i = half; i < nfft; i++) {
time_domain[i] = 0.0;
} }
// Normalize: FIR[0]=1, FIR[1]=0 // Step 6: forward real RFFT (opD)
std::vector<std::complex<double>> H_final(half + 1);
fft::execute_real_forward(&plan_, time_domain.data(), H_final.data());
// Step 7: Normalize and copy to output
double scale = 1.0; double scale = 1.0;
if (std::abs(H[0].real()) > 1e-12) { if (std::abs(H_final[0].real()) > 1e-12) {
scale = 1.0 / H[0].real(); scale = 1.0 / H_final[0].real();
} }
for (size_t i = 0; i < nfft; i++) {
fir[i] = H[i] * scale; // Copy to full complex array
for (size_t i = 0; i <= half; i++) {
fir[i] = H_final[i] * scale;
} }
// Mirror for negative frequencies
for (size_t i = 1; i < half; i++) {
fir[nfft - i] = std::conj(fir[i]);
}
// Enforce FIR[0]=1, FIR[1]=0
fir[0] = std::complex<double>(1.0, 0.0); fir[0] = std::complex<double>(1.0, 0.0);
if (half >= 1) { if (half >= 1) {
fir[1] = std::complex<double>(0.0, 0.0); fir[1] = std::complex<double>(0.0, 0.0);