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.
286 lines
9.9 KiB
C++
286 lines
9.9 KiB
C++
#include "spectral.hpp"
|
||
#include "fftconv.hpp"
|
||
#include "log2_ln.hpp"
|
||
#include "exp2_tables.hpp"
|
||
#include "exp2.hpp"
|
||
#include <cmath>
|
||
#include <cstring>
|
||
#include <vector>
|
||
#include <cstdlib>
|
||
#include <cstdio>
|
||
|
||
SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate)
|
||
: nfft_(nfft), hop_(hop), frame_count_(0), output_pos_(0),
|
||
detector_(nfft, sample_rate) {
|
||
window_ = new double[nfft_];
|
||
computeWindow();
|
||
fft::init_plan(&plan_, static_cast<uint32_t>(std::log2(nfft_)));
|
||
buf_ = new std::complex<double>[nfft_];
|
||
tmp_buf_ = new std::complex<double>[nfft_];
|
||
fir_buf_ = new std::complex<double>[nfft_];
|
||
fir_freq_ = new std::complex<double>[nfft_];
|
||
overlap_.resize(nfft_, 0.0f);
|
||
mask_.resize(nfft_, 1.0f);
|
||
|
||
// Build FIR window: falling half of periodic Hann(4096).
|
||
// Plugin reads window[N/2..N-1] of periodic Hann (rising 0→1).
|
||
fir_window_.resize(nfft_);
|
||
for (size_t i = 0; i < nfft_; i++) {
|
||
fir_window_[i] = 0.5 * (1.0 - std::cos(2.0 * M_PI * i / nfft_));
|
||
}
|
||
}
|
||
|
||
SpectralProcessor::~SpectralProcessor() {
|
||
delete[] window_;
|
||
delete[] buf_;
|
||
delete[] tmp_buf_;
|
||
delete[] fir_buf_;
|
||
delete[] fir_freq_;
|
||
}
|
||
|
||
void SpectralProcessor::setDetectorParams(const std::vector<DetectorBand>& bands) {
|
||
detector_.setParams(bands);
|
||
loadWinFreq();
|
||
}
|
||
|
||
void SpectralProcessor::computeWindow() {
|
||
// RT_WIN: 0=symmetric hann (legacy), 1=periodic hann, 2=rectangular
|
||
static const int winmode = getenv("RT_WIN") ? atoi(getenv("RT_WIN")) : 0;
|
||
for (size_t i = 0; i < nfft_; i++) {
|
||
double v;
|
||
if (winmode == 1) v = 0.5 * (1.0 - std::cos(2.0 * M_PI * i / nfft_));
|
||
else if (winmode == 2) v = 1.0;
|
||
else v = 0.5 * (1.0 - std::cos(2.0 * M_PI * i / (nfft_ - 1)));
|
||
window_[i] = v;
|
||
}
|
||
}
|
||
|
||
void SpectralProcessor::stftFrame(const float* in, std::complex<double>* out) {
|
||
for (size_t i = 0; i < nfft_; i++) {
|
||
out[i] = std::complex<double>(static_cast<double>(in[i]) * window_[i], 0.0);
|
||
}
|
||
fft::execute(&plan_, out);
|
||
}
|
||
|
||
void SpectralProcessor::istftFrame(std::complex<double>* in, float* out, float* overlap) {
|
||
memcpy(tmp_buf_, in, nfft_ * sizeof(std::complex<double>));
|
||
fft::execute_inverse(&plan_, tmp_buf_);
|
||
static bool wola_computed = false;
|
||
static float wola_norm = 1.0f;
|
||
// RT_SYN: 0=synthesis window = analysis window (WOLA), 1=none
|
||
static const int synmode = getenv("RT_SYN") ? atoi(getenv("RT_SYN")) : 0;
|
||
if (!wola_computed) {
|
||
double wola_sum = 0.0;
|
||
for (size_t i = 0; i < nfft_; i++) {
|
||
double w = (synmode == 1) ? 1.0 : window_[i];
|
||
wola_sum += window_[i] * w;
|
||
}
|
||
wola_norm = static_cast<float>(wola_sum / hop_);
|
||
wola_computed = true;
|
||
}
|
||
for (size_t i = 0; i < nfft_; i++) {
|
||
double w = (synmode == 1) ? 1.0f : window_[i];
|
||
overlap[i] += static_cast<float>(tmp_buf_[i].real() * w);
|
||
}
|
||
for (size_t i = 0; i < hop_; i++) {
|
||
out[i] = overlap[i] / wola_norm;
|
||
}
|
||
for (size_t i = 0; i < nfft_ - hop_; i++) {
|
||
overlap[i] = overlap[i + hop_];
|
||
}
|
||
for (size_t i = nfft_ - hop_; i < nfft_; i++) {
|
||
overlap[i] = 0.0f;
|
||
}
|
||
}
|
||
|
||
void SpectralProcessor::loadWinFreq() {
|
||
if (win_freq_loaded_) return;
|
||
win_freq_loaded_ = true;
|
||
// Try to load WIN_freq from live capture (handoff/rtwin_freq_44100.npy)
|
||
FILE* f = fopen("handoff/rtwin_freq_44100.npy", "rb");
|
||
if (!f) {
|
||
// Fallback: compute periodic Hann, second half (0.5→1.0 rising)
|
||
win_freq_.resize(nfft_ / 2 + 1);
|
||
for (size_t i = 0; i <= nfft_ / 2; i++) {
|
||
win_freq_[i] = static_cast<float>(0.5 * (1.0 - std::cos(2.0 * M_PI * i / nfft_)));
|
||
}
|
||
return;
|
||
}
|
||
// Read numpy header
|
||
char header[128];
|
||
if (fread(header, 1, 6, f) != 6) { fclose(f); return; }
|
||
// Skip to data (numpy format: magic + header_len + desc)
|
||
fseek(f, 0, SEEK_END);
|
||
long fsize = ftell(f);
|
||
fseek(f, 0, SEEK_SET);
|
||
// Simple approach: skip header until '\n' appears, then read raw float32
|
||
fseek(f, 0, SEEK_SET);
|
||
int c;
|
||
while ((c = fgetc(f)) != '\n' && c != EOF) {}
|
||
// Read count (should be 8193 for 44100)
|
||
int32_t count = 0;
|
||
fread(&count, 4, 1, f);
|
||
// Actually numpy header is more complex; just read all remaining as float32
|
||
fseek(f, 0, SEEK_SET);
|
||
// Skip to data: find first 'N' (for 'astype') then skip past it
|
||
fseek(f, 6, SEEK_SET);
|
||
while ((c = fgetc(f)) != '\n' && c != EOF) {}
|
||
// Now at data start. Read until we have enough floats
|
||
std::vector<float> raw;
|
||
float val;
|
||
while (fread(&val, 4, 1, f) == 1) {
|
||
raw.push_back(val);
|
||
}
|
||
fclose(f);
|
||
if (raw.size() > 0) {
|
||
win_freq_ = raw;
|
||
} else {
|
||
// Fallback
|
||
win_freq_.resize(nfft_ / 2 + 1);
|
||
for (size_t i = 0; i <= nfft_ / 2; i++) {
|
||
win_freq_[i] = static_cast<float>(0.5 * (1.0 - std::cos(2.0 * M_PI * i / nfft_)));
|
||
}
|
||
}
|
||
}
|
||
|
||
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.
|
||
// 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_;
|
||
|
||
// Compute ln(mask) and negate
|
||
std::vector<std::complex<double>> H(nfft);
|
||
for (size_t i = 0; i <= half; i++) {
|
||
float m = mask[i];
|
||
if (m > 1e-12f) {
|
||
float ln_m = soothe2::ln_plugin_f32(m);
|
||
ln_m = -ln_m;
|
||
H[i] = std::complex<double>(static_cast<double>(ln_m), 0.0);
|
||
} else {
|
||
H[i] = std::complex<double>(0.0, 0.0);
|
||
}
|
||
}
|
||
|
||
// Zero upper half
|
||
for (size_t i = half + 1; i < nfft; i++) {
|
||
H[i] = std::complex<double>(0.0, 0.0);
|
||
}
|
||
|
||
// IFFT to time domain
|
||
fft::execute_inverse(&plan_, H.data());
|
||
|
||
// Causal window: keep first half, apply rising Hann (0.5→1.0)
|
||
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++) {
|
||
H[i] *= static_cast<double>(win_freq_[i]);
|
||
}
|
||
}
|
||
|
||
// Zero upper half again
|
||
for (size_t i = half + 1; i < nfft; i++) {
|
||
H[i] = std::complex<double>(0.0, 0.0);
|
||
}
|
||
|
||
// Normalize: FIR[0]=1, FIR[1]=0
|
||
double scale = 1.0;
|
||
if (std::abs(H[0].real()) > 1e-12) {
|
||
scale = 1.0 / H[0].real();
|
||
}
|
||
for (size_t i = 0; i < nfft; i++) {
|
||
fir[i] = H[i] * scale;
|
||
}
|
||
fir[0] = std::complex<double>(1.0, 0.0);
|
||
if (half >= 1) {
|
||
fir[1] = std::complex<double>(0.0, 0.0);
|
||
}
|
||
}
|
||
|
||
void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples, size_t num_channels) {
|
||
memset(out, 0, num_samples * sizeof(float));
|
||
if (num_samples == 0 || num_samples < nfft_) {
|
||
return;
|
||
}
|
||
|
||
static const int firconv = []() {
|
||
const char* e = getenv("RT_FIRCONV");
|
||
return e ? atoi(e) : 0;
|
||
}();
|
||
|
||
size_t nframes = (num_samples - nfft_) / hop_ + 1;
|
||
|
||
for (size_t f = 0; f < nframes; f++) {
|
||
size_t offset = f * hop_;
|
||
if (offset + nfft_ > num_samples) break;
|
||
stftFrame(in + offset, buf_);
|
||
|
||
detector_.processFrame(buf_, mask_.data());
|
||
|
||
if (firconv == 3) {
|
||
// RT_FIRCONV=3 (NOTES 24k): plugin application law decoded live:
|
||
// applied_gain = 1.019 * V^1.8345 per bin (rms 0.0025 dB over
|
||
// 8 drive levels). V = band curve (detector output); here M.
|
||
for (size_t i = 0; i < nfft_; i++) {
|
||
double m = std::max(static_cast<double>(mask_[i]), 1e-12);
|
||
double a = 1.019 * std::pow(m, 1.8345);
|
||
buf_[i] *= a;
|
||
}
|
||
} else if (firconv) {
|
||
// RT_FIRCONV=2: Full FIR construction pipeline (52b550-52b8bb).
|
||
// mask → reciprocal (1/mask) → window → normalize → complex multiply.
|
||
// This replicates the plugin's FFT-conv FIR design path.
|
||
buildFirFromMask(mask_.data(), fir_freq_, nfft_);
|
||
// Complex multiply FIR × audio spectrum
|
||
for (size_t i = 0; i < nfft_; i++) {
|
||
buf_[i] *= fir_freq_[i];
|
||
}
|
||
} else if (firconv == 1) {
|
||
// RT_FIRCONV=1: Simple frequency-domain mask multiply (legacy).
|
||
for (size_t i = 0; i < nfft_; i++) {
|
||
fir_freq_[i] = std::complex<double>(
|
||
static_cast<double>(mask_[i % (nfft_/2+1)]), 0.0);
|
||
}
|
||
for (size_t i = 0; i < nfft_; i++) {
|
||
buf_[i] *= fir_freq_[i];
|
||
}
|
||
} else {
|
||
// Default path: simple frequency-domain mask multiply.
|
||
for (size_t i = 0; i < nfft_; i++) {
|
||
buf_[i] *= mask_[i];
|
||
}
|
||
}
|
||
|
||
istftFrame(buf_, out + offset, overlap_.data());
|
||
}
|
||
}
|