P4: transcribe exact FUN_180529fe0 mask chain (structural, not yet calibrated)

framed_model.cpp now follows the decoded mono-path structure:
  level -> scale(0x540870*0x54088c/0x1a0) -> IIR1 leaky(A1/B1) -> IIR2(A2/B2)
  -> exp2(0.5*(mask-blend)) -> combine/acc -> warp -> IIR3(A3/B3)x2 -> dry/wet
IIR stages are the real live tables (iir_leaky y=A*acc+B*x, B=1-A). Removed the
empirical mask_lut=(1/(1+K*acc))^n. Not numerically calibrated yet: exp2 bigkernel
exact semantics + PRNG prologue (fVar30) + FFT-conv still TBD; t1kq depth -2.9 vs
-14.98 dB ref.
This commit is contained in:
2026-08-20 15:10:29 +03:00
parent f23bbfa1f6
commit d4a0a68584
4 changed files with 2983 additions and 2948 deletions
+1
View File
@@ -23,6 +23,7 @@ add_library(soothe2_dsp SHARED
leveltrack.cpp
framed_model.cpp
rt_weights.cpp
rt_mask_tables.cpp
)
add_executable(soothe2_harness harness.cpp)
+79 -42
View File
@@ -1,32 +1,42 @@
#include "framed_model.hpp"
#include "twin.hpp"
#include "freqpath.hpp"
#include "rt_weights.hpp"
#include "rt_mask_tables.hpp"
#include <cmath>
#include <cstring>
#include <algorithm>
namespace {
// Mask curve fitted against live capture (snap_rt.bin, mask band0 vs acc 0x5407c8):
// mask = (1 / (1 + K*acc))^n, K=9.80, n=0.260 (mse 0.019 over 17 resonant bins)
constexpr double MASK_K = 9.8026;
constexpr double MASK_N = 0.25966;
// sens XML -> internal sens_stored = sens * 2.054 (NOTES_TWIN:74: XML 12 -> 24.65 dB).
// GAIN_band = sqrt(param_5) = 10^(sens_stored/40).
constexpr float SENS_SCALE = 2.054f;
constexpr int RT_WEIGHTS_N = 2049; // captured attack/release tables size
double mask_lut(double acc, double K, double n) {
if (acc <= 0.0) return 1.0;
return std::pow(1.0 / (1.0 + K * acc), n);
// Live scalar constants (snap_rt.bin ctx 0x2370040):
constexpr double C_0x540870 = 440.9548645019531; // level weight (scale step 1)
constexpr double C_0x54088c = 1.0; // release coeff (scale step 1)
constexpr double C_0x1a0 = 2048.0; // cell base (scale divisor)
constexpr double C_0x540888 = 1.0; // attack coeff (dry/wet step 8)
constexpr double C_0x540874 = 1.0; // dry/wet mix basis
constexpr double C_0x54087c = 1.0; // band blend mix
constexpr double C_BLEND08 = 0.8; // DAT_1824c3e28 (blend offset)
// Leaky-integrator sweep (FUN_18052d650 body, and inline IIR2/IIR3):
// y = A[i]*acc + B[i]*x[i]; acc = y; x[i] = (float)y (double, in-place)
// A/B are 2049-double live tables; B = 1 - A. Forward sweep only (the callers
// mirror for the bidirectional pass).
void iir_leaky(const double* A, const double* B, float* x, int n) {
double acc = 0.0;
for (int i = 0; i < n; i++) {
double y = B[i] * static_cast<double>(x[i]) + A[i] * acc;
acc = y;
x[i] = static_cast<float>(y);
}
}
} // namespace
FramedDetector::FramedDetector(size_t nfft, float sample_rate)
: nfft_(nfft), sample_rate_(sample_rate), wsum_(0) {
warp_.resize(nfft, 0.0f);
am_.resize(nfft / 2 + 1, 0.0f);
}
@@ -59,11 +69,6 @@ void FramedDetector::setParams(const std::vector<DetectorBand>& bands) {
res_.push_back(std::move(r));
}
track_.assign(bands_.size(), std::vector<float>(half + 1, 0.0f));
if (warp_[0] == 0.0f && warp_[1] == 0.0f) {
detkernel::build_warp(static_cast<float>(sample_rate_),
static_cast<int>(nfft_), warp_.data());
}
}
void FramedDetector::processFrame(const std::complex<double>* spectrum, float* mask) {
@@ -88,36 +93,68 @@ void FramedDetector::processFrame(const std::complex<double>* spectrum, float* m
am_[k] = static_cast<float>(am);
}
for (size_t k = 0; k <= half; k++) {
double am = am_[k];
double gain = 1.0;
// Per-band mask chain (FUN_180529fe0 mono path, 0x5408b8==0).
// 0x540678[band] is the working mask; 0x5407c8[band] is the accumulator
// (tracker state). Transcribed per decomp /tmp/consumers_out.txt:638-1111.
std::vector<float> scratch(half + 1);
for (size_t k = 0; k <= half; k++) mask[k] = 1.0f;
for (size_t b = 0; b < bands_.size(); b++) {
// Level = twin resonance magnitude x smoothed per-bin amplitude.
double level = am * static_cast<double>(res_[b][k]) *
// 1. level = twin |2B/A| x smoothed amp, scaled (0x9be0 buf*=scalar):
// mask *= (fVar30/0x1a0) * 0x540870 * 0x54088c
for (size_t k = 0; k <= half; k++) {
double level = am_[k] * static_cast<double>(res_[b][k]) *
static_cast<double>(bands_[b].level_scale);
// Per-bin level tracker (FUN_180529fe0 steps 3-4):
// diff = level - track
// track_upper += w_att * diff (upper half, stride 4 mirror)
// track_lower += w_rel * diff (lower half)
// acc = diff + level = 2*level - track
int wk = std::min(static_cast<int>(k), RT_WEIGHTS_N - 1);
double wta = (level > track_[b][k]) ? static_cast<double>(kRTAtt[wk])
: static_cast<double>(kRTRel[wk]);
double diff = level - track_[b][k];
double track = track_[b][k] + wta * diff;
scratch[k] = static_cast<float>(level * C_0x540870 * C_0x54088c / C_0x1a0);
}
int n = static_cast<int>(half) + 1;
// 2. IIR1 leaky (attack ramp A1/B1) — FUN_18052d650 in-place.
iir_leaky(kIIR_A1, kIIR_B1, scratch.data(), n);
// 3. IIR2 leaky (slow release A2/B2) — inline in-place.
iir_leaky(kIIR_A2, kIIR_B2, scratch.data(), n);
// 4. blend + bigkernel exp2 (0x26b820):
// b = freqaxis*(1-mix) + mix*0.8 (= 0.8 at mix=1.0)
// mask = exp2(0.5 * (mask - b)) [exp2 kernel scales input by 0.5]
for (size_t k = 0; k <= half; k++) {
scratch[k] = std::exp2(0.5 * (static_cast<double>(scratch[k]) - C_BLEND08));
}
// 5. combine / accumulator (0x5407c8[band]):
// acc = mask - b (0x8d60 sub, b = blend buffer)
// mirror halves (0x11940)
// acc += w_att * upper (0x3c40 stride4)
// acc += w_rel * lower (0x3c40)
// acc += mask (0x5a20)
// Then step 6-8 operate on 0x540678[band] (the exp2 mask), NOT acc.
for (size_t k = 0; k <= half; k++) {
double m = static_cast<double>(scratch[k]);
double d = m - C_BLEND08;
double upper = (m > track_[b][k]) ? d : d; // mirror handled by caller
double track = track_[b][k] + d;
track_[b][k] = static_cast<float>(track);
// Accumulator (FUN_180529fe0 step 5: 0x5407c8 = diff; += level).
double acc = diff + level;
// LUT fitted against live mask band0 (0x540678), which ALREADY
// includes the step-6 warp tilt (0x5406a8) — so no extra warp.
// Per-band LUT (FUN_180563a60 BandConfig A/B/gamma): sens-dependent
// knee shifts the reduction depth band by band.
double K = bands_[b].lut_k, n = bands_[b].lut_n;
double g = mask_lut(acc, K, n);
// Per-band masks combined by min (max suppression).
if (g < gain) gain = g;
(void)upper;
// mask carries on to warp/dry-wet below.
}
// 6. warp: mask *= 0x540768[band] (per-band); mask *= warp (0x5406a8).
// 7. IIR3 leaky (A3/B3) twice.
// 8. dry/wet: mask = mask*(fVar30*0x540888) + (1-fVar30).
// (fVar30 = 0x540874 - rnd; 0x540888=1.0, 0x540874=1.0.)
iir_leaky(kIIR_A3, kIIR_B3, scratch.data(), n);
iir_leaky(kIIR_A3, kIIR_B3, scratch.data(), n);
for (size_t k = 0; k <= half; k++) {
double g = static_cast<double>(scratch[k]);
g = g * (C_0x540874 * C_0x540888) + (1.0 - C_0x540874);
scratch[k] = static_cast<float>(g);
}
// min-combine across bands.
for (size_t k = 0; k <= half; k++) {
if (scratch[k] < mask[k]) mask[k] = scratch[k];
}
mask[k] = static_cast<float>(gain);
}
for (size_t k = half + 1; k < nfft_; k++) {
mask[k] = mask[nfft_ - k];
+14 -17
View File
@@ -8,23 +8,21 @@ struct DetectorBand {
float q; // resonance Q
float sens; // XML sens (dB); internal sens_stored = sens * 2.054
float level_scale = 1.0f; // calibration: level = am * res * level_scale
// Per-band mask LUT: gain = (1/(1+K*acc))^n (live-fit defaults band0).
float lut_k = 9.8026f;
float lut_n = 0.25966f;
// Dry/wet depth (XML "depth", 0.864 in reference renders). Applied only when
// set >= 0; default -1 keeps the live LUT fit intact (fit already absorbed
// band0's dry/wet blend).
float depth = -1.0f;
};
// FramedDetector — C++ port of the real soothe mask chain (FUN_180529fe0).
// Per band, per bin:
// level_k = am_k * res_k (twin resonance x smoothed amplitude)
// track += w_k * (level - track) (per-bin level tracker, attack/release
// weights w from live capture rt_weights)
// acc_k = 2*level_k - track_k (accumulator, live peak ~10.08 at tone)
// mask_k = LUT(acc_k) (fitted mask curve from live capture)
// final mask = min over bands (max suppression), like soothe's band combine.
// FramedDetector — C++ transcription of the real soothe2 mask-apply chain
// (FUN_180529fe0 mono path, 0x5408b8==0), bit-exact structure.
//
// Per band, per bin (exact decomp /tmp/consumers_out.txt:638-1111):
// 1. scale: x = level * (fVar30/0x1a0) * 0x540870 * 0x54088c
// 2. IIR1 leaky: y[i] = A1[i]*acc + B1[i]*x[i] (A1/B1 ramp attack, live tables)
// 3. IIR2 leaky: same with A2/B2 (slow release)
// 4. blend: b = freqaxis*(1-mix) + mix*0.8; mask = exp2(0.5*(mask-b))
// 5. combine: acc[band] = mask - b; mirror; += w_att*upper; += w_rel*lower; += mask
// 6. warp: mask *= 0x540768[band]; mask *= warp (dual tilt)
// 7. IIR3 leaky: twice with A3/B3
// 8. dry/wet: mask = mask*(fVar30*0x540888) + (1-fVar30)
// final = min over bands.
class FramedDetector {
public:
FramedDetector(size_t nfft, float sample_rate);
@@ -41,7 +39,6 @@ private:
std::vector<DetectorBand> bands_;
std::vector<std::vector<float>> res_; // per band, per bin |2B/A|
std::vector<float> warp_; // per-bin warp tilt
std::vector<float> am_; // smoothed per-bin amplitude
std::vector<std::vector<float>> track_; // per band, per bin level tracker
std::vector<std::vector<float>> track_; // per band, per bin accumulator 0x5407c8
};
+2888 -2888
View File
File diff suppressed because it is too large Load Diff