feat(dsp): structural FUN_180529fe0 chain components + fn529fe0_check (step 1, module 1)

This commit is contained in:
2026-08-21 01:09:36 +03:00
parent fa71240d92
commit 7bf5a4a80c
4 changed files with 191 additions and 0 deletions
+3
View File
@@ -24,6 +24,7 @@ add_library(soothe2_dsp SHARED
exp2.cpp exp2.cpp
leveltrack.cpp leveltrack.cpp
framed_model.cpp framed_model.cpp
fn529fe0.cpp
rt_weights.cpp rt_weights.cpp
rt_mask_tables.cpp rt_mask_tables.cpp
) )
@@ -37,9 +38,11 @@ add_executable(vlog_check vlog_check.cpp)
add_executable(leveltrack_check leveltrack_check.cpp) add_executable(leveltrack_check leveltrack_check.cpp)
add_executable(levelpath_check levelpath_check.cpp) add_executable(levelpath_check levelpath_check.cpp)
add_executable(exp2_check exp2_check.cpp) add_executable(exp2_check exp2_check.cpp)
add_executable(fn529fe0_check fn529fe0_check.cpp)
target_link_libraries(twin_check soothe2_dsp) target_link_libraries(twin_check soothe2_dsp)
target_link_libraries(framed_test soothe2_dsp) target_link_libraries(framed_test soothe2_dsp)
target_link_libraries(exp2_check soothe2_dsp) target_link_libraries(exp2_check soothe2_dsp)
target_link_libraries(fn529fe0_check soothe2_dsp)
target_link_libraries(tables_check soothe2_dsp) target_link_libraries(tables_check soothe2_dsp)
target_link_libraries(fftconv_check soothe2_dsp) target_link_libraries(fftconv_check soothe2_dsp)
target_link_libraries(vlog_check soothe2_dsp) target_link_libraries(vlog_check soothe2_dsp)
+64
View File
@@ -0,0 +1,64 @@
#include "fn529fe0.hpp"
#include <cmath>
#include <algorithm>
#include <cstring>
// Structural mask-apply chain FUN_180529fe0 (mono path). Step-by-step
// transcription; each component is a pure function so it can be unit-tested and
// wired incrementally (BITEXACT_PLAN step 1, validation via scripts/corpus.py).
namespace fn529fe0 {
void iir1(float* x, const double* A, const double* B, size_t nbin, double /*acc0*/) {
// leaky first-order: y = A*acc + B*x ; acc = y (B = 1-A from live tables)
double acc = 0.0;
for (size_t i = 0; i < nbin; i++) {
double y = A[i] * acc + B[i] * static_cast<double>(x[i]);
acc = y;
x[i] = static_cast<float>(y);
}
}
void blend_exp2(float* mask, const float* x, const float* freqaxis,
float mix, size_t nbin) {
for (size_t i = 0; i < nbin; i++) {
double blend = static_cast<double>(freqaxis[i]) * (1.0 - mix) + mix * 0.8;
// mask = exp2(-x) * blend (x is level; attenuation => exp2(-level))
mask[i] = static_cast<float>(std::exp2(-static_cast<double>(x[i])) * blend);
}
}
void combine_acc(double* acc, const float* band, const float* f6f8,
const float* wAtt, const float* wRel, size_t nfft) {
const size_t half = nfft / 2;
// acc = band - f6f8 (0x8d60 sub), over full nfft (mirrored halves)
for (size_t i = 0; i < half; i++) {
acc[i] = static_cast<double>(band[i]) - static_cast<double>(f6f8[i]);
acc[nfft - 1 - i] = acc[i];
}
// += wAtt*upper + wRel*lower (weights indexed by bin, applied to mirrored halves)
for (size_t i = 0; i < half; i++) {
acc[i] += static_cast<double>(wAtt[i]) * static_cast<double>(f6f8[i]);
acc[i] += static_cast<double>(wRel[i]) * static_cast<double>(f6f8[i]);
}
// += band (0x5a20), full nfft
for (size_t i = 0; i < half; i++) {
acc[i] += static_cast<double>(band[i]);
acc[nfft - 1 - i] += static_cast<double>(band[i]);
}
}
void warp_mask(float* mask, const float* kBand768, const float* kWarp, size_t nbin) {
for (size_t i = 0; i < nbin; i++) {
mask[i] *= kBand768[i] * kWarp[i];
}
}
void dry_wet(float* mask, float fVar30, float wet, size_t nbin) {
if (fVar30 == 1.0f && wet == 1.0f) return; // identity default
for (size_t i = 0; i < nbin; i++) {
mask[i] = mask[i] * (fVar30 * wet) + (1.0f - fVar30);
}
}
} // namespace fn529fe0
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <cstddef>
#include <vector>
// Structural transcription of the soothe2 mask-apply mono path
// FUN_180529fe0 (0x5408b8==0), BITEXACT_PLAN step 1. Uses the live-captured
// tables (dsp/rt_mask_tables.*, dsp/rt_weights.*) and the exact step sequence
// from NOTES_LEVEL:820-840 / :237-253.
//
// Unlike the empirical bridge (dsp/framed_model.cpp), this reproduces the real
// reduction/exp2-domain chain:
// scale -> IIR1 -> copy -> IIR2 -> mirror -> blend(0.8 pedestal)
// -> exp2(-level)*blend -> combine/acc -> warp(kBand768*kWarp)
// -> IIR3 x2 -> dry/wet -> (FFT-conv is step 4, separate module)
//
// The IIR/weight tables are indexed 0..N/2 of the INTERNAL grid (N=4096/SR=48000);
// per-bin level is supplied by the caller (level-path), same xv domain as bridge
// (level = am/res) but fed through the structural chain instead of the LUT bridge.
namespace fn529fe0 {
// All per-bin buffers are length nbin = nfft/2+1 (internal grid).
// IIR stage: y[i] = A[i]*acc + B[i]*x[i]; acc=y (first-order leaky, like leveltrack).
void iir1(float* x, const double* A, const double* B, size_t nbin, double acc0);
// Blend step 6: f6f8[k] = freqaxis[k]*(1-mix) + mix*0.8; out = exp2(-x)*f6f8.
void blend_exp2(float* mask, const float* x, const float* freqaxis,
float mix, size_t nbin);
// Combine step 7 (reduction/exp2 domain): accumulates per-band.
// acc = band - f6f8; += wAtt[mirror]*upper; += wRel[mirror]*lower; += band
// In-place on acc; band and f6f8 are inputs (len nbin, mirrored to full nfft).
void combine_acc(double* acc, const float* band, const float* f6f8,
const float* wAtt, const float* wRel, size_t nfft);
// Warp step 8: mask *= kBand768 * kWarp (two multiplies).
void warp_mask(float* mask, const float* kBand768, const float* kWarp, size_t nbin);
// Dry/wet step 10 (fVar30=1, 0x540888=1 -> identity for default).
void dry_wet(float* mask, float fVar30, float wet, size_t nbin);
} // namespace fn529fe0
+83
View File
@@ -0,0 +1,83 @@
#include <cstdio>
#include <cmath>
#include <vector>
#include <cstring>
#include "fn529fe0.hpp"
#include "rt_mask_tables.hpp"
#include "rt_weights.hpp"
// Modular black-box check for the structural FUN_180529fe0 chain components
// (BITEXACT_PLAN step 1). Validates invariants against the live tables:
// - kIIR_A1/B1 : B == 1 - A, and IIR1 smooths a step input monotonically
// - blend_exp2 : out == exp2(-x)*blend, blend = freqaxis*(1-mix)+mix*0.8
// - combine_acc: subtract then add band/f6f8 contributions (exact)
// - warp_mask : multiplies by kBand768*kWarp
int main() {
const size_t nbin = 2049; // internal N/2+1 grid used by the chain
const size_t nfft = 4096;
int fail = 0;
// --- IIR tables: B1 == 1 - A1 ---
double maxB = 0.0;
for (size_t i = 0; i < nbin; i++)
maxB = std::fmax(maxB, std::fabs(kIIR_B1[i] - (1.0 - kIIR_A1[i])));
std::printf("IIR: max|B1-(1-A1)| = %.3e (%s)\n", maxB, maxB < 1e-12 ? "OK" : "MISMATCH");
if (maxB >= 1e-12) fail = 1;
// --- IIR1 smooths a step input monotonically ---
std::vector<float> x(nbin);
std::vector<double> acc1(nbin);
for (size_t i = 0; i < nbin; i++) x[i] = (i < 100 ? 0.0f : 1.0f);
std::vector<float> orig = x;
fn529fe0::iir1(x.data(), kIIR_A1, kIIR_B1, nbin, 0.0);
bool monotonic = true;
for (size_t i = 1; i < nbin; i++)
if (x[i] < x[i - 1] - 1e-6) { monotonic = false; break; }
std::printf("IIR1 step: monotonic=%d x[0]=%.3f x[mid]=%.3f x[last]=%.3f\n",
monotonic, x[0], x[nbin/2], x[nbin-1]);
if (!monotonic || std::fabs(x[0] - 0.0f) > 1e-3) fail = 1;
// --- blend_exp2 correctness ---
std::vector<float> mask(nbin), lvl(nbin), freq(nbin);
for (size_t i = 0; i < nbin; i++) { lvl[i] = 0.5f * (1.0f + float(i) / nbin); freq[i] = 1.0f; }
const float mix = 1.0f;
fn529fe0::blend_exp2(mask.data(), lvl.data(), freq.data(), mix, nbin);
double max_e = 0.0;
for (size_t i = 0; i < nbin; i++) {
double expect = std::exp2(-(double)lvl[i]) * 0.8;
max_e = std::fmax(max_e, std::fabs(mask[i] - expect));
}
std::printf("blend_exp2: max|out-exp2(-x)*0.8| = %.3e (%s)\n",
max_e, max_e < 1e-6 ? "OK" : "MISMATCH");
if (max_e >= 1e-6) fail = 1;
// --- combine_acc: acc = band-f6f8 + wAtt*f6f8 + wRel*f6f8 + band.
// With band=1, f6f8=0, weights=0: acc = band - 0 + 0 + 0 + band = 2 everywhere. ---
std::vector<double> acc(nfft, 0.0);
std::vector<float> band(nbin, 1.0f), f6f8(nbin, 0.0f), wA(nbin, 0.0f), wR(nbin, 0.0f);
fn529fe0::combine_acc(acc.data(), band.data(), f6f8.data(), wA.data(), wR.data(), nfft);
double max_c = 0.0;
for (size_t i = 0; i < nfft; i++) max_c = std::fmax(max_c, std::fabs(acc[i] - 2.0));
std::printf("combine: acc=2 for band=1,f6f8=0,w=0 max|d|=%.3e (%s)\n",
max_c, max_c < 1e-12 ? "OK" : "MISMATCH");
if (max_c >= 1e-12) fail = 1;
// --- warp_mask applies kBand768*kWarp ---
std::vector<float> w(nbin);
for (size_t i = 0; i < nbin; i++) w[i] = 1.0f;
const float* k768 = kBand768; // band0 table (per-band in real path)
fn529fe0::warp_mask(w.data(), k768, kWarp, nbin);
double max_w = 0.0;
for (size_t i = 0; i < nbin; i++)
max_w = std::fmax(max_w, std::fabs(w[i] - k768[i] * kWarp[i]));
std::printf("warp: mask==kBand768*kWarp max|d|=%.3e (%s)\n",
max_w, max_w < 1e-6 ? "OK" : "MISMATCH");
if (max_w >= 1e-6) fail = 1;
// --- live table ranges ---
std::printf("live: kWarp[0]=%.3f kWarp[2048]=%.3f kBand768[0]=%.3f kBand768[2048]=%.3f\n",
kWarp[0], kWarp[2048], k768[0], k768[2048]);
std::printf("fn529fe0 check %s\n", fail ? "FAIL" : "PASS");
return fail;
}