Files
soothe2-re/dsp/spectral.cpp
T
Matiq 2f854cd1da spectral: vectors instead of new[]; vlaw: extract law + vlaw_check target
- spectral.cpp: window_/buf_/tmp_buf_/fir_buf_/fir_freq_ as std::vector (no
  exception-leak in ctor, destructor = default)
- framed_model.hpp: extract vlaw_cut/vlaw_mask inline (BLOCKMAP:314 softplus)
- dsp/vlaw_check.cpp: unit test for law (monotonic, zero-level, delta, ref,
  comb-neutral) — PASS
- CMake: add vlaw_check target
- Guard: corpus --compare d=+0.000, fn529fe0_check PASS, twin_check PASS
2026-09-02 23:19:34 +03:00

317 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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_.resize(nfft_);
computeWindow();
fft::init_plan(&plan_, static_cast<uint32_t>(std::log2(nfft_)));
buf_.resize(nfft_);
tmp_buf_.resize(nfft_);
fir_buf_.resize(nfft_);
fir_freq_.resize(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() = default;
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_.data(), in, nfft_ * sizeof(std::complex<double>));
fft::execute_inverse(&plan_, tmp_buf_.data());
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) {
// 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<double> buf548;
static std::vector<float> 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<double> 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<double>(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<std::complex<double>> H(half + 1);
for (size_t i = 0; i <= half; i++) {
H[i] = std::complex<double>(design[i], 0.0);
}
std::vector<double> 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/21) деление" - DIVIDE FIR[1..2047]
// "52db50(&FIR[2049], xmm9, n/21)" - 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<std::complex<double>> 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<double>(exp_mag * std::cos(angle), exp_mag * std::sin(angle));
} else {
freq_domain[i] = std::complex<double>(1.0, 0.0);
}
}
// Step 6: opC = inv-RFFT (th2180): freq_domain → time domain
std::vector<double> 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<double>(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<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_.data());
detector_.processFrame(buf_.data(), 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_.data(), 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_.data(), out + offset, overlap_.data());
}
}