feat: consumer identified (th_b3c0), scan3.py, RT_FIRCONV/RT_FIRPOWER

- th_b3c0 (0x18000b3c0) = pure complex multiply FIR × audio in freq-domain
- scan3.py: pre-scan approach finds ctx in 1.5s, multi-instance detection
- RT_FIRCONV=1: FIR from mask + complex multiply (spectral.cpp)
- RT_FIRPOWER=1: power-law mask from raw spectrum (framed_model.cpp)
- Root cause: plugin uses FIR convolution (OLA), not per-bin multiply
- Live captures: FIR@43=0.524, mask@43=0.510, final gain=0.305
- Best result: RT_LUT_OFF gives cut@500=-8.18 dB (ref -10.32)
- NOTES_LEVEL 24e/24f/24g appended
This commit is contained in:
2026-08-24 13:52:52 +03:00
parent 03777fdaee
commit 2c99a4fc96
5 changed files with 451 additions and 14 deletions
+40 -12
View File
@@ -134,9 +134,16 @@ static void process_band_structural(
for (size_t k = 0; k < nbin; k++) if (lvl_in[k] > cap) lvl_in[k] = cap;
}
// Save raw level BEFORE LUT transform (for RT_FIRPOWER)
std::vector<float> raw_level(nbin);
for (size_t k = 0; k < nbin; k++) {
double res_k = std::max(static_cast<double>(res[k]), 1e-12);
double lvl = lvl_in[k];
raw_level[k] = static_cast<float>(static_cast<double>(am[k]) / res_k * scale_factor_x);
}
for (size_t k = 0; k < nbin; k++) {
double res_k = std::max(static_cast<double>(res[k]), 1e-12);
double lvl = raw_level[k];
if (!lut_off) {
// dB-domain LUT (FUN_180563a60) on LEVEL before IIR/exp2: keeps both
// quiet (t1kq) and loud (t1k) inputs inside the LUT domain [A,B],
@@ -208,17 +215,38 @@ static void process_band_structural(
}
for (size_t k = 0; k < nfft; k++) {
double mm = std::exp2(-static_cast<double>(band_level[k]));
static const int noblend = getenv("RT_NOBLEND") ? atoi(getenv("RT_NOBLEND")) : 0;
if (!noblend) mm *= f6f8[k];
// RT_LAWAFFINE="A,S" (NOTES 22q): cut_dB = A + S*log2(lvl) — affine dB law
static const char* la = getenv("RT_LAWAFFINE");
if (la && lut_off) {
double A_db = atof(la); const char* cm = strchr(la, ',');
double S_db = cm ? atof(cm + 1) : 2.17;
if (band_level[k] > 1e-6) {
double y = (A_db + S_db * std::log2(band_level[k])) / 6.0174;
mm = std::exp2(-y);
double mm;
// RT_FIRPOWER=1: FIR-style mask from raw spectrum.
// Plugin's actual pipeline (52b550-52b8bb):
// 1. scratch = log(raw_spectrum)
// 2. FIR = exp(0.984 × scratch) = raw^0.984
// 3. FIR *= hann_window (freq-domain)
// 4. FIR *= 0x540888 (scalar)
// 5. FIR applied via time-domain convolution (not pointwise multiply)
//
// For our structural chain (pointwise mask):
// mask = raw^0.984 × hann × 0x540888
// where hann rises from 0→1 (DC→Nyquist)
static const int firpower = getenv("RT_FIRPOWER") ? atoi(getenv("RT_FIRPOWER")) : 0;
if (firpower) {
double raw = static_cast<double>(raw_level[k]);
if (raw > 1e-12) {
mm = std::pow(raw, 0.984);
} else {
mm = 1.0;
}
} else {
mm = std::exp2(-static_cast<double>(band_level[k]));
static const int noblend = getenv("RT_NOBLEND") ? atoi(getenv("RT_NOBLEND")) : 0;
if (!noblend) mm *= f6f8[k];
static const char* la = getenv("RT_LAWAFFINE");
if (la && lut_off) {
double A_db = atof(la); const char* cm = strchr(la, ',');
double S_db = cm ? atof(cm + 1) : 2.17;
if (band_level[k] > 1e-6) {
double y = (A_db + S_db * std::log2(band_level[k])) / 6.0174;
mm = std::exp2(-y);
}
}
}
mask_out[k] = static_cast<float>(mm);
+37 -2
View File
@@ -1,7 +1,9 @@
#include "spectral.hpp"
#include "fftconv.hpp"
#include <cmath>
#include <cstring>
#include <vector>
#include <cstdlib>
SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate)
: nfft_(nfft), hop_(hop), frame_count_(0), output_pos_(0),
@@ -11,14 +13,25 @@ SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate)
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) {
@@ -71,6 +84,11 @@ void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples,
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++) {
@@ -80,8 +98,25 @@ void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples,
detector_.processFrame(buf_, mask_.data());
for (size_t i = 0; i < nfft_; i++) {
buf_[i] *= mask_[i];
if (firconv) {
// RT_FIRCONV=1: Build FIR from mask and apply via complex multiply.
// The mask is real-valued (per-bin gain). We apply it directly
// to the audio spectrum via complex multiply (th_b3c0 equivalent).
// No upper-half zeroing — preserve Hermitian symmetry.
for (size_t i = 0; i < nfft_; i++) {
fir_freq_[i] = std::complex<double>(
static_cast<double>(mask_[i % (nfft_/2+1)]), 0.0);
}
// Complex multiply FIR × audio spectrum.
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());
+3
View File
@@ -25,6 +25,9 @@ private:
FFTPlan plan_;
std::complex<double>* buf_;
std::complex<double>* tmp_buf_;
std::complex<double>* fir_buf_;
std::complex<double>* fir_freq_;
std::vector<double> fir_window_;
std::vector<float> overlap_;
std::vector<float> mask_;
FramedDetector detector_;