diff --git a/dsp/CMakeLists.txt b/dsp/CMakeLists.txt index 44ee14c..64f7262 100644 --- a/dsp/CMakeLists.txt +++ b/dsp/CMakeLists.txt @@ -24,6 +24,7 @@ add_library(soothe2_dsp SHARED exp2.cpp leveltrack.cpp framed_model.cpp + fn529fe0.cpp rt_weights.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(levelpath_check levelpath_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(framed_test 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(fftconv_check soothe2_dsp) target_link_libraries(vlog_check soothe2_dsp) diff --git a/dsp/fn529fe0.cpp b/dsp/fn529fe0.cpp new file mode 100644 index 0000000..87f8cc1 --- /dev/null +++ b/dsp/fn529fe0.cpp @@ -0,0 +1,64 @@ +#include "fn529fe0.hpp" +#include +#include +#include + +// 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(x[i]); + acc = y; + x[i] = static_cast(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(freqaxis[i]) * (1.0 - mix) + mix * 0.8; + // mask = exp2(-x) * blend (x is level; attenuation => exp2(-level)) + mask[i] = static_cast(std::exp2(-static_cast(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(band[i]) - static_cast(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(wAtt[i]) * static_cast(f6f8[i]); + acc[i] += static_cast(wRel[i]) * static_cast(f6f8[i]); + } + // += band (0x5a20), full nfft + for (size_t i = 0; i < half; i++) { + acc[i] += static_cast(band[i]); + acc[nfft - 1 - i] += static_cast(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 diff --git a/dsp/fn529fe0.hpp b/dsp/fn529fe0.hpp new file mode 100644 index 0000000..aedf54e --- /dev/null +++ b/dsp/fn529fe0.hpp @@ -0,0 +1,41 @@ +#pragma once +#include +#include + +// 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 diff --git a/dsp/fn529fe0_check.cpp b/dsp/fn529fe0_check.cpp new file mode 100644 index 0000000..c316b28 --- /dev/null +++ b/dsp/fn529fe0_check.cpp @@ -0,0 +1,83 @@ +#include +#include +#include +#include +#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 x(nbin); + std::vector acc1(nbin); + for (size_t i = 0; i < nbin; i++) x[i] = (i < 100 ? 0.0f : 1.0f); + std::vector 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 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 acc(nfft, 0.0); + std::vector 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 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; +}