#include "spectral.hpp" #include "fftconv.hpp" #include "log2_ln.hpp" #include "exp2_tables.hpp" #include "exp2.hpp" #include #include #include #include #include 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(std::log2(nfft_))); buf_ = new std::complex[nfft_]; tmp_buf_ = new std::complex[nfft_]; fir_buf_ = new std::complex[nfft_]; fir_freq_ = new std::complex[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& 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* out) { for (size_t i = 0; i < nfft_; i++) { out[i] = std::complex(static_cast(in[i]) * window_[i], 0.0); } fft::execute(&plan_, out); } void SpectralProcessor::istftFrame(std::complex* in, float* out, float* overlap) { memcpy(tmp_buf_, in, nfft_ * sizeof(std::complex)); 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(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(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(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 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(0.5 * (1.0 - std::cos(2.0 * M_PI * i / nfft_))); } } } void SpectralProcessor::buildFirFromMask(const float* mask, std::complex* fir, size_t nbin) { // Bit-exact FIR construction pipeline from decompilation (BLOCKMAP 24mm9): // 1. design = ln(mask) → negate // 2. opA = inv-RFFT (th2180) with buf548 // 3. fold: bins 1..2047 *= 2.0, bins 2049..4095 = 0 // 4. opB = fwd-RFFT (th1a90) with buf548 // 5. EXP: complex polynomial exp with q≈0.80 scaling // 6. opC = inv-RFFT (th2180) with buf548 // 7. zero Nyquist // 8. window: falling Hann WIN_freq[2048..4095] (w[1024]=0.5, w[2048]=1.0) // 9. opD = fwd-RFFT (th1a90) with buf548 // 10. normalize: FIR[0]=1.0, FIR[1]=0.0 const size_t half = nfft_ / 2; const size_t nfft = nfft_; // Build buf548 and mask598 tables (plugin's exact parameters) static std::vector buf548; static std::vector mask598; static bool tables_built = false; if (!tables_built) { buf548.resize(nfft); // N doubles = 2 * N/2 entries mask598.resize(nfft / 4); // N/4 floats fft::build_buf548(buf548.data(), nfft); fft::build_mask598(mask598.data(), nfft); tables_built = true; } // Step 1: design = ln(mask) and negate (already in real domain) // Input is real mask [nbin], convert to real array for RFFT std::vector design(nfft, 0.0); for (size_t i = 0; i <= half; i++) { float m = mask[i]; if (m > 1e-12f) { float ln_m = soothe2::ln_plugin_f32(m); design[i] = -static_cast(ln_m); } else { design[i] = 0.0; } } // Step 2: opA = inv-RFFT (th2180): design (real) → time domain // But wait: inv-RFFT takes N/2+1 complex → N real // We need to pack design as complex first (im=0) std::vector> H(half + 1); for (size_t i = 0; i <= half; i++) { H[i] = std::complex(design[i], 0.0); } std::vector time_domain(nfft); fft::execute_real_inverse_exact(&plan_, H.data(), time_domain.data(), buf548.data(), mask598.data()); // Step 3: fold - from BLOCKMAP: "FIR[n]=0 (n=0x540534=4096!)" // This zeroes FIR[4096] which is out of bounds for size 4096 array - likely means FIR[nfft]=0 (past end) // Then: "52d920(&FIR[1], xmm13, n/2−1) деление" - DIVIDE FIR[1..2047] // "52db50(&FIR[2049], xmm9, n/2−1)" - multiply/zero FIR[2049..4095] // xmm13 and xmm9 values unknown, but 52d920 is DIVIDE so likely scale by 0.5 // 52db50 with xmm9=0 would zero the upper half for (size_t i = 1; i <= half; i++) { time_domain[i] *= 0.5; // DIVIDE by 2 (xmm13 = 0.5?) } for (size_t i = half + 1; i < nfft; i++) { time_domain[i] = 0.0; // xmm9 = 0 zeros upper half } // Step 4: opB = fwd-RFFT (th1a90): time_domain (real) → complex std::vector> freq_domain(half + 1); fft::execute_real_forward_exact(&plan_, time_domain.data(), freq_domain.data(), buf548.data(), mask598.data()); // Step 5: EXP: complex polynomial exp with q≈0.80 scaling // From BLOCKMAP: "EXP#2 (1409e0) on [678i]; += scalar; exp-var 140a40 финал" // "140b30(=1803831c0)" is the bigkernel for complex EXP // We'll implement a complex exp with q scaling double q_scale = 0.80; for (size_t i = 0; i <= half; i++) { double re = freq_domain[i].real(); double im = freq_domain[i].imag(); double mag = std::sqrt(re*re + im*im); if (mag > 1e-12) { double angle = std::atan2(im, re); double exp_mag = std::exp(q_scale * mag); freq_domain[i] = std::complex(exp_mag * std::cos(angle), exp_mag * std::sin(angle)); } else { freq_domain[i] = std::complex(1.0, 0.0); } } // Step 6: opC = inv-RFFT (th2180): freq_domain → time domain std::vector time_domain2(nfft); fft::execute_real_inverse_exact(&plan_, freq_domain.data(), time_domain2.data(), buf548.data(), mask598.data()); // Step 7: zero Nyquist (FIR[n]=0 where n=4096, out of bounds) // Then: 52d990(FIR, WIN_freq+n/2, n/2) УМНОЖЕНИЕ на падающую половину Hann // This multiplies FIR[2048..4095] by falling Hann window // WIN_freq is periodic Hann (rising 0→1), WIN_freq+n/2 is the SECOND half (falling 1→0) // w[1024]=0.5, w[2048]=1.0 means: // - For i=2048 (offset 0): window = WIN_freq[2048+0] = WIN_freq[2048] = 1.0 // - For i=3072 (offset 1024): window = WIN_freq[2048+1024] = WIN_freq[3072] = 0.5 // - For i=4095 (offset 2047): window = WIN_freq[2048+2047] = WIN_freq[4095] = 0.0 for (size_t i = half; i < nfft; i++) { size_t win_idx = half + (i - half); if (win_idx < win_freq_.size()) { time_domain2[i] *= static_cast(win_freq_[win_idx]); } else { // Falling Hann: 0.5 * (1.0 + cos(2*pi*i/N)) double win = 0.5 * (1.0 + std::cos(2.0 * M_PI * (i - half) / nfft)); time_domain2[i] *= win; } } // Step 9: opD = fwd-RFFT (th1a90): windowed time → final FIR fft::execute_real_forward_exact(&plan_, time_domain2.data(), fir, buf548.data(), mask598.data()); // Step 10: normalize: FIR[0]=1.0, FIR[1]=0.0 fir[0] = std::complex(1.0, 0.0); if (half > 1) { fir[1] = std::complex(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(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( static_cast(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()); } }