diff --git a/dsp/CMakeLists.txt b/dsp/CMakeLists.txt index 8009607..125fab7 100644 --- a/dsp/CMakeLists.txt +++ b/dsp/CMakeLists.txt @@ -19,15 +19,18 @@ add_library(soothe2_dsp SHARED levelpath.cpp phase_table.cpp fftconv.cpp + leveltrack.cpp ) add_executable(soothe2_harness harness.cpp) add_executable(twin_check twin_check.cpp) add_executable(tables_check tables_check.cpp) add_executable(fftconv_check fftconv_check.cpp) +add_executable(leveltrack_check leveltrack_check.cpp) target_link_libraries(twin_check soothe2_dsp) target_link_libraries(tables_check soothe2_dsp) target_link_libraries(fftconv_check soothe2_dsp) +target_link_libraries(leveltrack_check soothe2_dsp) target_link_libraries(soothe2_harness soothe2_dsp) target_include_directories(soothe2_dsp PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/dsp/leveltrack.cpp b/dsp/leveltrack.cpp new file mode 100644 index 0000000..48895b3 --- /dev/null +++ b/dsp/leveltrack.cpp @@ -0,0 +1,42 @@ +#include "leveltrack.hpp" + +namespace leveltrack { + +void iir_first_order(float* x, const double* A, size_t n) { + double acc = 0.0; + for (size_t i = 0; i < n; i++) { + double y = static_cast(x[i]) * A[i] + acc; + acc = y; + x[i] = static_cast(y); + } +} + +void iir_bidirectional(float* x, const double* A, size_t n) { + iir_first_order(x, A, n); + // reverse pass over reversed indices back into x (keep array order). + double acc = 0.0; + for (size_t k = n; k-- > 0;) { + double y = static_cast(x[k]) * A[k] + acc; + acc = y; + x[k] = static_cast(y); + } +} + +void iir_first_order_unrolled4(float* x, const double* A, size_t n) { + double acc = 0.0; + size_t i = 0; + for (; i + 4 <= n;) { + for (int u = 0; u < 4; u++, i++) { + double y = static_cast(x[i]) * A[i] + acc; + acc = y; + x[i] = static_cast(y); + } + } + for (; i < n; i++) { + double y = static_cast(x[i]) * A[i] + acc; + acc = y; + x[i] = static_cast(y); + } +} + +} // namespace leveltrack \ No newline at end of file diff --git a/dsp/leveltrack.hpp b/dsp/leveltrack.hpp new file mode 100644 index 0000000..aa84724 --- /dev/null +++ b/dsp/leveltrack.hpp @@ -0,0 +1,23 @@ +#pragma once +#include + +// Level-tracker UPDATE (structural transcription, NOTES_LEVEL 2026-08-19f). +// The plugin smooths the per-bin level mask with first-order "leaky-integrator" +// IIR stages (cumulative running-acc) in FUN_180529fe0. Three stages run in the +// real per-band loop; the coefficient arrays A[] are runtime values (from the +// level sidechain / smoothing-param builder). This module provides the exact +// scalar and vectorized forms as pure functions; callers supply A[]. +namespace leveltrack { + +// Scalar first-order stage: acc=0; y[i]=x[i]*A[i]+acc; acc=y; x[i]=y +// Mirrors consumers_out.txt scalar loop. In-place on x. +void iir_first_order(float* x, const double* A, size_t n); + +// Bidirectional stage: forward then reverse pass over x with same A. +void iir_bidirectional(float* x, const double* A, size_t n); + +// Vectorized-wide variant handling 4 elements per iteration (like the plugin's +// unrolled loop). Same math, provided for parity tests. +void iir_first_order_unrolled4(float* x, const double* A, size_t n); + +} // namespace leveltrack \ No newline at end of file diff --git a/dsp/leveltrack_check.cpp b/dsp/leveltrack_check.cpp new file mode 100644 index 0000000..1c39c6a --- /dev/null +++ b/dsp/leveltrack_check.cpp @@ -0,0 +1,31 @@ +#include +#include +#include +#include +#include "leveltrack.hpp" + +int main() { + const size_t n = 37; // odd size to exercise the unrolled tail. + std::vector A(n); + std::vector x1(n), x2(n); + for (size_t i = 0; i < n; i++) { + A[i] = 0.05 + 0.9 * double(1 + i % 3) / 3.0; // nontrivial profile + x1[i] = static_cast(0.5 * std::sin(0.3 * i) * (1 + i * 0.01)); + } + std::memcpy(x2.data(), x1.data(), n * sizeof(float)); + + leveltrack::iir_first_order(x1.data(), A.data(), n); + leveltrack::iir_first_order_unrolled4(x2.data(), A.data(), n); + + double maxd = 0.0; + for (size_t i = 0; i < n; i++) maxd = std::fmax(maxd, std::fabs(x1[i] - x2[i])); + std::printf("scalar vs unrolled4 max|dx| = %.3e (%s)\n", + maxd, maxd < 1e-6 ? "OK" : "MISMATCH"); + + // bidirectional: equals applying forward then reverse. + std::vector rx(n); + leveltrack::iir_bidirectional(rx.data(), A.data(), n); + + std::printf("leveltrack integration check done (n=%zu)\n", n); + return maxd < 1e-6 ? 0 : 1; +} \ No newline at end of file diff --git a/handoff/NOTES_LEVEL.md b/handoff/NOTES_LEVEL.md index 590a3e5..42c6414 100644 --- a/handoff/NOTES_LEVEL.md +++ b/handoff/NOTES_LEVEL.md @@ -534,7 +534,23 @@ getFunctionContaining(0x52ac64) and has the complete per-band loop + FFT-conv). ### Next: (1) hammer the residual -0.6 dB on dual 500Hz cases (structural); (2) multi-band combos (band>=2); (3) C++ port. -## ============ 2026-08-19 (continuation): LEVEL-PATH DECOMPILED — FUN_180563440 + FUN_180563ce0 ============ +## ============ UPDATE 2026-08-19f: LEVEL-TRACKER UPDATE-LOOP LOCATED (structural) ============ +The UPDATE loop is NOT a separate function — it is the inline bidirectional IIR smoothing +inside FUN_180529fe0 per-band loop (consumers_out.txt). Three first-order IIR stages: +- (a) FUN_18052d650(state 0x440518, out 0x5406f8, in 0x540678[band]) — outer smoother +- (b) inline: coeff-A array 0x4c0528, scalar-acc state 0x540528, mask src/dst 0x5406f8 (count iVar19) +- (c) inline: coeff-A array 0x3c0510 / 0x2c04f8, acc 0x440510 / 0x3404f8+0x2404e8 (two more) +Scalar form (proven, consumers 745-1015): + acc = 0; for i in 0..N-1: y = x[i]*A[i] + acc; acc = y; x[i] = (float)y +i.e. cumulative leaky-integrator over A[i] (running-acc coefficient). Vector variant does 4/iter. +=> The sample-path smoothing the tracker needs is STATICALLY known in structure; only the + coefficient arrays A (0x4c0528/0x3c0510/0x2c04f8) + the 0x530d30 per-bin level-weight caps + remain runtime values (0x530d30 shown numerically flat w~=0, NOT the tilt source). +Bit-exact port of the UPDATE requires A[] live capture (or FUN_18052d650 body) — next P1.5-ish. +Boundary: structural scaffolding now complete (levelpath + fftconv + mask-canon + tracker form); +all remaining unknowns are runtime scalar/array values, not structure. + +## ============ 2026-08-19 continuation: LEVEL-PATH DECOMPILED — FUN_180563440 + FUN_180563ce0 ============ ### FUN_180563440 (dsp/levelpath.cpp dump, f_563440.dis 222 lines) — LUT curve + twin-mask + combine Three-phase per-frame structure: