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
This commit is contained in:
2026-09-02 23:19:34 +03:00
parent e4c53480ad
commit 2f854cd1da
8 changed files with 163 additions and 29 deletions
+2
View File
@@ -42,11 +42,13 @@ add_executable(vlog_check vlog_check.cpp)
add_executable(leveltrack_check leveltrack_check.cpp)
add_executable(levelpath_check levelpath_check.cpp)
add_executable(exp2_check exp2_check.cpp)
add_executable(vlaw_check vlaw_check.cpp)
add_executable(fn529fe0_check fn529fe0_check.cpp)
target_link_libraries(twin_check soothe2_dsp)
target_link_libraries(framed_test soothe2_dsp)
target_link_libraries(render48k soothe2_dsp ${SAMPLERATE})
target_link_libraries(exp2_check soothe2_dsp)
target_link_libraries(vlaw_check soothe2_dsp)
target_link_libraries(fn529fe0_check soothe2_dsp)
target_link_libraries(tables_check soothe2_dsp)
target_link_libraries(fftconv_check soothe2_dsp)
+3 -4
View File
@@ -316,10 +316,9 @@ static void process_band_structural(
if (delta_state) {
}
for (size_t k2 = 0; k2 < nbin; k2++) {
double cs = vlaw_alpha * std::log1p(static_cast<double>(raw_level[k2]) / vlaw_beta)
+ vlaw_c
+ (delta_mark[k2] ? vlaw_delta : 0.0);
band_level[k2] = static_cast<float>(std::pow(10.0, -cs / 20.0));
band_level[k2] = static_cast<float>(vlaw_mask(
static_cast<double>(raw_level[k2]), vlaw_alpha, vlaw_beta,
vlaw_c, delta_mark[k2] ? vlaw_delta : 0.0));
}
frame_dbg_ctr++;
} else
+12
View File
@@ -1,5 +1,6 @@
#pragma once
#include <cstddef>
#include <cmath>
#include <complex>
#include <vector>
#include "fn529fe0.hpp"
@@ -45,6 +46,17 @@ inline double lut_parametric(double x, double A, double B, double gamma) {
return A + (B - A) * 0.5 * (1.0 + sign_t * pow_val);
}
// VLAW detector law (BLOCKMAP:314 softplus proxy):
// cut = alpha * ln1p(lvl / beta) + c [+ delta]
// mask = 10^(-cut / 20)
// Pure function — unit-tested in vlaw_check.cpp.
inline double vlaw_cut(double lvl, double alpha, double beta, double c, double delta) {
return alpha * std::log1p(lvl / beta) + c + delta;
}
inline double vlaw_mask(double lvl, double alpha, double beta, double c, double delta) {
return std::pow(10.0, -vlaw_cut(lvl, alpha, beta, c, delta) / 20.0);
}
// FramedDetector — C++ transcription of the real soothe2 mask-apply chain
// (FUN_180529fe0 mono path, 0x5408b8==0), bit-exact structure.
+12 -18
View File
@@ -12,13 +12,13 @@
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_];
window_.resize(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_];
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);
@@ -30,13 +30,7 @@ SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate)
}
}
SpectralProcessor::~SpectralProcessor() {
delete[] window_;
delete[] buf_;
delete[] tmp_buf_;
delete[] fir_buf_;
delete[] fir_freq_;
}
SpectralProcessor::~SpectralProcessor() = default;
void SpectralProcessor::setDetectorParams(const std::vector<DetectorBand>& bands) {
detector_.setParams(bands);
@@ -63,8 +57,8 @@ void SpectralProcessor::stftFrame(const float* in, std::complex<double>* 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_);
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
@@ -279,9 +273,9 @@ void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples,
for (size_t f = 0; f < nframes; f++) {
size_t offset = f * hop_;
if (offset + nfft_ > num_samples) break;
stftFrame(in + offset, buf_);
stftFrame(in + offset, buf_.data());
detector_.processFrame(buf_, mask_.data());
detector_.processFrame(buf_.data(), mask_.data());
if (firconv == 3) {
// RT_FIRCONV=3 (NOTES 24k): plugin application law decoded live:
@@ -296,7 +290,7 @@ void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples,
// 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_);
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];
@@ -317,6 +311,6 @@ void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples,
}
}
istftFrame(buf_, out + offset, overlap_.data());
istftFrame(buf_.data(), out + offset, overlap_.data());
}
}
+5 -5
View File
@@ -21,12 +21,12 @@ public:
private:
size_t nfft_;
size_t hop_;
double* window_;
std::vector<double> window_;
FFTPlan plan_;
std::complex<double>* buf_;
std::complex<double>* tmp_buf_;
std::complex<double>* fir_buf_;
std::complex<double>* fir_freq_;
std::vector<std::complex<double>> buf_;
std::vector<std::complex<double>> tmp_buf_;
std::vector<std::complex<double>> fir_buf_;
std::vector<std::complex<double>> fir_freq_;
std::vector<double> fir_window_;
std::vector<float> overlap_;
std::vector<float> mask_;
+55
View File
@@ -0,0 +1,55 @@
#include <cstdio>
#include <cmath>
#include "framed_model.hpp"
// Unit check for the VLAW detector law (BLOCKMAP:314 softplus proxy):
// cut = alpha * ln1p(lvl/beta) + c [+ delta]
// mask = 10^(-cut/20)
// Reference values hand-computed from the dual-calibrated constants
// (alpha=3.2193, beta=0.4927, c=0.5423, delta=6.9177 — README.md:26).
int main() {
int fail = 0;
// --- law monotonicity: higher level -> stronger cut -> smaller mask ---
double m0 = vlaw_mask(0.01, 3.2193, 0.4927, 0.5423, 0.0);
double m1 = vlaw_mask(1.0, 3.2193, 0.4927, 0.5423, 0.0);
double m2 = vlaw_mask(10.0, 3.2193, 0.4927, 0.5423, 0.0);
bool mono = (m0 > m1) && (m1 > m2);
std::printf("vlaw monotonic: m(0.01)=%.4f m(1)=%.4f m(10)=%.4f (%s)\n",
m0, m1, m2, mono ? "OK" : "MISMATCH");
if (!mono) fail = 1;
// --- zero level: cut = c => mask = 10^(-c/20) ---
double mz = vlaw_mask(0.0, 3.2193, 0.4927, 0.5423, 0.0);
double ez = std::pow(10.0, -0.5423 / 20.0);
bool zok = std::fabs(mz - ez) < 1e-9;
std::printf("vlaw zero-level: mask=%.6f expect=%.6f (%s)\n",
mz, ez, zok ? "OK" : "MISMATCH");
if (!zok) fail = 1;
// --- delta branch adds cut -> deeper mask ---
double md = vlaw_mask(1.0, 3.2193, 0.4927, 0.5423, 6.9177);
bool dok = md < m1;
std::printf("vlaw delta: mask+delta=%.4f < %.4f (%s)\n",
md, m1, dok ? "OK" : "MISMATCH");
if (!dok) fail = 1;
// --- numeric reference: lvl=1.0, dual params ---
// cut = 3.2193 * ln(1 + 1/0.4927) + 0.5423
double cut_ref = 3.2193 * std::log1p(1.0 / 0.4927) + 0.5423;
double mref = std::pow(10.0, -cut_ref / 20.0);
bool rok = std::fabs(m1 - mref) < 1e-9;
std::printf("vlaw ref: mask=%.6f expect=%.6f cut=%.4f (%s)\n",
m1, mref, cut_ref, rok ? "OK" : "MISMATCH");
if (!rok) fail = 1;
// --- comb neutrality: alpha=0.05 beta=5.0 c=0 -> mask ~ 1 for lvl=0 ---
double mc = vlaw_mask(0.0, 0.05, 5.0, 0.0, 0.0);
bool cok = std::fabs(mc - 1.0) < 1e-9;
std::printf("vlaw comb-neutral: mask(0)=%.6f expect=1.0 (%s)\n",
mc, cok ? "OK" : "MISMATCH");
if (!cok) fail = 1;
std::printf("vlaw_check %s\n", fail ? "FAIL" : "PASS");
return fail;
}