- 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
317 lines
12 KiB
C++
317 lines
12 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_.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/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<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());
|
||
}
|
||
}
|